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