Index: utils/lit/lit/formats/__init__.py =================================================================== --- utils/lit/lit/formats/__init__.py +++ utils/lit/lit/formats/__init__.py @@ -1,4 +1,6 @@ from __future__ import absolute_import from lit.formats.base import TestFormat, FileBasedTest, OneCommandPerFileTest +from lit.formats.suffixdispatchtest import (SuffixDispatchTest, + CompileAndExecuteTestRunner) from lit.formats.googletest import GoogleTest from lit.formats.shtest import ShTest Index: utils/lit/lit/formats/base.py =================================================================== --- utils/lit/lit/formats/base.py +++ utils/lit/lit/formats/base.py @@ -22,8 +22,7 @@ filepath = os.path.join(source_path, filename) if not os.path.isdir(filepath): - base,ext = os.path.splitext(filename) - if ext in localConfig.suffixes: + if any((filepath.endswith(ext) for ext in localConfig.suffixes)): yield lit.Test.Test(testSuite, path_in_suite + (filename,), localConfig) Index: utils/lit/lit/formats/suffixdispatchtest.py =================================================================== --- /dev/null +++ utils/lit/lit/formats/suffixdispatchtest.py @@ -0,0 +1,135 @@ +from __future__ import absolute_import +import errno +import os +import sys +import tempfile + +import lit.Test +import lit.util +from .base import FileBasedTest + +class SuffixDispatchTest(FileBasedTest): + """ + A custom test format that stores a list of (suffix, test_runner) pairs. + SuffixDispatchTest dispatches tests based on their suffix to different + test runners. Test runners must have function signature of + # __call__(self, test, lit_config). + """ + def __init__(self, suffixes=[]): + super(FileBasedTest, self).__init__() + self.suffix_tests = list(suffixes) + + def add_suffix_test(self, suffix, test_runner): + self.suffix_tests += [tuple((suffix, test_runner))] + + def remove_suffix_test(self, suffix): + self.suffix_tests = [t for t in self.suffix_tests if t[0] != suffix] + + def execute(self, test, lit_config): + while True: + try: + return self._execute(test, lit_config) + except OSError, oe: + if oe.errno != errno.ETXTBSY: + raise + time.sleep(0.1) + + def _execute(self, test, lit_config): + # Extract test metadata from the test file. + requires = [] + unsupported = [] + # A helper function that reads a line of features that follow a tag. + def _parse_metadata_line(ln, tag, out): + if tag not in ln: + return False + items = ln[ln.index(tag) + len(tag):].split(',') + items = [s.strip() for s in items if s.strip()] + out.extend(items) + return True + with open(test.getSourcePath()) as f: + for ln in f: + if _parse_metadata_line(ln, 'XFAIL:', test.xfails): + pass + elif _parse_metadata_line(ln, 'REQUIRES:', requires): + pass + elif _parse_metadata_line(ln, 'UNSUPPORTED:', unsupported): + pass + elif not ln.strip().startswith("//") and ln.strip(): + # Stop at the first non-empty line that is not a C++ + # comment. + break + + missing_required_features = [f for f in requires + if f not in test.config.available_features + or f not in test.config.target_triple] + if missing_required_features: + return (lit.Test.UNSUPPORTED, + "Test requires the following features: %s" % ( + ', '.join(missing_required_features))) + + unsupported_features = [f for f in unsupported + if f in test.config.available_features + or f in test.config.target_triple + or f == '*'] + if unsupported_features: + return (lit.Test.UNSUPPORTED, + "Test is unsupported with the following features: %s" % ( + ', '.join(unsupported_features))) + + # Dispatch the test based on name and execute it. + name = test.path_in_suite[-1] + test_runner = None + for (suffix, runner) in self.suffix_tests: + if name.endswith(suffix): + test_runner = runner + break + if test_runner is None: + # Kill lit if we are given a test that doesn't match an existing + # suffix + lit_config.fatal('Unrecognized test suffix: %s' % name) + + return test_runner(test, lit_config) + +## + +class CompileAndExecuteTestRunner(object): + def __init__(self, cxx, exec_env={}): + self.cxx = cxx.clone() + self.exec_env = dict(exec_env) + + def __call__(self, test, lit_config): + source_path = test.getSourcePath() + source_dir = os.path.dirname(source_path) + exec_file = tempfile.NamedTemporaryFile(suffix="exe", delete=False) + exec_path = exec_file.name + exec_file.close() + report = "" + + try: + compile_cmd = self.cxx.compileLinkCmd(['-o', exec_path, source_path]) + cmd = compile_cmd + out, err, exit_code, report = lit.util.executeCommandAndReport(cmd) + if exit_code != 0: + return lit.Test.FAIL, report + "Compilation failed unexpectedly!" + + cmd = [] + if self.exec_env: + cmd.append('env') + cmd.extend('%s=%s' % (name, value) + for name, value in self.exec_env.items()) + cmd.append(exec_path) + if lit_config.useValgrind: + cmd = lit_config.valgrindArgs + cmd + out, err, exit_code, report = lit.util.executeCommandAndReport(cmd, + cwd=source_dir) + report = 'Compiled With: %s\n' % \ + ' '.join(["'%s'" % a for a in compile_cmd]) + report + if exit_code != 0: + return lit.Test.FAIL, report + "Compiled test failed unexpectedly!" + finally: + try: + os.remove(exec_path) + except: + pass + return lit.Test.PASS, report +## Index: utils/lit/lit/util.py =================================================================== --- utils/lit/lit/util.py +++ utils/lit/lit/util.py @@ -3,6 +3,8 @@ import math import os import platform +import pty +import select import signal import subprocess import sys @@ -143,6 +145,19 @@ # Close extra file handles on UNIX (on Windows this cannot be done while # also redirecting input). kUseCloseFDs = not (platform.system() == 'Windows') + +def _to_string(str_bytes): + if isinstance(str_bytes, str): + return str_bytes + return str_bytes.encode('utf-8') + +def _convert_string(str_bytes): + try: + return _to_string(str_bytes.decode('utf-8')) + except: + return str(str_bytes) + + def executeCommand(command, cwd=None, env=None): p = subprocess.Popen(command, cwd=cwd, stdin=subprocess.PIPE, @@ -151,28 +166,97 @@ env=env, close_fds=kUseCloseFDs) out,err = p.communicate() exitCode = p.wait() - # Detect Ctrl-C in subprocess. if exitCode == -signal.SIGINT: raise KeyboardInterrupt - def to_string(bytes): - if isinstance(bytes, str): - return bytes - return bytes.encode('utf-8') - # Ensure the resulting output is always of string type. + out = _convert_string(out) + err = _convert_string(err) + + return out, err, exitCode + +def _read_and_close_pty(p): + os.close(p[1]) + with os.fdopen(p[0], 'r') as f: + out = '' + try: + out = _convert_string(f.read()) + except IOError: + pass + return out + +def executeCommandPTY(command, cwd=None, env=None): try: - out = to_string(out.decode('utf-8')) - except: - out = str(out) - try: - err = to_string(err.decode('utf-8')) + out_pty = tuple(pty.openpty()) + err_pty = tuple(pty.openpty()) + kwargs = { + 'stdin': subprocess.PIPE, + 'stdout': out_pty[1], + 'stderr': err_pty[1], + 'cwd': cwd, + 'env': env, + 'close_fds':kUseCloseFDs, + } + p = subprocess.Popen(command, **kwargs) + exitCode = None + out = '' + err = '' + while True: + exitCode = p.poll() + if exitCode is not None: + break + to_read,_,_ = select.select([out_pty[0], err_pty[0]], [], [], 0.0) + for fd in to_read: + data = _convert_string(os.read(fd, 4096)) + if fd == out_pty[0]: + out += data + else: + err += data + # Detect Ctrl-C in subprocess. + if exitCode == -signal.SIGINT: + raise KeyboardInterrupt + out += _read_and_close_pty(out_pty) + err += _read_and_close_pty(err_pty) except: - err = str(err) - + for fd in out_pty + err_pty: + try: + os.close(fd) + except: + pass + raise return out, err, exitCode + + +def _executeCommandAndReport(exec_fn, command, cwd=None, env=None): + """ Execute a command and generate a report represeting the command and + other input as well as the ouput of the command.""" + out, err, exitCode = exec_fn(command, cwd=cwd, env=env) + report = "Command: %s\n" % \ + ' '.join(["'%s'" % a for a in command]) + if cwd: + report += "Current Directory: %s\n" % cwd + if env: + report += "Environment: %s\n" % \ + ' '.join(["'%s'='%s'" % (k,env[k]) for k in env]) + report += "Exit Code: %d\n" % exitCode + if out: + report += "Standard Output:\n--\n%s--\n" % out + if err: + report += "Standard Error:\n--\n%s--\n" % err + report += '\n' + return out, err, exitCode, report + + +def executeCommandAndReport(command, cwd=None, env=None): + return _executeCommandAndReport(executeCommand, command, cwd=cwd, env=env) + + +def executeCommandAndReportPTY(command, cwd=None, env=None): + return _executeCommandAndReport(executeCommandPTY, command, cwd=cwd, env=env) + + def usePlatformSdkOnDarwin(config, lit_config): # On Darwin, support relocatable SDKs by providing Clang with a # default system root path. @@ -189,3 +273,35 @@ sdk_path = out lit_config.note('using SDKROOT: %r' % sdk_path) config.environment['SDKROOT'] = sdk_path + + +# A simple yet very useful abstraction for represeting a compiler. +# Used in formats/suffixdispatchtest.py +class Compiler(object): + def __init__(self, path, compile_flags=[], link_flags=[]): + self.path = str(path) + self.compile_flags = list(compile_flags) + self.link_flags = list(link_flags) + + def clone(self): + return Compiler(self.path, self.compile_flags, self.link_flags) + + def compileCmd(self, compile_flags=[]): + return [self.path, '-c'] + self.compile_flags + compile_flags + + def linkCmd(self, link_flags=[]): + return [self.path] + self.link_flags + link_flags + + def compileLinkCmd(self, compile_flags=[], link_flags=[]): + return [self.path] + self.compile_flags + compile_flags \ + + self.link_flags + link_flags + + def compile(self, compile_flags=[]): + return executeCommandAndReport(self.compileCmd(compile_flags)) + + def link(self, link_flags=[]): + return executeCommandAndReport(self.linkCmd(link_flags)) + + def compileLink(self, compile_flags=[], link_flags=[]): + return executeCommandAndReport( + self.compileLinkCmd(compile_flags, link_flags))