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,10 +22,10 @@ filepath = os.path.join(source_path, filename) if not os.path.isdir(filepath): - base,ext = os.path.splitext(filename) - if ext in localConfig.suffixes: - yield lit.Test.Test(testSuite, path_in_suite + (filename,), - localConfig) + for ext in localConfig.suffixes: + if filepath.endswith(ext): + 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,173 @@ +from __future__ import absolute_import +import os +import sys +import errno +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) + + @staticmethod + def _handle_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 + + def _execute(self, test, lit_config): + # Extract test metadata from the test file. + requires = [] + unsupported = [] + unsupported_xfail = [] + requires_xfail = [] + use_verify = False + with open(test.getSourcePath()) as f: + for ln in f: + if self._handle_metadata_line(ln, 'REQUIRES-XFAIL:', requires_xfail): + pass + elif self._handle_metadata_line(ln, 'UNSUPPORTED-XFAIL:', unsupported_xfail): + pass + elif self._handle_metadata_line(ln, 'XFAIL:', test.xfails): + pass + elif self._handle_metadata_line(ln, 'REQUIRES:', requires): + pass + elif self._handle_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 + + # Check that we have the required features. + # + # FIXME: For now, this is cribbed from lit.TestRunner, to avoid + # introducing a dependency there. What we more ideally would like to do + # is lift the "requires" handling to be a core lit framework feature. + 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),)) + + # Compute list of features that make a test unsupported but also check + # that it fails + missing_required_xfail_features = [f for f in requires_xfail + if f not in test.config.available_features + and f not in test.config.target_triple] + unsupported_xfail_features = [f for f in unsupported_xfail + if f in test.config.available_features + or f in test.config.target_triple + or f == '*'] + + + # Dispatch the test based on name and execute it. + name = test.path_in_suite[-1] + test_runner = None + for suffix_test in self.suffix_tests: + if name.endswith(suffix_test[0]): + test_runner = suffix_test[1] + break + if test_runner is None: + lit_config.fatal('Unrecognized test suffix: %s' % name) + + status,report,msg = test_runner(test, lit_config) + # If the test is REQUIRES-XFAIL on UNSUPPORTED-XFAIL translate the + # result of the test to reflect that. + if len(missing_required_xfail_features) != 0 or \ + len(unsupported_xfail_features) != 0: + if status == lit.Test.FAIL: + return lit.Test.UNSUPPORTED,"" + elif status == lit.Test.PASS: + report += "Test unexpectedly passed!" + return lit.Test.XPASS, report + else: + # TODO figure out what generates this + assert False + # Otherwise check for an error message and return the result + if msg: + report += msg + return status,report + +## + +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,exitCode,report = lit.util.executeCommandAndReport(cmd) + if exitCode != 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,exitCode,report = lit.util.executeCommandAndReport(cmd, + cwd=source_dir) + report = """Compiled With: %s\n""" % \ + ' '.join(["'%s'" % a for a in compile_cmd]) + report + if exitCode != 0: + return lit.Test.FAIL,report,"Compiled test failed unexpectedly!" + finally: + try: + os.remove(exec_path) + except: + pass + return lit.Test.PASS,report,"" +## \ No newline at end of file Index: utils/lit/lit/util.py =================================================================== --- utils/lit/lit/util.py +++ utils/lit/lit/util.py @@ -6,6 +6,8 @@ import signal import subprocess import sys +import pty +import select def detectCPUs(): """ @@ -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,95 @@ 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): + out,err,exitCode = exec_fn(command, cwd=cwd, env=env) + report = '' + 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 +271,33 @@ sdk_path = out lit_config.note('using SDKROOT: %r' % sdk_path) config.environment['SDKROOT'] = sdk_path + + +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))