lit: Add 'cd' support to the internal shell and port some tests
[oota-llvm.git] / utils / lit / lit / TestRunner.py
index 1a2df20477b445526b64a69b7b9fec2f24ef7849..f1734eca37263f36ac060e1c42a59bd580836ee5 100644 (file)
@@ -7,6 +7,7 @@ import tempfile
 import lit.ShUtil as ShUtil
 import lit.Test as Test
 import lit.util
+from lit.util import to_bytes, to_string
 
 class InternalShellError(Exception):
     def __init__(self, command, message):
@@ -21,33 +22,56 @@ kUseCloseFDs = not kIsWindows
 # Use temporary files to replace /dev/null on Windows.
 kAvoidDevNull = kIsWindows
 
-def executeShCmd(cmd, cfg, cwd, results):
+class ShellEnvironment(object):
+
+    """Mutable shell environment containing things like CWD and env vars.
+
+    Environment variables are not implemented, but cwd tracking is.
+    """
+
+    def __init__(self, cwd, env):
+        self.cwd = cwd
+        self.env = env
+
+def executeShCmd(cmd, shenv, results):
     if isinstance(cmd, ShUtil.Seq):
         if cmd.op == ';':
-            res = executeShCmd(cmd.lhs, cfg, cwd, results)
-            return executeShCmd(cmd.rhs, cfg, cwd, results)
+            res = executeShCmd(cmd.lhs, shenv, results)
+            return executeShCmd(cmd.rhs, shenv, results)
 
         if cmd.op == '&':
             raise InternalShellError(cmd,"unsupported shell operator: '&'")
 
         if cmd.op == '||':
-            res = executeShCmd(cmd.lhs, cfg, cwd, results)
+            res = executeShCmd(cmd.lhs, shenv, results)
             if res != 0:
-                res = executeShCmd(cmd.rhs, cfg, cwd, results)
+                res = executeShCmd(cmd.rhs, shenv, results)
             return res
 
         if cmd.op == '&&':
-            res = executeShCmd(cmd.lhs, cfg, cwd, results)
+            res = executeShCmd(cmd.lhs, shenv, results)
             if res is None:
                 return res
 
             if res == 0:
-                res = executeShCmd(cmd.rhs, cfg, cwd, results)
+                res = executeShCmd(cmd.rhs, shenv, results)
             return res
 
         raise ValueError('Unknown shell command: %r' % cmd.op)
-
     assert isinstance(cmd, ShUtil.Pipeline)
+
+    # Handle shell builtins first.
+    if cmd.commands[0].args[0] == 'cd':
+        # Update the cwd in the environment.
+        if len(cmd.commands[0].args) != 2:
+            raise ValueError('cd supports only one argument')
+        newdir = cmd.commands[0].args[1]
+        if os.path.isabs(newdir):
+            shenv.cwd = newdir
+        else:
+            shenv.cwd = os.path.join(shenv.cwd, newdir)
+        return 0
+
     procs = []
     input = subprocess.PIPE
     stderrTempFiles = []
@@ -101,7 +125,9 @@ def executeShCmd(cmd, cfg, cwd, results):
                     if kAvoidDevNull and r[0] == '/dev/null':
                         r[2] = tempfile.TemporaryFile(mode=r[1])
                     else:
-                        r[2] = open(r[0], r[1])
+                        # Make sure relative paths are relative to the cwd.
+                        redir_filename = os.path.join(shenv.cwd, r[0])
+                        r[2] = open(redir_filename, r[1])
                     # Workaround a Win32 and/or subprocess bug when appending.
                     #
                     # FIXME: Actually, this is probably an instance of PR6753.
@@ -131,7 +157,7 @@ def executeShCmd(cmd, cfg, cwd, results):
 
         # Resolve the executable path ourselves.
         args = list(j.args)
-        executable = lit.util.which(args[0], cfg.environment['PATH'])
+        executable = lit.util.which(args[0], shenv.env['PATH'])
         if not executable:
             raise InternalShellError(j, '%r: command not found' % j.args[0])
 
@@ -145,12 +171,12 @@ def executeShCmd(cmd, cfg, cwd, results):
                     args[i] = f.name
 
         try:
-            procs.append(subprocess.Popen(args, cwd=cwd,
+            procs.append(subprocess.Popen(args, cwd=shenv.cwd,
                                           executable = executable,
                                           stdin = stdin,
                                           stdout = stdout,
                                           stderr = stderr,
-                                          env = cfg.environment,
+                                          env = shenv.env,
                                           close_fds = kUseCloseFDs))
         except OSError as e:
             raise InternalShellError(j, 'Could not create process due to {}'.format(e))
@@ -256,7 +282,8 @@ def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
 
     results = []
     try:
-        exitCode = executeShCmd(cmd, test.config, cwd, results)
+        shenv = ShellEnvironment(cwd, test.config.environment)
+        exitCode = executeShCmd(cmd, shenv, results)
     except InternalShellError:
         e = sys.exc_info()[1]
         exitCode = 127
@@ -325,16 +352,8 @@ def parseIntegratedTestScriptCommands(source_path):
     # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
     # remaining code can work with "strings" agnostic of the executing Python
     # version.
-    
-    def to_bytes(str):
-        # Encode to UTF-8 to get binary data.
-        return str.encode('utf-8')
-    def to_string(bytes):
-        if isinstance(bytes, str):
-            return bytes
-        return to_bytes(bytes)
-        
-    keywords = ('RUN:', 'XFAIL:', 'REQUIRES:', 'END.')
+
+    keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
     keywords_re = re.compile(
         to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
 
@@ -368,11 +387,15 @@ def parseIntegratedTestScriptCommands(source_path):
     finally:
         f.close()
 
+
 def parseIntegratedTestScript(test, normalize_slashes=False,
-                              extra_substitutions=[]):
+                              extra_substitutions=[], require_script=True):
     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
-    information. The RUN lines also will have variable substitution performed.
+    and 'UNSUPPORTED' information. The RUN lines also will have variable
+    substitution performed. If 'require_script' is False an empty script may be
+    returned. This can be used for test formats where the actual script is
+    optional or ignored.
     """
 
     # Get the temporary location, this is always relative to the test suite
@@ -417,6 +440,7 @@ def parseIntegratedTestScript(test, normalize_slashes=False,
     # Collect the test lines from the script.
     script = []
     requires = []
+    unsupported = []
     for line_number, command_type, ln in \
             parseIntegratedTestScriptCommands(sourcepath):
         if command_type == 'RUN':
@@ -441,6 +465,8 @@ def parseIntegratedTestScript(test, normalize_slashes=False,
             test.xfails.extend([s.strip() for s in ln.split(',')])
         elif command_type == 'REQUIRES':
             requires.extend([s.strip() for s in ln.split(',')])
+        elif command_type == 'UNSUPPORTED':
+            unsupported.extend([s.strip() for s in ln.split(',')])
         elif command_type == 'END':
             # END commands are only honored if the rest of the line is empty.
             if not ln.strip():
@@ -465,11 +491,11 @@ def parseIntegratedTestScript(test, normalize_slashes=False,
               for ln in script]
 
     # Verify the script contains a run line.
-    if not script:
+    if require_script and not script:
         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
 
     # Check for unterminated run lines.
-    if script[-1][-1] == '\\':
+    if script and script[-1][-1] == '\\':
         return lit.Test.Result(Test.UNRESOLVED,
                                "Test has unterminated run lines (with '\\')")
 
@@ -480,22 +506,17 @@ def parseIntegratedTestScript(test, normalize_slashes=False,
         msg = ', '.join(missing_required_features)
         return lit.Test.Result(Test.UNSUPPORTED,
                                "Test requires the following features: %s" % msg)
+    unsupported_features = [f for f in unsupported
+                            if f in test.config.available_features]
+    if unsupported_features:
+        msg = ', '.join(unsupported_features)
+        return lit.Test.Result(Test.UNSUPPORTED,
+                    "Test is unsupported with the following features: %s" % msg)
 
     return script,tmpBase,execdir
 
-def executeShTest(test, litConfig, useExternalSh,
-                  extra_substitutions=[]):
-    if test.config.unsupported:
-        return (Test.UNSUPPORTED, 'Test is unsupported')
-
-    res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
-    if isinstance(res, lit.Test.Result):
-        return res
-    if litConfig.noExecute:
-        return lit.Test.Result(Test.PASS)
-
-    script, tmpBase, execdir = res
-
+def _runShTest(test, litConfig, useExternalSh,
+                   script, tmpBase, execdir):
     # Create the output directory if it does not already exist.
     lit.util.mkdir_p(os.path.dirname(tmpBase))
 
@@ -523,3 +544,19 @@ def executeShTest(test, litConfig, useExternalSh,
         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
 
     return lit.Test.Result(status, output)
+
+
+def executeShTest(test, litConfig, useExternalSh,
+                  extra_substitutions=[]):
+    if test.config.unsupported:
+        return (Test.UNSUPPORTED, 'Test is unsupported')
+
+    res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
+    if isinstance(res, lit.Test.Result):
+        return res
+    if litConfig.noExecute:
+        return lit.Test.Result(Test.PASS)
+
+    script, tmpBase, execdir = res
+    return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)
+