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