[lit] Speculatively fix PR24554 by manually closing the process handle
[oota-llvm.git] / utils / lit / lit / TestRunner.py
1 from __future__ import absolute_import
2 import os, signal, subprocess, sys
3 import re
4 import platform
5 import tempfile
6
7 import lit.ShUtil as ShUtil
8 import lit.Test as Test
9 import lit.util
10 from lit.util import to_bytes, to_string
11
12 class InternalShellError(Exception):
13     def __init__(self, command, message):
14         self.command = command
15         self.message = message
16
17 kIsWindows = platform.system() == 'Windows'
18
19 # Don't use close_fds on Windows.
20 kUseCloseFDs = not kIsWindows
21
22 # Use temporary files to replace /dev/null on Windows.
23 kAvoidDevNull = kIsWindows
24
25 class ShellEnvironment(object):
26
27     """Mutable shell environment containing things like CWD and env vars.
28
29     Environment variables are not implemented, but cwd tracking is.
30     """
31
32     def __init__(self, cwd, env):
33         self.cwd = cwd
34         self.env = dict(env)
35
36 def executeShCmd(cmd, shenv, results):
37     if isinstance(cmd, ShUtil.Seq):
38         if cmd.op == ';':
39             res = executeShCmd(cmd.lhs, shenv, results)
40             return executeShCmd(cmd.rhs, shenv, results)
41
42         if cmd.op == '&':
43             raise InternalShellError(cmd,"unsupported shell operator: '&'")
44
45         if cmd.op == '||':
46             res = executeShCmd(cmd.lhs, shenv, results)
47             if res != 0:
48                 res = executeShCmd(cmd.rhs, shenv, results)
49             return res
50
51         if cmd.op == '&&':
52             res = executeShCmd(cmd.lhs, shenv, results)
53             if res is None:
54                 return res
55
56             if res == 0:
57                 res = executeShCmd(cmd.rhs, shenv, results)
58             return res
59
60         raise ValueError('Unknown shell command: %r' % cmd.op)
61     assert isinstance(cmd, ShUtil.Pipeline)
62
63     # Handle shell builtins first.
64     if cmd.commands[0].args[0] == 'cd':
65         if len(cmd.commands) != 1:
66             raise ValueError("'cd' cannot be part of a pipeline")
67         if len(cmd.commands[0].args) != 2:
68             raise ValueError("'cd' supports only one argument")
69         newdir = cmd.commands[0].args[1]
70         # Update the cwd in the parent environment.
71         if os.path.isabs(newdir):
72             shenv.cwd = newdir
73         else:
74             shenv.cwd = os.path.join(shenv.cwd, newdir)
75         # The cd builtin always succeeds. If the directory does not exist, the
76         # following Popen calls will fail instead.
77         return 0
78
79     procs = []
80     input = subprocess.PIPE
81     stderrTempFiles = []
82     opened_files = []
83     named_temp_files = []
84     # To avoid deadlock, we use a single stderr stream for piped
85     # output. This is null until we have seen some output using
86     # stderr.
87     for i,j in enumerate(cmd.commands):
88         # Reference the global environment by default.
89         cmd_shenv = shenv
90         if j.args[0] == 'env':
91             # Create a copy of the global environment and modify it for this one
92             # command. There might be multiple envs in a pipeline:
93             #   env FOO=1 llc < %s | env BAR=2 llvm-mc | FileCheck %s
94             cmd_shenv = ShellEnvironment(shenv.cwd, shenv.env)
95             arg_idx = 1
96             for arg_idx, arg in enumerate(j.args[1:]):
97                 # Partition the string into KEY=VALUE.
98                 key, eq, val = arg.partition('=')
99                 # Stop if there was no equals.
100                 if eq == '':
101                     break
102                 cmd_shenv.env[key] = val
103             j.args = j.args[arg_idx+1:]
104
105         # Apply the redirections, we use (N,) as a sentinel to indicate stdin,
106         # stdout, stderr for N equal to 0, 1, or 2 respectively. Redirects to or
107         # from a file are represented with a list [file, mode, file-object]
108         # where file-object is initially None.
109         redirects = [(0,), (1,), (2,)]
110         for r in j.redirects:
111             if r[0] == ('>',2):
112                 redirects[2] = [r[1], 'w', None]
113             elif r[0] == ('>>',2):
114                 redirects[2] = [r[1], 'a', None]
115             elif r[0] == ('>&',2) and r[1] in '012':
116                 redirects[2] = redirects[int(r[1])]
117             elif r[0] == ('>&',) or r[0] == ('&>',):
118                 redirects[1] = redirects[2] = [r[1], 'w', None]
119             elif r[0] == ('>',):
120                 redirects[1] = [r[1], 'w', None]
121             elif r[0] == ('>>',):
122                 redirects[1] = [r[1], 'a', None]
123             elif r[0] == ('<',):
124                 redirects[0] = [r[1], 'r', None]
125             else:
126                 raise InternalShellError(j,"Unsupported redirect: %r" % (r,))
127
128         # Map from the final redirections to something subprocess can handle.
129         final_redirects = []
130         for index,r in enumerate(redirects):
131             if r == (0,):
132                 result = input
133             elif r == (1,):
134                 if index == 0:
135                     raise InternalShellError(j,"Unsupported redirect for stdin")
136                 elif index == 1:
137                     result = subprocess.PIPE
138                 else:
139                     result = subprocess.STDOUT
140             elif r == (2,):
141                 if index != 2:
142                     raise InternalShellError(j,"Unsupported redirect on stdout")
143                 result = subprocess.PIPE
144             else:
145                 if r[2] is None:
146                     if kAvoidDevNull and r[0] == '/dev/null':
147                         r[2] = tempfile.TemporaryFile(mode=r[1])
148                     else:
149                         # Make sure relative paths are relative to the cwd.
150                         redir_filename = os.path.join(cmd_shenv.cwd, r[0])
151                         r[2] = open(redir_filename, r[1])
152                     # Workaround a Win32 and/or subprocess bug when appending.
153                     #
154                     # FIXME: Actually, this is probably an instance of PR6753.
155                     if r[1] == 'a':
156                         r[2].seek(0, 2)
157                     opened_files.append(r[2])
158                 result = r[2]
159             final_redirects.append(result)
160
161         stdin, stdout, stderr = final_redirects
162
163         # If stderr wants to come from stdout, but stdout isn't a pipe, then put
164         # stderr on a pipe and treat it as stdout.
165         if (stderr == subprocess.STDOUT and stdout != subprocess.PIPE):
166             stderr = subprocess.PIPE
167             stderrIsStdout = True
168         else:
169             stderrIsStdout = False
170
171             # Don't allow stderr on a PIPE except for the last
172             # process, this could deadlock.
173             #
174             # FIXME: This is slow, but so is deadlock.
175             if stderr == subprocess.PIPE and j != cmd.commands[-1]:
176                 stderr = tempfile.TemporaryFile(mode='w+b')
177                 stderrTempFiles.append((i, stderr))
178
179         # Resolve the executable path ourselves.
180         args = list(j.args)
181         executable = None
182         # For paths relative to cwd, use the cwd of the shell environment.
183         if args[0].startswith('.'):
184             exe_in_cwd = os.path.join(cmd_shenv.cwd, args[0])
185             if os.path.isfile(exe_in_cwd):
186                 executable = exe_in_cwd
187         if not executable:
188             executable = lit.util.which(args[0], cmd_shenv.env['PATH'])
189         if not executable:
190             raise InternalShellError(j, '%r: command not found' % j.args[0])
191
192         # Replace uses of /dev/null with temporary files.
193         if kAvoidDevNull:
194             for i,arg in enumerate(args):
195                 if arg == "/dev/null":
196                     f = tempfile.NamedTemporaryFile(delete=False)
197                     f.close()
198                     named_temp_files.append(f.name)
199                     args[i] = f.name
200
201         try:
202             procs.append(subprocess.Popen(args, cwd=cmd_shenv.cwd,
203                                           executable = executable,
204                                           stdin = stdin,
205                                           stdout = stdout,
206                                           stderr = stderr,
207                                           env = cmd_shenv.env,
208                                           close_fds = kUseCloseFDs))
209         except OSError as e:
210             raise InternalShellError(j, 'Could not create process due to {}'.format(e))
211
212         # Immediately close stdin for any process taking stdin from us.
213         if stdin == subprocess.PIPE:
214             procs[-1].stdin.close()
215             procs[-1].stdin = None
216
217         # Update the current stdin source.
218         if stdout == subprocess.PIPE:
219             input = procs[-1].stdout
220         elif stderrIsStdout:
221             input = procs[-1].stderr
222         else:
223             input = subprocess.PIPE
224
225     # Explicitly close any redirected files. We need to do this now because we
226     # need to release any handles we may have on the temporary files (important
227     # on Win32, for example). Since we have already spawned the subprocess, our
228     # handles have already been transferred so we do not need them anymore.
229     for f in opened_files:
230         f.close()
231
232     # FIXME: There is probably still deadlock potential here. Yawn.
233     procData = [None] * len(procs)
234     procData[-1] = procs[-1].communicate()
235
236     for i in range(len(procs) - 1):
237         if procs[i].stdout is not None:
238             out = procs[i].stdout.read()
239         else:
240             out = ''
241         if procs[i].stderr is not None:
242             err = procs[i].stderr.read()
243         else:
244             err = ''
245         procData[i] = (out,err)
246
247     # Read stderr out of the temp files.
248     for i,f in stderrTempFiles:
249         f.seek(0, 0)
250         procData[i] = (procData[i][0], f.read())
251
252     def to_string(bytes):
253         if isinstance(bytes, str):
254             return bytes
255         return bytes.encode('utf-8')
256
257     exitCode = None
258     for i,(out,err) in enumerate(procData):
259         res = procs[i].wait()
260         # On Windows, manually close the process handles.
261         if kIsWindows:
262             procs[i]._handle.Close()
263         # Detect Ctrl-C in subprocess.
264         if res == -signal.SIGINT:
265             raise KeyboardInterrupt
266
267         # Ensure the resulting output is always of string type.
268         try:
269             out = to_string(out.decode('utf-8'))
270         except:
271             out = str(out)
272         try:
273             err = to_string(err.decode('utf-8'))
274         except:
275             err = str(err)
276
277         results.append((cmd.commands[i], out, err, res))
278         if cmd.pipe_err:
279             # Python treats the exit code as a signed char.
280             if exitCode is None:
281                 exitCode = res
282             elif res < 0:
283                 exitCode = min(exitCode, res)
284             else:
285                 exitCode = max(exitCode, res)
286         else:
287             exitCode = res
288
289     # Remove any named temporary files we created.
290     for f in named_temp_files:
291         try:
292             os.remove(f)
293         except OSError:
294             pass
295
296     if cmd.negate:
297         exitCode = not exitCode
298
299     return exitCode
300
301 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
302     cmds = []
303     for ln in commands:
304         try:
305             cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
306                                         test.config.pipefail).parse())
307         except:
308             return lit.Test.Result(Test.FAIL, "shell parser error on: %r" % ln)
309
310     cmd = cmds[0]
311     for c in cmds[1:]:
312         cmd = ShUtil.Seq(cmd, '&&', c)
313
314     results = []
315     try:
316         shenv = ShellEnvironment(cwd, test.config.environment)
317         exitCode = executeShCmd(cmd, shenv, results)
318     except InternalShellError:
319         e = sys.exc_info()[1]
320         exitCode = 127
321         results.append((e.command, '', e.message, exitCode))
322
323     out = err = ''
324     for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
325         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
326         out += 'Command %d Result: %r\n' % (i, res)
327         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
328         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
329
330     return out, err, exitCode
331
332 def executeScript(test, litConfig, tmpBase, commands, cwd):
333     bashPath = litConfig.getBashPath();
334     isWin32CMDEXE = (litConfig.isWindows and not bashPath)
335     script = tmpBase + '.script'
336     if isWin32CMDEXE:
337         script += '.bat'
338
339     # Write script file
340     mode = 'w'
341     if litConfig.isWindows and not isWin32CMDEXE:
342       mode += 'b'  # Avoid CRLFs when writing bash scripts.
343     f = open(script, mode)
344     if isWin32CMDEXE:
345         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
346     else:
347         if test.config.pipefail:
348             f.write('set -o pipefail;')
349         f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
350     f.write('\n')
351     f.close()
352
353     if isWin32CMDEXE:
354         command = ['cmd','/c', script]
355     else:
356         if bashPath:
357             command = [bashPath, script]
358         else:
359             command = ['/bin/sh', script]
360         if litConfig.useValgrind:
361             # FIXME: Running valgrind on sh is overkill. We probably could just
362             # run on clang with no real loss.
363             command = litConfig.valgrindArgs + command
364
365     return lit.util.executeCommand(command, cwd=cwd,
366                                    env=test.config.environment)
367
368 def parseIntegratedTestScriptCommands(source_path):
369     """
370     parseIntegratedTestScriptCommands(source_path) -> commands
371
372     Parse the commands in an integrated test script file into a list of
373     (line_number, command_type, line).
374     """
375
376     # This code is carefully written to be dual compatible with Python 2.5+ and
377     # Python 3 without requiring input files to always have valid codings. The
378     # trick we use is to open the file in binary mode and use the regular
379     # expression library to find the commands, with it scanning strings in
380     # Python2 and bytes in Python3.
381     #
382     # Once we find a match, we do require each script line to be decodable to
383     # UTF-8, so we convert the outputs to UTF-8 before returning. This way the
384     # remaining code can work with "strings" agnostic of the executing Python
385     # version.
386
387     keywords = ['RUN:', 'XFAIL:', 'REQUIRES:', 'UNSUPPORTED:', 'END.']
388     keywords_re = re.compile(
389         to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
390
391     f = open(source_path, 'rb')
392     try:
393         # Read the entire file contents.
394         data = f.read()
395
396         # Ensure the data ends with a newline.
397         if not data.endswith(to_bytes('\n')):
398             data = data + to_bytes('\n')
399
400         # Iterate over the matches.
401         line_number = 1
402         last_match_position = 0
403         for match in keywords_re.finditer(data):
404             # Compute the updated line number by counting the intervening
405             # newlines.
406             match_position = match.start()
407             line_number += data.count(to_bytes('\n'), last_match_position,
408                                       match_position)
409             last_match_position = match_position
410
411             # Convert the keyword and line to UTF-8 strings and yield the
412             # command. Note that we take care to return regular strings in
413             # Python 2, to avoid other code having to differentiate between the
414             # str and unicode types.
415             keyword,ln = match.groups()
416             yield (line_number, to_string(keyword[:-1].decode('utf-8')),
417                    to_string(ln.decode('utf-8')))
418     finally:
419         f.close()
420
421
422 def parseIntegratedTestScript(test, normalize_slashes=False,
423                               extra_substitutions=[], require_script=True):
424     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
425     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
426     and 'UNSUPPORTED' information. The RUN lines also will have variable
427     substitution performed. If 'require_script' is False an empty script may be
428     returned. This can be used for test formats where the actual script is
429     optional or ignored.
430     """
431
432     # Get the temporary location, this is always relative to the test suite
433     # root, not test source root.
434     #
435     # FIXME: This should not be here?
436     sourcepath = test.getSourcePath()
437     sourcedir = os.path.dirname(sourcepath)
438     execpath = test.getExecPath()
439     execdir,execbase = os.path.split(execpath)
440     tmpDir = os.path.join(execdir, 'Output')
441     tmpBase = os.path.join(tmpDir, execbase)
442
443     # Normalize slashes, if requested.
444     if normalize_slashes:
445         sourcepath = sourcepath.replace('\\', '/')
446         sourcedir = sourcedir.replace('\\', '/')
447         tmpDir = tmpDir.replace('\\', '/')
448         tmpBase = tmpBase.replace('\\', '/')
449
450     # We use #_MARKER_# to hide %% while we do the other substitutions.
451     substitutions = list(extra_substitutions)
452     substitutions.extend([('%%', '#_MARKER_#')])
453     substitutions.extend(test.config.substitutions)
454     substitutions.extend([('%s', sourcepath),
455                           ('%S', sourcedir),
456                           ('%p', sourcedir),
457                           ('%{pathsep}', os.pathsep),
458                           ('%t', tmpBase + '.tmp'),
459                           ('%T', tmpDir),
460                           ('#_MARKER_#', '%')])
461
462     # "%/[STpst]" should be normalized.
463     substitutions.extend([
464             ('%/s', sourcepath.replace('\\', '/')),
465             ('%/S', sourcedir.replace('\\', '/')),
466             ('%/p', sourcedir.replace('\\', '/')),
467             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
468             ('%/T', tmpDir.replace('\\', '/')),
469             ])
470
471     # Collect the test lines from the script.
472     script = []
473     requires = []
474     unsupported = []
475     for line_number, command_type, ln in \
476             parseIntegratedTestScriptCommands(sourcepath):
477         if command_type == 'RUN':
478             # Trim trailing whitespace.
479             ln = ln.rstrip()
480
481             # Substitute line number expressions
482             ln = re.sub('%\(line\)', str(line_number), ln)
483             def replace_line_number(match):
484                 if match.group(1) == '+':
485                     return str(line_number + int(match.group(2)))
486                 if match.group(1) == '-':
487                     return str(line_number - int(match.group(2)))
488             ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
489
490             # Collapse lines with trailing '\\'.
491             if script and script[-1][-1] == '\\':
492                 script[-1] = script[-1][:-1] + ln
493             else:
494                 script.append(ln)
495         elif command_type == 'XFAIL':
496             test.xfails.extend([s.strip() for s in ln.split(',')])
497         elif command_type == 'REQUIRES':
498             requires.extend([s.strip() for s in ln.split(',')])
499         elif command_type == 'UNSUPPORTED':
500             unsupported.extend([s.strip() for s in ln.split(',')])
501         elif command_type == 'END':
502             # END commands are only honored if the rest of the line is empty.
503             if not ln.strip():
504                 break
505         else:
506             raise ValueError("unknown script command type: %r" % (
507                     command_type,))
508
509     # Apply substitutions to the script.  Allow full regular
510     # expression syntax.  Replace each matching occurrence of regular
511     # expression pattern a with substitution b in line ln.
512     def processLine(ln):
513         # Apply substitutions
514         for a,b in substitutions:
515             if kIsWindows:
516                 b = b.replace("\\","\\\\")
517             ln = re.sub(a, b, ln)
518
519         # Strip the trailing newline and any extra whitespace.
520         return ln.strip()
521     script = [processLine(ln)
522               for ln in script]
523
524     # Verify the script contains a run line.
525     if require_script and not script:
526         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
527
528     # Check for unterminated run lines.
529     if script and script[-1][-1] == '\\':
530         return lit.Test.Result(Test.UNRESOLVED,
531                                "Test has unterminated run lines (with '\\')")
532
533     # Check that we have the required features:
534     missing_required_features = [f for f in requires
535                                  if f not in test.config.available_features]
536     if missing_required_features:
537         msg = ', '.join(missing_required_features)
538         return lit.Test.Result(Test.UNSUPPORTED,
539                                "Test requires the following features: %s" % msg)
540     unsupported_features = [f for f in unsupported
541                             if f in test.config.available_features]
542     if unsupported_features:
543         msg = ', '.join(unsupported_features)
544         return lit.Test.Result(Test.UNSUPPORTED,
545                     "Test is unsupported with the following features: %s" % msg)
546
547     if test.config.limit_to_features:
548         # Check that we have one of the limit_to_features features in requires.
549         limit_to_features_tests = [f for f in test.config.limit_to_features
550                                    if f in requires]
551         if not limit_to_features_tests:
552             msg = ', '.join(test.config.limit_to_features)
553             return lit.Test.Result(Test.UNSUPPORTED,
554                  "Test requires one of the limit_to_features features %s" % msg)
555
556     return script,tmpBase,execdir
557
558 def _runShTest(test, litConfig, useExternalSh,
559                    script, tmpBase, execdir):
560     # Create the output directory if it does not already exist.
561     lit.util.mkdir_p(os.path.dirname(tmpBase))
562
563     if useExternalSh:
564         res = executeScript(test, litConfig, tmpBase, script, execdir)
565     else:
566         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
567     if isinstance(res, lit.Test.Result):
568         return res
569
570     out,err,exitCode = res
571     if exitCode == 0:
572         status = Test.PASS
573     else:
574         status = Test.FAIL
575
576     # Form the output log.
577     output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
578         '\n'.join(script), exitCode)
579
580     # Append the outputs, if present.
581     if out:
582         output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
583     if err:
584         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
585
586     return lit.Test.Result(status, output)
587
588
589 def executeShTest(test, litConfig, useExternalSh,
590                   extra_substitutions=[]):
591     if test.config.unsupported:
592         return (Test.UNSUPPORTED, 'Test is unsupported')
593
594     res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
595     if isinstance(res, lit.Test.Result):
596         return res
597     if litConfig.noExecute:
598         return lit.Test.Result(Test.PASS)
599
600     script, tmpBase, execdir = res
601     return _runShTest(test, litConfig, useExternalSh, script, tmpBase, execdir)
602