Fix ShTest parsing error when a keyword line doesn't end with a newline.
[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         # Ensure the data ends with a newline.
347         if not data.endswith('\n'):
348             data = data + '\n'
349
350         # Iterate over the matches.
351         line_number = 1
352         last_match_position = 0
353         for match in keywords_re.finditer(data):
354             # Compute the updated line number by counting the intervening
355             # newlines.
356             match_position = match.start()
357             line_number += data.count(to_bytes('\n'), last_match_position,
358                                       match_position)
359             last_match_position = match_position
360
361             # Convert the keyword and line to UTF-8 strings and yield the
362             # command. Note that we take care to return regular strings in
363             # Python 2, to avoid other code having to differentiate between the
364             # str and unicode types.
365             keyword,ln = match.groups()
366             yield (line_number, to_string(keyword[:-1].decode('utf-8')),
367                    to_string(ln.decode('utf-8')))
368     finally:
369         f.close()
370
371 def parseIntegratedTestScript(test, normalize_slashes=False,
372                               extra_substitutions=[]):
373     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
374     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
375     information. The RUN lines also will have variable substitution performed.
376     """
377
378     # Get the temporary location, this is always relative to the test suite
379     # root, not test source root.
380     #
381     # FIXME: This should not be here?
382     sourcepath = test.getSourcePath()
383     sourcedir = os.path.dirname(sourcepath)
384     execpath = test.getExecPath()
385     execdir,execbase = os.path.split(execpath)
386     tmpDir = os.path.join(execdir, 'Output')
387     tmpBase = os.path.join(tmpDir, execbase)
388
389     # Normalize slashes, if requested.
390     if normalize_slashes:
391         sourcepath = sourcepath.replace('\\', '/')
392         sourcedir = sourcedir.replace('\\', '/')
393         tmpDir = tmpDir.replace('\\', '/')
394         tmpBase = tmpBase.replace('\\', '/')
395
396     # We use #_MARKER_# to hide %% while we do the other substitutions.
397     substitutions = list(extra_substitutions)
398     substitutions.extend([('%%', '#_MARKER_#')])
399     substitutions.extend(test.config.substitutions)
400     substitutions.extend([('%s', sourcepath),
401                           ('%S', sourcedir),
402                           ('%p', sourcedir),
403                           ('%{pathsep}', os.pathsep),
404                           ('%t', tmpBase + '.tmp'),
405                           ('%T', tmpDir),
406                           ('#_MARKER_#', '%')])
407
408     # "%/[STpst]" should be normalized.
409     substitutions.extend([
410             ('%/s', sourcepath.replace('\\', '/')),
411             ('%/S', sourcedir.replace('\\', '/')),
412             ('%/p', sourcedir.replace('\\', '/')),
413             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
414             ('%/T', tmpDir.replace('\\', '/')),
415             ])
416
417     # Collect the test lines from the script.
418     script = []
419     requires = []
420     for line_number, command_type, ln in \
421             parseIntegratedTestScriptCommands(sourcepath):
422         if command_type == 'RUN':
423             # Trim trailing whitespace.
424             ln = ln.rstrip()
425
426             # Substitute line number expressions
427             ln = re.sub('%\(line\)', str(line_number), ln)
428             def replace_line_number(match):
429                 if match.group(1) == '+':
430                     return str(line_number + int(match.group(2)))
431                 if match.group(1) == '-':
432                     return str(line_number - int(match.group(2)))
433             ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
434
435             # Collapse lines with trailing '\\'.
436             if script and script[-1][-1] == '\\':
437                 script[-1] = script[-1][:-1] + ln
438             else:
439                 script.append(ln)
440         elif command_type == 'XFAIL':
441             test.xfails.extend([s.strip() for s in ln.split(',')])
442         elif command_type == 'REQUIRES':
443             requires.extend([s.strip() for s in ln.split(',')])
444         elif command_type == 'END':
445             # END commands are only honored if the rest of the line is empty.
446             if not ln.strip():
447                 break
448         else:
449             raise ValueError("unknown script command type: %r" % (
450                     command_type,))
451
452     # Apply substitutions to the script.  Allow full regular
453     # expression syntax.  Replace each matching occurrence of regular
454     # expression pattern a with substitution b in line ln.
455     def processLine(ln):
456         # Apply substitutions
457         for a,b in substitutions:
458             if kIsWindows:
459                 b = b.replace("\\","\\\\")
460             ln = re.sub(a, b, ln)
461
462         # Strip the trailing newline and any extra whitespace.
463         return ln.strip()
464     script = [processLine(ln)
465               for ln in script]
466
467     # Verify the script contains a run line.
468     if not script:
469         return lit.Test.Result(Test.UNRESOLVED, "Test has no run line!")
470
471     # Check for unterminated run lines.
472     if script[-1][-1] == '\\':
473         return lit.Test.Result(Test.UNRESOLVED,
474                                "Test has unterminated run lines (with '\\')")
475
476     # Check that we have the required features:
477     missing_required_features = [f for f in requires
478                                  if f not in test.config.available_features]
479     if missing_required_features:
480         msg = ', '.join(missing_required_features)
481         return lit.Test.Result(Test.UNSUPPORTED,
482                                "Test requires the following features: %s" % msg)
483
484     return script,tmpBase,execdir
485
486 def executeShTest(test, litConfig, useExternalSh,
487                   extra_substitutions=[]):
488     if test.config.unsupported:
489         return (Test.UNSUPPORTED, 'Test is unsupported')
490
491     res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
492     if isinstance(res, lit.Test.Result):
493         return res
494     if litConfig.noExecute:
495         return lit.Test.Result(Test.PASS)
496
497     script, tmpBase, execdir = res
498
499     # Create the output directory if it does not already exist.
500     lit.util.mkdir_p(os.path.dirname(tmpBase))
501
502     if useExternalSh:
503         res = executeScript(test, litConfig, tmpBase, script, execdir)
504     else:
505         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
506     if isinstance(res, lit.Test.Result):
507         return res
508
509     out,err,exitCode = res
510     if exitCode == 0:
511         status = Test.PASS
512     else:
513         status = Test.FAIL
514
515     # Form the output log.
516     output = """Script:\n--\n%s\n--\nExit Code: %d\n\n""" % (
517         '\n'.join(script), exitCode)
518
519     # Append the outputs, if present.
520     if out:
521         output += """Command Output (stdout):\n--\n%s\n--\n""" % (out,)
522     if err:
523         output += """Command Output (stderr):\n--\n%s\n--\n""" % (err,)
524
525     return lit.Test.Result(status, output)