[lit] Support parsing scripts with inconsistent or invalid encodings.
[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         args[0] = lit.util.which(args[0], cfg.environment['PATH'])
135         if not args[0]:
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         procs.append(subprocess.Popen(args, cwd=cwd,
148                                       stdin = stdin,
149                                       stdout = stdout,
150                                       stderr = stderr,
151                                       env = cfg.environment,
152                                       close_fds = kUseCloseFDs))
153
154         # Immediately close stdin for any process taking stdin from us.
155         if stdin == subprocess.PIPE:
156             procs[-1].stdin.close()
157             procs[-1].stdin = None
158
159         # Update the current stdin source.
160         if stdout == subprocess.PIPE:
161             input = procs[-1].stdout
162         elif stderrIsStdout:
163             input = procs[-1].stderr
164         else:
165             input = subprocess.PIPE
166
167     # Explicitly close any redirected files. We need to do this now because we
168     # need to release any handles we may have on the temporary files (important
169     # on Win32, for example). Since we have already spawned the subprocess, our
170     # handles have already been transferred so we do not need them anymore.
171     for f in opened_files:
172         f.close()
173
174     # FIXME: There is probably still deadlock potential here. Yawn.
175     procData = [None] * len(procs)
176     procData[-1] = procs[-1].communicate()
177
178     for i in range(len(procs) - 1):
179         if procs[i].stdout is not None:
180             out = procs[i].stdout.read()
181         else:
182             out = ''
183         if procs[i].stderr is not None:
184             err = procs[i].stderr.read()
185         else:
186             err = ''
187         procData[i] = (out,err)
188
189     # Read stderr out of the temp files.
190     for i,f in stderrTempFiles:
191         f.seek(0, 0)
192         procData[i] = (procData[i][0], f.read())
193
194     exitCode = None
195     for i,(out,err) in enumerate(procData):
196         res = procs[i].wait()
197         # Detect Ctrl-C in subprocess.
198         if res == -signal.SIGINT:
199             raise KeyboardInterrupt
200
201         results.append((cmd.commands[i], out, err, res))
202         if cmd.pipe_err:
203             # Python treats the exit code as a signed char.
204             if exitCode is None:
205                 exitCode = res
206             elif res < 0:
207                 exitCode = min(exitCode, res)
208             else:
209                 exitCode = max(exitCode, res)
210         else:
211             exitCode = res
212
213     # Remove any named temporary files we created.
214     for f in named_temp_files:
215         try:
216             os.remove(f)
217         except OSError:
218             pass
219
220     if cmd.negate:
221         exitCode = not exitCode
222
223     return exitCode
224
225 def executeScriptInternal(test, litConfig, tmpBase, commands, cwd):
226     cmds = []
227     for ln in commands:
228         try:
229             cmds.append(ShUtil.ShParser(ln, litConfig.isWindows,
230                                         test.config.pipefail).parse())
231         except:
232             return (Test.FAIL, "shell parser error on: %r" % ln)
233
234     cmd = cmds[0]
235     for c in cmds[1:]:
236         cmd = ShUtil.Seq(cmd, '&&', c)
237
238     results = []
239     try:
240         exitCode = executeShCmd(cmd, test.config, cwd, results)
241     except InternalShellError:
242         e = sys.exc_info()[1]
243         exitCode = 127
244         results.append((e.command, '', e.message, exitCode))
245
246     out = err = ''
247     for i,(cmd, cmd_out,cmd_err,res) in enumerate(results):
248         out += 'Command %d: %s\n' % (i, ' '.join('"%s"' % s for s in cmd.args))
249         out += 'Command %d Result: %r\n' % (i, res)
250         out += 'Command %d Output:\n%s\n\n' % (i, cmd_out)
251         out += 'Command %d Stderr:\n%s\n\n' % (i, cmd_err)
252
253     return out, err, exitCode
254
255 def executeScript(test, litConfig, tmpBase, commands, cwd):
256     bashPath = litConfig.getBashPath();
257     isWin32CMDEXE = (litConfig.isWindows and not bashPath)
258     script = tmpBase + '.script'
259     if isWin32CMDEXE:
260         script += '.bat'
261
262     # Write script file
263     mode = 'w'
264     if litConfig.isWindows and not isWin32CMDEXE:
265       mode += 'b'  # Avoid CRLFs when writing bash scripts.
266     f = open(script, mode)
267     if isWin32CMDEXE:
268         f.write('\nif %ERRORLEVEL% NEQ 0 EXIT\n'.join(commands))
269     else:
270         if test.config.pipefail:
271             f.write('set -o pipefail;')
272         f.write('{ ' + '; } &&\n{ '.join(commands) + '; }')
273     f.write('\n')
274     f.close()
275
276     if isWin32CMDEXE:
277         command = ['cmd','/c', script]
278     else:
279         if bashPath:
280             command = [bashPath, script]
281         else:
282             command = ['/bin/sh', script]
283         if litConfig.useValgrind:
284             # FIXME: Running valgrind on sh is overkill. We probably could just
285             # run on clang with no real loss.
286             command = litConfig.valgrindArgs + command
287
288     return lit.util.executeCommand(command, cwd=cwd,
289                                    env=test.config.environment)
290
291 def isExpectedFail(test, xfails):
292     # Check if any of the xfails match an available feature or the target.
293     for item in xfails:
294         # If this is the wildcard, it always fails.
295         if item == '*':
296             return True
297
298         # If this is an exact match for one of the features, it fails.
299         if item in test.config.available_features:
300             return True
301
302         # If this is a part of the target triple, it fails.
303         if item in test.suite.config.target_triple:
304             return True
305
306     return False
307
308 def parseIntegratedTestScriptCommands(source_path):
309     """
310     parseIntegratedTestScriptCommands(source_path) -> commands
311
312     Parse the commands in an integrated test script file into a list of
313     (line_number, command_type, line).
314     """
315
316     # This code is carefully written to be dual compatible with Python 2.5+ and
317     # Python 3 without requiring input files to always have valid codings. The
318     # trick we use is to open the file in binary mode and use the regular
319     # expression library to find the commands, with it scanning strings in
320     # Python2 and bytes in Python3.
321     #
322     # Once we find a match, we do require each script line to be decodable to
323     # ascii, so we convert the outputs to ascii before returning. This way the
324     # remaining code can work with "strings" agnostic of the executing Python
325     # version.
326     
327     def to_bytes(str):
328         # Encode to Latin1 to get binary data.
329         return str.encode('ISO-8859-1')
330     keywords = ('RUN:', 'XFAIL:', 'REQUIRES:', 'END.')
331     keywords_re = re.compile(
332         to_bytes("(%s)(.*)\n" % ("|".join(k for k in keywords),)))
333
334     f = open(source_path, 'rb')
335     try:
336         # Read the entire file contents.
337         data = f.read()
338
339         # Iterate over the matches.
340         line_number = 1
341         last_match_position = 0
342         for match in keywords_re.finditer(data):
343             # Compute the updated line number by counting the intervening
344             # newlines.
345             match_position = match.start()
346             line_number += data.count(to_bytes('\n'), last_match_position,
347                                       match_position)
348             last_match_position = match_position
349
350             # Convert the keyword and line to ascii and yield the command.
351             keyword,ln = match.groups()
352             yield (line_number, keyword[:-1].decode('ascii'),
353                    ln.decode('ascii'))
354     finally:
355         f.close()
356
357 def parseIntegratedTestScript(test, normalize_slashes=False,
358                               extra_substitutions=[]):
359     """parseIntegratedTestScript - Scan an LLVM/Clang style integrated test
360     script and extract the lines to 'RUN' as well as 'XFAIL' and 'REQUIRES'
361     information. The RUN lines also will have variable substitution performed.
362     """
363
364     # Get the temporary location, this is always relative to the test suite
365     # root, not test source root.
366     #
367     # FIXME: This should not be here?
368     sourcepath = test.getSourcePath()
369     sourcedir = os.path.dirname(sourcepath)
370     execpath = test.getExecPath()
371     execdir,execbase = os.path.split(execpath)
372     tmpDir = os.path.join(execdir, 'Output')
373     tmpBase = os.path.join(tmpDir, execbase)
374
375     # Normalize slashes, if requested.
376     if normalize_slashes:
377         sourcepath = sourcepath.replace('\\', '/')
378         sourcedir = sourcedir.replace('\\', '/')
379         tmpDir = tmpDir.replace('\\', '/')
380         tmpBase = tmpBase.replace('\\', '/')
381
382     # We use #_MARKER_# to hide %% while we do the other substitutions.
383     substitutions = list(extra_substitutions)
384     substitutions.extend([('%%', '#_MARKER_#')])
385     substitutions.extend(test.config.substitutions)
386     substitutions.extend([('%s', sourcepath),
387                           ('%S', sourcedir),
388                           ('%p', sourcedir),
389                           ('%{pathsep}', os.pathsep),
390                           ('%t', tmpBase + '.tmp'),
391                           ('%T', tmpDir),
392                           ('#_MARKER_#', '%')])
393
394     # "%/[STpst]" should be normalized.
395     substitutions.extend([
396             ('%/s', sourcepath.replace('\\', '/')),
397             ('%/S', sourcedir.replace('\\', '/')),
398             ('%/p', sourcedir.replace('\\', '/')),
399             ('%/t', tmpBase.replace('\\', '/') + '.tmp'),
400             ('%/T', tmpDir.replace('\\', '/')),
401             ])
402
403     # Collect the test lines from the script.
404     script = []
405     xfails = []
406     requires = []
407     for line_number, command_type, ln in \
408             parseIntegratedTestScriptCommands(sourcepath):
409         if command_type == 'RUN':
410             # Trim trailing whitespace.
411             ln = ln.rstrip()
412
413             # Substitute line number expressions
414             ln = re.sub('%\(line\)', str(line_number), ln)
415             def replace_line_number(match):
416                 if match.group(1) == '+':
417                     return str(line_number + int(match.group(2)))
418                 if match.group(1) == '-':
419                     return str(line_number - int(match.group(2)))
420             ln = re.sub('%\(line *([\+-]) *(\d+)\)', replace_line_number, ln)
421
422             # Collapse lines with trailing '\\'.
423             if script and script[-1][-1] == '\\':
424                 script[-1] = script[-1][:-1] + ln
425             else:
426                 script.append(ln)
427         elif command_type == 'XFAIL':
428             xfails.extend([s.strip() for s in ln.split(',')])
429         elif command_type == 'REQUIRES':
430             requires.extend([s.strip() for s in ln.split(',')])
431         elif command_type == 'END':
432             # END commands are only honored if the rest of the line is empty.
433             if not ln.strip():
434                 break
435         else:
436             raise ValueError("unknown script command type: %r" % (
437                     command_type,))
438
439     # Apply substitutions to the script.  Allow full regular
440     # expression syntax.  Replace each matching occurrence of regular
441     # expression pattern a with substitution b in line ln.
442     def processLine(ln):
443         # Apply substitutions
444         for a,b in substitutions:
445             if kIsWindows:
446                 b = b.replace("\\","\\\\")
447             ln = re.sub(a, b, ln)
448
449         # Strip the trailing newline and any extra whitespace.
450         return ln.strip()
451     script = [processLine(ln)
452               for ln in script]
453
454     # Verify the script contains a run line.
455     if not script:
456         return (Test.UNRESOLVED, "Test has no run line!")
457
458     # Check for unterminated run lines.
459     if script[-1][-1] == '\\':
460         return (Test.UNRESOLVED, "Test has unterminated run lines (with '\\')")
461
462     # Check that we have the required features:
463     missing_required_features = [f for f in requires
464                                  if f not in test.config.available_features]
465     if missing_required_features:
466         msg = ', '.join(missing_required_features)
467         return (Test.UNSUPPORTED,
468                 "Test requires the following features: %s" % msg)
469
470     isXFail = isExpectedFail(test, xfails)
471     return script,isXFail,tmpBase,execdir
472
473 def formatTestOutput(status, out, err, exitCode, script):
474     output = """\
475 Script:
476 --
477 %s
478 --
479 Exit Code: %d
480
481 """ % ('\n'.join(script), exitCode)
482
483     # Append the stdout, if present.
484     if out:
485         output += """\
486 Command Output (stdout):
487 --
488 %s
489 --
490 """ % (out,)
491
492     # Append the stderr, if present.
493     if err:
494         output += """\
495 Command Output (stderr):
496 --
497 %s
498 --
499 """ % (err,)
500     return (status, output)
501
502 def executeShTest(test, litConfig, useExternalSh,
503                   extra_substitutions=[]):
504     if test.config.unsupported:
505         return (Test.UNSUPPORTED, 'Test is unsupported')
506
507     res = parseIntegratedTestScript(test, useExternalSh, extra_substitutions)
508     if len(res) == 2:
509         return res
510
511     script, isXFail, tmpBase, execdir = res
512
513     if litConfig.noExecute:
514         return (Test.PASS, '')
515
516     # Create the output directory if it does not already exist.
517     lit.util.mkdir_p(os.path.dirname(tmpBase))
518
519     if useExternalSh:
520         res = executeScript(test, litConfig, tmpBase, script, execdir)
521     else:
522         res = executeScriptInternal(test, litConfig, tmpBase, script, execdir)
523     if len(res) == 2:
524         return res
525
526     out,err,exitCode = res
527     if isXFail:
528         ok = exitCode != 0
529         if ok:
530             status = Test.XFAIL
531         else:
532             status = Test.XPASS
533     else:
534         ok = exitCode == 0
535         if ok:
536             status = Test.PASS
537         else:
538             status = Test.FAIL
539
540     if ok:
541         return (status,'')
542
543     return formatTestOutput(status, out, err, exitCode, script)