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