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