blob: ba43330d9a688c1e5e4a5946c09b715a96a97099 [file] [log] [blame]
Eli Friedman77a1fe92009-07-10 20:15:12 +00001#!/usr/bin/env python
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +00002#
3# TestRunner.py - This script is used to run arbitrary unit tests. Unit
4# tests must contain the command used to run them in the input file, starting
5# immediately after a "RUN:" string.
6#
7# This runner recognizes and replaces the following strings in the command:
8#
9# %s - Replaced with the input name of the program, or the program to
10# execute, as appropriate.
Daniel Dunbar4d8076a2009-04-10 19:49:21 +000011# %S - Replaced with the directory where the input resides.
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000012# %llvmgcc - llvm-gcc command
13# %llvmgxx - llvm-g++ command
14# %prcontext - prcontext.tcl script
15# %t - temporary file name (derived from testcase name)
16#
17
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000018import errno
Daniel Dunbar69e07a72009-06-17 21:33:37 +000019import os
Daniel Dunbar8fe83f12009-07-25 09:42:24 +000020import platform
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000021import re
Daniel Dunbar69e07a72009-06-17 21:33:37 +000022import signal
23import subprocess
24import sys
25
Daniel Dunbar78a88e72009-07-25 10:14:19 +000026# Increase determinism by explicitly choosing the environment.
27kChildEnv = { 'PATH' : os.environ.get('PATH','') }
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000028
Daniel Dunbar8fe83f12009-07-25 09:42:24 +000029kSystemName = platform.system()
30
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000031class TestStatus:
32 Pass = 0
33 XFail = 1
34 Fail = 2
35 XPass = 3
Daniel Dunbar8bf0ccd2009-07-25 12:47:38 +000036 Invalid = 4
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000037
Daniel Dunbar8bf0ccd2009-07-25 12:47:38 +000038 kNames = ['Pass','XFail','Fail','XPass','Invalid']
Daniel Dunbarfbbb1e72009-07-02 23:58:07 +000039 @staticmethod
Daniel Dunbar8afede22009-07-02 23:56:37 +000040 def getName(code):
41 return TestStatus.kNames[code]
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000042
43def mkdir_p(path):
44 if not path:
45 pass
46 elif os.path.exists(path):
47 pass
48 else:
49 parent = os.path.dirname(path)
50 if parent != path:
51 mkdir_p(parent)
52 try:
53 os.mkdir(path)
54 except OSError,e:
55 if e.errno != errno.EEXIST:
56 raise
57
58def remove(path):
59 try:
60 os.remove(path)
61 except OSError:
62 pass
63
64def cat(path, output):
65 f = open(path)
66 output.writelines(f)
67 f.close()
68
Daniel Dunbar69e07a72009-06-17 21:33:37 +000069def runOneTest(FILENAME, SUBST, OUTPUT, TESTNAME, CLANG, CLANGCC,
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000070 useValgrind=False,
71 useDGCompat=False,
72 useScript=None,
73 output=sys.stdout):
Nuno Lopesa7afc452009-07-11 18:34:43 +000074 OUTPUT = os.path.abspath(OUTPUT)
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000075 if useValgrind:
76 VG_OUTPUT = '%s.vg'%(OUTPUT,)
Nuno Lopesa7afc452009-07-11 18:34:43 +000077 os.system('rm -f %s.*'%(VG_OUTPUT))
78 VALGRIND = 'valgrind -q --tool=memcheck --leak-check=full --trace-children=yes --log-file=%s.%%p'%(VG_OUTPUT)
79 CLANG = '%s %s'%(VALGRIND, CLANG)
80 CLANGCC = '%s %s'%(VALGRIND, CLANGCC)
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +000081
82 # Create the output directory if it does not already exist.
83 mkdir_p(os.path.dirname(OUTPUT))
84
85 # FIXME
86 #ulimit -t 40
87
88 # FIXME: Load script once
89 # FIXME: Support "short" script syntax
90
91 if useScript:
92 scriptFile = useScript
93 else:
94 # See if we have a per-dir test script.
95 dirScriptFile = os.path.join(os.path.dirname(FILENAME), 'test.script')
96 if os.path.exists(dirScriptFile):
97 scriptFile = dirScriptFile
98 else:
99 scriptFile = FILENAME
100
101 # Verify the script contains a run line.
102 for ln in open(scriptFile):
103 if 'RUN:' in ln:
104 break
105 else:
106 print >>output, "******************** TEST '%s' HAS NO RUN LINE! ********************"%(TESTNAME,)
107 output.flush()
Daniel Dunbar8bf0ccd2009-07-25 12:47:38 +0000108 return TestStatus.Fail
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000109
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000110 FILENAME = os.path.abspath(FILENAME)
111 SCRIPT = OUTPUT + '.script'
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000112 if kSystemName == 'Windows':
113 SCRIPT += '.bat'
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000114 TEMPOUTPUT = OUTPUT + '.tmp'
115
116 substitutions = [('%s',SUBST),
Daniel Dunbar4d8076a2009-04-10 19:49:21 +0000117 ('%S',os.path.dirname(SUBST)),
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000118 ('%llvmgcc','llvm-gcc -emit-llvm -w'),
119 ('%llvmgxx','llvm-g++ -emit-llvm -w'),
120 ('%prcontext','prcontext.tcl'),
121 ('%t',TEMPOUTPUT),
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000122 (' clang ', ' ' + CLANG + ' '),
123 (' clang-cc ', ' ' + CLANGCC + ' ')]
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000124
125 # Collect the test lines from the script.
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000126 scriptLines = []
127 xfailLines = []
128 for ln in open(scriptFile):
129 if 'RUN:' in ln:
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000130 # Isolate the command to run.
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000131 index = ln.index('RUN:')
132 ln = ln[index+4:]
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000133
Daniel Dunbar322f7892009-07-25 12:23:35 +0000134 # Strip trailing newline.
135 scriptLines.append(ln)
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000136 elif 'XFAIL' in ln:
137 xfailLines.append(ln)
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000138
139 # FIXME: Support something like END, in case we need to process large
140 # files.
Daniel Dunbar322f7892009-07-25 12:23:35 +0000141
142 # Apply substitutions to the script.
143 def processLine(ln):
144 # Apply substitutions
145 for a,b in substitutions:
146 ln = ln.replace(a,b)
147
148 if useDGCompat:
149 ln = re.sub(r'\{(.*)\}', r'"\1"', ln)
150
151 # Strip the trailing newline and any extra whitespace.
152 return ln.strip()
153 scriptLines = map(processLine, scriptLines)
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000154
155 # Validate interior lines for '&&', a lovely historical artifact.
156 for i in range(len(scriptLines) - 1):
157 ln = scriptLines[i]
158
159 if not ln.endswith('&&'):
160 print >>output, "MISSING \'&&\': %s" % ln
161 print >>output, "FOLLOWED BY : %s" % scriptLines[i + 1]
162 return TestStatus.Fail
163
164 # Strip off '&&'
165 scriptLines[i] = ln[:-2]
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000166
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000167 if xfailLines:
168 print >>output, "XFAILED '%s':"%(TESTNAME,)
169 output.writelines(xfailLines)
170
171 # Write script file
172 f = open(SCRIPT,'w')
Daniel Dunbar025f80d2009-07-25 11:27:37 +0000173 f.write(' &&\n'.join(scriptLines))
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000174 f.close()
175
176 outputFile = open(OUTPUT,'w')
177 p = None
178 try:
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000179 if kSystemName == 'Windows':
180 command = ['cmd','/c', SCRIPT]
181 else:
182 command = ['/bin/sh', SCRIPT]
183
184 p = subprocess.Popen(command,
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000185 cwd=os.path.dirname(FILENAME),
Daniel Dunbar02cee152009-07-13 21:24:28 +0000186 stdin=subprocess.PIPE,
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000187 stdout=subprocess.PIPE,
Daniel Dunbar78a88e72009-07-25 10:14:19 +0000188 stderr=subprocess.PIPE,
189 env=kChildEnv)
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000190 out,err = p.communicate()
191 outputFile.write(out)
192 outputFile.write(err)
193 SCRIPT_STATUS = p.wait()
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000194
195 # Detect Ctrl-C in subprocess.
196 if SCRIPT_STATUS == -signal.SIGINT:
197 raise KeyboardInterrupt
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000198 except KeyboardInterrupt:
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000199 raise
200 outputFile.close()
201
202 if xfailLines:
203 SCRIPT_STATUS = not SCRIPT_STATUS
204
205 if useValgrind:
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000206 if kSystemName == 'Windows':
207 raise NotImplementedError,'Cannot run valgrind on windows'
208 else:
209 VG_OUTPUT = capture(['/bin/sh','-c','cat %s.*'%(VG_OUTPUT)])
Nuno Lopesa7afc452009-07-11 18:34:43 +0000210 VG_STATUS = len(VG_OUTPUT)
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000211 else:
212 VG_STATUS = 0
213
214 if SCRIPT_STATUS or VG_STATUS:
215 print >>output, "******************** TEST '%s' FAILED! ********************"%(TESTNAME,)
216 print >>output, "Command: "
217 output.writelines(scriptLines)
218 if not SCRIPT_STATUS:
219 print >>output, "Output:"
220 else:
221 print >>output, "Incorrect Output:"
222 cat(OUTPUT, output)
223 if VG_STATUS:
224 print >>output, "Valgrind Output:"
Nuno Lopesa7afc452009-07-11 18:34:43 +0000225 print >>output, VG_OUTPUT
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000226 print >>output, "******************** TEST '%s' FAILED! ********************"%(TESTNAME,)
227 output.flush()
228 if xfailLines:
229 return TestStatus.XPass
230 else:
231 return TestStatus.Fail
232
233 if xfailLines:
234 return TestStatus.XFail
235 else:
236 return TestStatus.Pass
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000237
238def capture(args):
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000239 p = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000240 out,_ = p.communicate()
241 return out
242
243def which(command):
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000244 # Check for absolute match first.
245 if os.path.exists(command):
246 return command
247
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000248 # Would be nice if Python had a lib function for this.
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000249 paths = os.environ.get('PATH')
250 if not paths:
251 paths = os.defpath
252
253 # Get suffixes to search.
254 pathext = os.environ.get('PATHEXT', '').split(os.pathsep)
255
256 # Search the paths...
257 for path in paths.split(os.pathsep):
258 for ext in pathext:
259 p = os.path.join(path, command + ext)
260 if os.path.exists(p):
261 return p
262
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000263 return None
264
265def inferClang():
266 # Determine which clang to use.
267 clang = os.getenv('CLANG')
268
269 # If the user set clang in the environment, definitely use that and don't
270 # try to validate.
271 if clang:
272 return clang
273
274 # Otherwise look in the path.
275 clang = which('clang')
276
277 if not clang:
278 print >>sys.stderr, "error: couldn't find 'clang' program, try setting CLANG in your environment"
279 sys.exit(1)
280
281 return clang
282
283def inferClangCC(clang):
284 clangcc = os.getenv('CLANGCC')
285
286 # If the user set clang in the environment, definitely use that and don't
287 # try to validate.
288 if clangcc:
289 return clangcc
290
291 # Otherwise try adding -cc since we expect to be looking in a build
292 # directory.
Daniel Dunbar8fe83f12009-07-25 09:42:24 +0000293 if clang.endswith('.exe'):
294 clangccName = clang[:-4] + '-cc.exe'
295 else:
296 clangccName = clang + '-cc'
297 clangcc = which(clangccName)
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000298 if not clangcc:
299 # Otherwise ask clang.
300 res = capture([clang, '-print-prog-name=clang-cc'])
301 res = res.strip()
302 if res and os.path.exists(res):
303 clangcc = res
304
305 if not clangcc:
306 print >>sys.stderr, "error: couldn't find 'clang-cc' program, try setting CLANGCC in your environment"
307 sys.exit(1)
308
309 return clangcc
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000310
Daniel Dunbardf084892009-07-25 09:53:43 +0000311def getTestOutputBase(dir, testpath):
Daniel Dunbarfecdd002009-07-25 12:05:55 +0000312 """getTestOutputBase(dir, testpath) - Get the full path for temporary files
Daniel Dunbardf084892009-07-25 09:53:43 +0000313 corresponding to the given test path."""
314
315 # Form the output base out of the test parent directory name and the test
316 # name. FIXME: Find a better way to organize test results.
317 return os.path.join(dir,
318 os.path.basename(os.path.dirname(testpath)),
319 os.path.basename(testpath))
320
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000321def main():
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000322 global options
323 from optparse import OptionParser
324 parser = OptionParser("usage: %prog [options] {tests}")
325 parser.add_option("", "--clang", dest="clang",
326 help="Program to use as \"clang\"",
327 action="store", default=None)
328 parser.add_option("", "--clang-cc", dest="clangcc",
329 help="Program to use as \"clang-cc\"",
330 action="store", default=None)
331 parser.add_option("", "--vg", dest="useValgrind",
332 help="Run tests under valgrind",
333 action="store_true", default=False)
334 parser.add_option("", "--dg", dest="useDGCompat",
335 help="Use llvm dejagnu compatibility mode",
336 action="store_true", default=False)
337 (opts, args) = parser.parse_args()
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000338
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000339 if not args:
340 parser.error('No tests specified')
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000341
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000342 if opts.clang is None:
343 opts.clang = inferClang()
344 if opts.clangcc is None:
345 opts.clangcc = inferClangCC(opts.clang)
346
347 for path in args:
348 command = path
Daniel Dunbarfecdd002009-07-25 12:05:55 +0000349 output = getTestOutputBase('Output', path) + '.out'
Daniel Dunbar69e07a72009-06-17 21:33:37 +0000350 testname = path
351
352 res = runOneTest(path, command, output, testname,
353 opts.clang, opts.clangcc,
354 useValgrind=opts.useValgrind,
355 useDGCompat=opts.useDGCompat,
356 useScript=os.getenv("TEST_SCRIPT"))
Daniel Dunbar6fc0bdf2009-03-06 22:20:40 +0000357
358 sys.exit(res == TestStatus.Fail or res == TestStatus.XPass)
359
360if __name__=='__main__':
361 main()