##// END OF EJS Templates
tests: add -R switch...
Matt Mackall -
r3625:cc0cd594 default
parent child Browse files
Show More
@@ -1,393 +1,410 b''
1 1 #!/usr/bin/env python
2 2 #
3 3 # run-tests.py - Run a set of tests on Mercurial
4 4 #
5 5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms
8 8 # of the GNU General Public License, incorporated herein by reference.
9 9
10 10 import difflib
11 11 import errno
12 12 import optparse
13 13 import os
14 14 import popen2
15 15 import re
16 16 import shutil
17 17 import signal
18 18 import sys
19 19 import tempfile
20 20 import time
21 21
22 22 required_tools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed", "merge"]
23 23
24 24 parser = optparse.OptionParser("%prog [options] [tests]")
25 25 parser.add_option("-v", "--verbose", action="store_true",
26 26 help="output verbose messages")
27 27 parser.add_option("-t", "--timeout", type="int",
28 28 help="kill errant tests after TIMEOUT seconds")
29 29 parser.add_option("-c", "--cover", action="store_true",
30 30 help="print a test coverage report")
31 31 parser.add_option("-s", "--cover_stdlib", action="store_true",
32 32 help="print a test coverage report inc. standard libraries")
33 33 parser.add_option("-C", "--annotate", action="store_true",
34 34 help="output files annotated with coverage")
35 35 parser.add_option("-r", "--retest", action="store_true",
36 36 help="retest failed tests")
37 37 parser.add_option("-f", "--first", action="store_true",
38 38 help="exit on the first test failure")
39 parser.add_option("-R", "--restart", action="store_true",
40 help="restart at last error")
39 41
40 42 parser.set_defaults(timeout=180)
41 43 (options, args) = parser.parse_args()
42 44 verbose = options.verbose
43 45 coverage = options.cover or options.cover_stdlib or options.annotate
44 46
45 47 def vlog(*msg):
46 48 if verbose:
47 49 for m in msg:
48 50 print m,
49 51 print
50 52
51 53 def splitnewlines(text):
52 54 '''like str.splitlines, but only split on newlines.
53 55 keep line endings.'''
54 56 i = 0
55 57 lines = []
56 58 while True:
57 59 n = text.find('\n', i)
58 60 if n == -1:
59 61 last = text[i:]
60 62 if last:
61 63 lines.append(last)
62 64 return lines
63 65 lines.append(text[i:n+1])
64 66 i = n + 1
65 67
66 68 def show_diff(expected, output):
67 69 for line in difflib.unified_diff(expected, output,
68 70 "Expected output", "Test output"):
69 71 sys.stdout.write(line)
70 72
71 73 def find_program(program):
72 74 """Search PATH for a executable program"""
73 75 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
74 76 name = os.path.join(p, program)
75 77 if os.access(name, os.X_OK):
76 78 return name
77 79 return None
78 80
79 81 def check_required_tools():
80 82 # Before we go any further, check for pre-requisite tools
81 83 # stuff from coreutils (cat, rm, etc) are not tested
82 84 for p in required_tools:
83 85 if os.name == 'nt':
84 86 p += '.exe'
85 87 found = find_program(p)
86 88 if found:
87 89 vlog("# Found prerequisite", p, "at", found)
88 90 else:
89 91 print "WARNING: Did not find prerequisite tool: "+p
90 92
91 93 def cleanup_exit():
92 94 if verbose:
93 95 print "# Cleaning up HGTMP", HGTMP
94 96 shutil.rmtree(HGTMP, True)
95 97
96 98 def use_correct_python():
97 99 # some tests run python interpreter. they must use same
98 100 # interpreter we use or bad things will happen.
99 101 exedir, exename = os.path.split(sys.executable)
100 102 if exename == 'python':
101 103 path = find_program('python')
102 104 if os.path.dirname(path) == exedir:
103 105 return
104 106 vlog('# Making python executable in test path use correct Python')
105 107 my_python = os.path.join(BINDIR, 'python')
106 108 try:
107 109 os.symlink(sys.executable, my_python)
108 110 except AttributeError:
109 111 # windows fallback
110 112 shutil.copyfile(sys.executable, my_python)
111 113 shutil.copymode(sys.executable, my_python)
112 114
113 115 def install_hg():
114 116 vlog("# Performing temporary installation of HG")
115 117 installerrs = os.path.join("tests", "install.err")
116 118
117 119 os.chdir("..") # Get back to hg root
118 120 cmd = ('%s setup.py clean --all'
119 121 ' install --force --home="%s" --install-lib="%s" >%s 2>&1'
120 122 % (sys.executable, INST, PYTHONDIR, installerrs))
121 123 vlog("# Running", cmd)
122 124 if os.system(cmd) == 0:
123 125 if not verbose:
124 126 os.remove(installerrs)
125 127 else:
126 128 f = open(installerrs)
127 129 for line in f:
128 130 print line,
129 131 f.close()
130 132 sys.exit(1)
131 133 os.chdir(TESTDIR)
132 134
133 135 os.environ["PATH"] = "%s%s%s" % (BINDIR, os.pathsep, os.environ["PATH"])
134 136 os.environ["PYTHONPATH"] = PYTHONDIR
135 137
136 138 use_correct_python()
137 139
138 140 if coverage:
139 141 vlog("# Installing coverage wrapper")
140 142 os.environ['COVERAGE_FILE'] = COVERAGE_FILE
141 143 if os.path.exists(COVERAGE_FILE):
142 144 os.unlink(COVERAGE_FILE)
143 145 # Create a wrapper script to invoke hg via coverage.py
144 146 os.rename(os.path.join(BINDIR, "hg"), os.path.join(BINDIR, "_hg.py"))
145 147 f = open(os.path.join(BINDIR, 'hg'), 'w')
146 148 f.write('#!' + sys.executable + '\n')
147 149 f.write('import sys, os; os.execv(sys.executable, [sys.executable, '+ \
148 150 '"%s", "-x", "%s"] + sys.argv[1:])\n' % (
149 151 os.path.join(TESTDIR, 'coverage.py'),
150 152 os.path.join(BINDIR, '_hg.py')))
151 153 f.close()
152 154 os.chmod(os.path.join(BINDIR, 'hg'), 0700)
153 155
154 156 def output_coverage():
155 157 vlog("# Producing coverage report")
156 158 omit = [BINDIR, TESTDIR, PYTHONDIR]
157 159 if not options.cover_stdlib:
158 160 # Exclude as system paths (ignoring empty strings seen on win)
159 161 omit += [x for x in sys.path if x != '']
160 162 omit = ','.join(omit)
161 163 os.chdir(PYTHONDIR)
162 164 cmd = '"%s" "%s" -r "--omit=%s"' % (
163 165 sys.executable, os.path.join(TESTDIR, 'coverage.py'), omit)
164 166 vlog("# Running: "+cmd)
165 167 os.system(cmd)
166 168 if options.annotate:
167 169 adir = os.path.join(TESTDIR, 'annotated')
168 170 if not os.path.isdir(adir):
169 171 os.mkdir(adir)
170 172 cmd = '"%s" "%s" -a "--directory=%s" "--omit=%s"' % (
171 173 sys.executable, os.path.join(TESTDIR, 'coverage.py'),
172 174 adir, omit)
173 175 vlog("# Running: "+cmd)
174 176 os.system(cmd)
175 177
176 178 class Timeout(Exception):
177 179 pass
178 180
179 181 def alarmed(signum, frame):
180 182 raise Timeout
181 183
182 184 def run(cmd):
183 185 """Run command in a sub-process, capturing the output (stdout and stderr).
184 186 Return the exist code, and output."""
185 187 # TODO: Use subprocess.Popen if we're running on Python 2.4
186 188 if os.name == 'nt':
187 189 tochild, fromchild = os.popen4(cmd)
188 190 tochild.close()
189 191 output = fromchild.read()
190 192 ret = fromchild.close()
191 193 if ret == None:
192 194 ret = 0
193 195 else:
194 196 proc = popen2.Popen4(cmd)
195 197 try:
196 198 output = ''
197 199 proc.tochild.close()
198 200 output = proc.fromchild.read()
199 201 ret = proc.wait()
200 202 except Timeout:
201 203 vlog('# Process %d timed out - killing it' % proc.pid)
202 204 os.kill(proc.pid, signal.SIGTERM)
203 205 ret = proc.wait()
204 206 if ret == 0:
205 207 ret = signal.SIGTERM << 8
206 208 return ret, splitnewlines(output)
207 209
208 210 def run_one(test):
209 211 '''tristate output:
210 212 None -> skipped
211 213 True -> passed
212 214 False -> failed'''
213 215
214 216 vlog("# Test", test)
215 217 if not verbose:
216 218 sys.stdout.write('.')
217 219 sys.stdout.flush()
218 220
219 221 # create a fresh hgrc
220 222 hgrc = file(HGRCPATH, 'w+')
221 223 hgrc.close()
222 224
223 225 err = os.path.join(TESTDIR, test+".err")
224 226 ref = os.path.join(TESTDIR, test+".out")
225 227
226 228 if os.path.exists(err):
227 229 os.remove(err) # Remove any previous output files
228 230
229 231 # Make a tmp subdirectory to work in
230 232 tmpd = os.path.join(HGTMP, test)
231 233 os.mkdir(tmpd)
232 234 os.chdir(tmpd)
233 235
234 236 lctest = test.lower()
235 237
236 238 if lctest.endswith('.py'):
237 239 cmd = '%s "%s"' % (sys.executable, os.path.join(TESTDIR, test))
238 240 elif lctest.endswith('.bat'):
239 241 # do not run batch scripts on non-windows
240 242 if os.name != 'nt':
241 243 print '\nSkipping %s: batch script' % test
242 244 return None
243 245 # To reliably get the error code from batch files on WinXP,
244 246 # the "cmd /c call" prefix is needed. Grrr
245 247 cmd = 'cmd /c call "%s"' % (os.path.join(TESTDIR, test))
246 248 else:
247 249 # do not run shell scripts on windows
248 250 if os.name == 'nt':
249 251 print '\nSkipping %s: shell script' % test
250 252 return None
251 253 # do not try to run non-executable programs
252 254 if not os.access(os.path.join(TESTDIR, test), os.X_OK):
253 255 print '\nSkipping %s: not executable' % test
254 256 return None
255 257 cmd = '"%s"' % (os.path.join(TESTDIR, test))
256 258
257 259 if options.timeout > 0:
258 260 signal.alarm(options.timeout)
259 261
260 262 vlog("# Running", cmd)
261 263 ret, out = run(cmd)
262 264 vlog("# Ret was:", ret)
263 265
264 266 if options.timeout > 0:
265 267 signal.alarm(0)
266 268
267 269 diffret = 0
268 270 # If reference output file exists, check test output against it
269 271 if os.path.exists(ref):
270 272 f = open(ref, "r")
271 273 ref_out = splitnewlines(f.read())
272 274 f.close()
273 275 else:
274 276 ref_out = []
275 277 if out != ref_out:
276 278 diffret = 1
277 279 print "\nERROR: %s output changed" % (test)
278 280 show_diff(ref_out, out)
279 281 if ret:
280 282 print "\nERROR: %s failed with error code %d" % (test, ret)
281 283 elif diffret:
282 284 ret = diffret
283 285
284 286 if ret != 0: # Save errors to a file for diagnosis
285 287 f = open(err, "wb")
286 288 for line in out:
287 289 f.write(line)
288 290 f.close()
289 291
290 292 # Kill off any leftover daemon processes
291 293 try:
292 294 fp = file(DAEMON_PIDS)
293 295 for line in fp:
294 296 try:
295 297 pid = int(line)
296 298 except ValueError:
297 299 continue
298 300 try:
299 301 os.kill(pid, 0)
300 302 vlog('# Killing daemon process %d' % pid)
301 303 os.kill(pid, signal.SIGTERM)
302 304 time.sleep(0.25)
303 305 os.kill(pid, 0)
304 306 vlog('# Daemon process %d is stuck - really killing it' % pid)
305 307 os.kill(pid, signal.SIGKILL)
306 308 except OSError, err:
307 309 if err.errno != errno.ESRCH:
308 310 raise
309 311 fp.close()
310 312 os.unlink(DAEMON_PIDS)
311 313 except IOError:
312 314 pass
313 315
314 316 os.chdir(TESTDIR)
315 317 shutil.rmtree(tmpd, True)
316 318 return ret == 0
317 319
318 320
319 321 os.umask(022)
320 322
321 323 check_required_tools()
322 324
323 325 # Reset some environment variables to well-known values so that
324 326 # the tests produce repeatable output.
325 327 os.environ['LANG'] = os.environ['LC_ALL'] = 'C'
326 328 os.environ['TZ'] = 'GMT'
327 329
328 330 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
329 331 os.environ["HGMERGE"] = sys.executable + ' -c "import sys; sys.exit(0)"'
330 332 os.environ["HGUSER"] = "test"
331 333
332 334 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
333 335 HGTMP = os.environ["HGTMP"] = tempfile.mkdtemp("", "hgtests.")
334 336 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
335 337 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
336 338
337 339 vlog("# Using TESTDIR", TESTDIR)
338 340 vlog("# Using HGTMP", HGTMP)
339 341
340 342 INST = os.path.join(HGTMP, "install")
341 343 BINDIR = os.path.join(INST, "bin")
342 344 PYTHONDIR = os.path.join(INST, "lib", "python")
343 345 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
344 346
345 347 try:
346 348 try:
347 349 install_hg()
348 350
349 351 if options.timeout > 0:
350 352 try:
351 353 signal.signal(signal.SIGALRM, alarmed)
352 354 vlog('# Running tests with %d-second timeout' %
353 355 options.timeout)
354 356 except AttributeError:
355 357 print 'WARNING: cannot run tests with timeouts'
356 358 options.timeout = 0
357 359
358 tests = 0
360 tested = 0
359 361 failed = 0
360 362 skipped = 0
361 363
362 364 if len(args) == 0:
363 365 args = os.listdir(".")
364 366 args.sort()
365 367
368
369 tests = []
366 370 for test in args:
367 371 if (test.startswith("test-") and '~' not in test and
368 372 ('.' not in test or test.endswith('.py') or
369 373 test.endswith('.bat'))):
374 tests.append(test)
375
376 if options.restart:
377 orig = list(tests)
378 while tests:
379 if os.path.exists(tests[0] + ".err"):
380 break
381 tests.pop(0)
382 if not tests:
383 print "running all tests"
384 tests = orig
385
386 for test in tests:
370 387 if options.retest and not os.path.exists(test + ".err"):
371 388 skipped += 1
372 389 continue
373 390 ret = run_one(test)
374 391 if ret is None:
375 392 skipped += 1
376 393 elif not ret:
377 394 failed += 1
378 395 if options.first:
379 396 break
380 tests += 1
397 tested += 1
381 398
382 print "\n# Ran %d tests, %d skipped, %d failed." % (tests, skipped,
399 print "\n# Ran %d tests, %d skipped, %d failed." % (tested, skipped,
383 400 failed)
384 401 if coverage:
385 402 output_coverage()
386 403 except KeyboardInterrupt:
387 404 failed = True
388 405 print "\ninterrupted!"
389 406 finally:
390 407 cleanup_exit()
391 408
392 409 if failed:
393 410 sys.exit(1)
General Comments 0
You need to be logged in to leave comments. Login now