##// END OF EJS Templates
run-tests: take the basepath when constructing the test temp dir...
Idan Kamara -
r14264:8e009060 default
parent child Browse files
Show More
@@ -1,1169 +1,1169 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 of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 # Modifying this script is tricky because it has many modes:
11 11 # - serial (default) vs parallel (-jN, N > 1)
12 12 # - no coverage (default) vs coverage (-c, -C, -s)
13 13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 14 # - tests are a mix of shell scripts and Python scripts
15 15 #
16 16 # If you change this script, it is recommended that you ensure you
17 17 # haven't broken it by running it in various modes with a representative
18 18 # sample of test scripts. For example:
19 19 #
20 20 # 1) serial, no coverage, temp install:
21 21 # ./run-tests.py test-s*
22 22 # 2) serial, no coverage, local hg:
23 23 # ./run-tests.py --local test-s*
24 24 # 3) serial, coverage, temp install:
25 25 # ./run-tests.py -c test-s*
26 26 # 4) serial, coverage, local hg:
27 27 # ./run-tests.py -c --local test-s* # unsupported
28 28 # 5) parallel, no coverage, temp install:
29 29 # ./run-tests.py -j2 test-s*
30 30 # 6) parallel, no coverage, local hg:
31 31 # ./run-tests.py -j2 --local test-s*
32 32 # 7) parallel, coverage, temp install:
33 33 # ./run-tests.py -j2 -c test-s* # currently broken
34 34 # 8) parallel, coverage, local install:
35 35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 36 # 9) parallel, custom tmp dir:
37 37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 38 #
39 39 # (You could use any subset of the tests: test-s* happens to match
40 40 # enough that it's worth doing parallel runs, few enough that it
41 41 # completes fairly quickly, includes both shell and Python scripts, and
42 42 # includes some scripts that run daemon processes.)
43 43
44 44 from distutils import version
45 45 import difflib
46 46 import errno
47 47 import optparse
48 48 import os
49 49 import shutil
50 50 import subprocess
51 51 import signal
52 52 import sys
53 53 import tempfile
54 54 import time
55 55 import re
56 56 import threading
57 57
58 58 processlock = threading.Lock()
59 59
60 60 closefds = os.name == 'posix'
61 61 def Popen4(cmd, wd, timeout):
62 62 processlock.acquire()
63 63 orig = os.getcwd()
64 64 os.chdir(wd)
65 65 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
66 66 close_fds=closefds,
67 67 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
68 68 stderr=subprocess.STDOUT)
69 69 os.chdir(orig)
70 70 processlock.release()
71 71
72 72 p.fromchild = p.stdout
73 73 p.tochild = p.stdin
74 74 p.childerr = p.stderr
75 75
76 76 if timeout:
77 77 p.timeout = False
78 78 def t():
79 79 start = time.time()
80 80 while time.time() - start < timeout and p.returncode is None:
81 81 time.sleep(1)
82 82 p.timeout = True
83 83 if p.returncode is None:
84 84 try:
85 85 p.terminate()
86 86 except OSError:
87 87 pass
88 88 threading.Thread(target=t).start()
89 89
90 90 return p
91 91
92 92 # reserved exit code to skip test (used by hghave)
93 93 SKIPPED_STATUS = 80
94 94 SKIPPED_PREFIX = 'skipped: '
95 95 FAILED_PREFIX = 'hghave check failed: '
96 96 PYTHON = sys.executable
97 97 IMPL_PATH = 'PYTHONPATH'
98 98 if 'java' in sys.platform:
99 99 IMPL_PATH = 'JYTHONPATH'
100 100
101 101 requiredtools = ["python", "diff", "grep", "unzip", "gunzip", "bunzip2", "sed"]
102 102
103 103 defaults = {
104 104 'jobs': ('HGTEST_JOBS', 1),
105 105 'timeout': ('HGTEST_TIMEOUT', 180),
106 106 'port': ('HGTEST_PORT', 20059),
107 107 'shell': ('HGTEST_SHELL', '/bin/sh'),
108 108 }
109 109
110 110 def parseargs():
111 111 parser = optparse.OptionParser("%prog [options] [tests]")
112 112
113 113 # keep these sorted
114 114 parser.add_option("--blacklist", action="append",
115 115 help="skip tests listed in the specified blacklist file")
116 116 parser.add_option("-C", "--annotate", action="store_true",
117 117 help="output files annotated with coverage")
118 118 parser.add_option("--child", type="int",
119 119 help="run as child process, summary to given fd")
120 120 parser.add_option("-c", "--cover", action="store_true",
121 121 help="print a test coverage report")
122 122 parser.add_option("-d", "--debug", action="store_true",
123 123 help="debug mode: write output of test scripts to console"
124 124 " rather than capturing and diff'ing it (disables timeout)")
125 125 parser.add_option("-f", "--first", action="store_true",
126 126 help="exit on the first test failure")
127 127 parser.add_option("--inotify", action="store_true",
128 128 help="enable inotify extension when running tests")
129 129 parser.add_option("-i", "--interactive", action="store_true",
130 130 help="prompt to accept changed output")
131 131 parser.add_option("-j", "--jobs", type="int",
132 132 help="number of jobs to run in parallel"
133 133 " (default: $%s or %d)" % defaults['jobs'])
134 134 parser.add_option("--keep-tmpdir", action="store_true",
135 135 help="keep temporary directory after running tests")
136 136 parser.add_option("-k", "--keywords",
137 137 help="run tests matching keywords")
138 138 parser.add_option("-l", "--local", action="store_true",
139 139 help="shortcut for --with-hg=<testdir>/../hg")
140 140 parser.add_option("-n", "--nodiff", action="store_true",
141 141 help="skip showing test changes")
142 142 parser.add_option("-p", "--port", type="int",
143 143 help="port on which servers should listen"
144 144 " (default: $%s or %d)" % defaults['port'])
145 145 parser.add_option("--pure", action="store_true",
146 146 help="use pure Python code instead of C extensions")
147 147 parser.add_option("-R", "--restart", action="store_true",
148 148 help="restart at last error")
149 149 parser.add_option("-r", "--retest", action="store_true",
150 150 help="retest failed tests")
151 151 parser.add_option("-S", "--noskips", action="store_true",
152 152 help="don't report skip tests verbosely")
153 153 parser.add_option("--shell", type="string",
154 154 help="shell to use (default: $%s or %s)" % defaults['shell'])
155 155 parser.add_option("-t", "--timeout", type="int",
156 156 help="kill errant tests after TIMEOUT seconds"
157 157 " (default: $%s or %d)" % defaults['timeout'])
158 158 parser.add_option("--tmpdir", type="string",
159 159 help="run tests in the given temporary directory"
160 160 " (implies --keep-tmpdir)")
161 161 parser.add_option("-v", "--verbose", action="store_true",
162 162 help="output verbose messages")
163 163 parser.add_option("--view", type="string",
164 164 help="external diff viewer")
165 165 parser.add_option("--with-hg", type="string",
166 166 metavar="HG",
167 167 help="test using specified hg script rather than a "
168 168 "temporary installation")
169 169 parser.add_option("-3", "--py3k-warnings", action="store_true",
170 170 help="enable Py3k warnings on Python 2.6+")
171 171 parser.add_option('--extra-config-opt', action="append",
172 172 help='set the given config opt in the test hgrc')
173 173
174 174 for option, (envvar, default) in defaults.items():
175 175 defaults[option] = type(default)(os.environ.get(envvar, default))
176 176 parser.set_defaults(**defaults)
177 177 (options, args) = parser.parse_args()
178 178
179 179 # jython is always pure
180 180 if 'java' in sys.platform or '__pypy__' in sys.modules:
181 181 options.pure = True
182 182
183 183 if not (os.path.isfile(options.shell) and
184 184 os.access(options.shell, os.X_OK)):
185 185 parser.error('--shell must be executable')
186 186
187 187 if options.with_hg:
188 188 if not (os.path.isfile(options.with_hg) and
189 189 os.access(options.with_hg, os.X_OK)):
190 190 parser.error('--with-hg must specify an executable hg script')
191 191 if not os.path.basename(options.with_hg) == 'hg':
192 192 sys.stderr.write('warning: --with-hg should specify an hg script')
193 193 if options.local:
194 194 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
195 195 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
196 196 if not os.access(hgbin, os.X_OK):
197 197 parser.error('--local specified, but %r not found or not executable'
198 198 % hgbin)
199 199 options.with_hg = hgbin
200 200
201 201 options.anycoverage = options.cover or options.annotate
202 202 if options.anycoverage:
203 203 try:
204 204 import coverage
205 205 covver = version.StrictVersion(coverage.__version__).version
206 206 if covver < (3, 3):
207 207 parser.error('coverage options require coverage 3.3 or later')
208 208 except ImportError:
209 209 parser.error('coverage options now require the coverage package')
210 210
211 211 if options.anycoverage and options.local:
212 212 # this needs some path mangling somewhere, I guess
213 213 parser.error("sorry, coverage options do not work when --local "
214 214 "is specified")
215 215
216 216 global vlog
217 217 if options.verbose:
218 218 if options.jobs > 1 or options.child is not None:
219 219 pid = "[%d]" % os.getpid()
220 220 else:
221 221 pid = None
222 222 def vlog(*msg):
223 223 iolock.acquire()
224 224 if pid:
225 225 print pid,
226 226 for m in msg:
227 227 print m,
228 228 print
229 229 sys.stdout.flush()
230 230 iolock.release()
231 231 else:
232 232 vlog = lambda *msg: None
233 233
234 234 if options.tmpdir:
235 235 options.tmpdir = os.path.expanduser(options.tmpdir)
236 236
237 237 if options.jobs < 1:
238 238 parser.error('--jobs must be positive')
239 239 if options.interactive and options.jobs > 1:
240 240 print '(--interactive overrides --jobs)'
241 241 options.jobs = 1
242 242 if options.interactive and options.debug:
243 243 parser.error("-i/--interactive and -d/--debug are incompatible")
244 244 if options.debug:
245 245 if options.timeout != defaults['timeout']:
246 246 sys.stderr.write(
247 247 'warning: --timeout option ignored with --debug\n')
248 248 options.timeout = 0
249 249 if options.py3k_warnings:
250 250 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
251 251 parser.error('--py3k-warnings can only be used on Python 2.6+')
252 252 if options.blacklist:
253 253 blacklist = dict()
254 254 for filename in options.blacklist:
255 255 try:
256 256 path = os.path.expanduser(os.path.expandvars(filename))
257 257 f = open(path, "r")
258 258 except IOError, err:
259 259 if err.errno != errno.ENOENT:
260 260 raise
261 261 print "warning: no such blacklist file: %s" % filename
262 262 continue
263 263
264 264 for line in f.readlines():
265 265 line = line.split('#', 1)[0].strip()
266 266 if line:
267 267 blacklist[line] = filename
268 268
269 269 f.close()
270 270
271 271 options.blacklist = blacklist
272 272
273 273 return (options, args)
274 274
275 275 def rename(src, dst):
276 276 """Like os.rename(), trade atomicity and opened files friendliness
277 277 for existing destination support.
278 278 """
279 279 shutil.copy(src, dst)
280 280 os.remove(src)
281 281
282 282 def splitnewlines(text):
283 283 '''like str.splitlines, but only split on newlines.
284 284 keep line endings.'''
285 285 i = 0
286 286 lines = []
287 287 while True:
288 288 n = text.find('\n', i)
289 289 if n == -1:
290 290 last = text[i:]
291 291 if last:
292 292 lines.append(last)
293 293 return lines
294 294 lines.append(text[i:n + 1])
295 295 i = n + 1
296 296
297 297 def parsehghaveoutput(lines):
298 298 '''Parse hghave log lines.
299 299 Return tuple of lists (missing, failed):
300 300 * the missing/unknown features
301 301 * the features for which existence check failed'''
302 302 missing = []
303 303 failed = []
304 304 for line in lines:
305 305 if line.startswith(SKIPPED_PREFIX):
306 306 line = line.splitlines()[0]
307 307 missing.append(line[len(SKIPPED_PREFIX):])
308 308 elif line.startswith(FAILED_PREFIX):
309 309 line = line.splitlines()[0]
310 310 failed.append(line[len(FAILED_PREFIX):])
311 311
312 312 return missing, failed
313 313
314 314 def showdiff(expected, output, ref, err):
315 315 print
316 316 for line in difflib.unified_diff(expected, output, ref, err):
317 317 sys.stdout.write(line)
318 318
319 319 def findprogram(program):
320 320 """Search PATH for a executable program"""
321 321 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
322 322 name = os.path.join(p, program)
323 323 if os.access(name, os.X_OK):
324 324 return name
325 325 return None
326 326
327 327 def checktools():
328 328 # Before we go any further, check for pre-requisite tools
329 329 # stuff from coreutils (cat, rm, etc) are not tested
330 330 for p in requiredtools:
331 331 if os.name == 'nt':
332 332 p += '.exe'
333 333 found = findprogram(p)
334 334 if found:
335 335 vlog("# Found prerequisite", p, "at", found)
336 336 else:
337 337 print "WARNING: Did not find prerequisite tool: "+p
338 338
339 339 def killdaemons():
340 340 # Kill off any leftover daemon processes
341 341 try:
342 342 fp = open(DAEMON_PIDS)
343 343 for line in fp:
344 344 try:
345 345 pid = int(line)
346 346 except ValueError:
347 347 continue
348 348 try:
349 349 os.kill(pid, 0)
350 350 vlog('# Killing daemon process %d' % pid)
351 351 os.kill(pid, signal.SIGTERM)
352 352 time.sleep(0.25)
353 353 os.kill(pid, 0)
354 354 vlog('# Daemon process %d is stuck - really killing it' % pid)
355 355 os.kill(pid, signal.SIGKILL)
356 356 except OSError, err:
357 357 if err.errno != errno.ESRCH:
358 358 raise
359 359 fp.close()
360 360 os.unlink(DAEMON_PIDS)
361 361 except IOError:
362 362 pass
363 363
364 364 def cleanup(options):
365 365 if not options.keep_tmpdir:
366 366 vlog("# Cleaning up HGTMP", HGTMP)
367 367 shutil.rmtree(HGTMP, True)
368 368
369 369 def usecorrectpython():
370 370 # some tests run python interpreter. they must use same
371 371 # interpreter we use or bad things will happen.
372 372 exedir, exename = os.path.split(sys.executable)
373 373 if exename == 'python':
374 374 path = findprogram('python')
375 375 if os.path.dirname(path) == exedir:
376 376 return
377 377 vlog('# Making python executable in test path use correct Python')
378 378 mypython = os.path.join(BINDIR, 'python')
379 379 try:
380 380 os.symlink(sys.executable, mypython)
381 381 except AttributeError:
382 382 # windows fallback
383 383 shutil.copyfile(sys.executable, mypython)
384 384 shutil.copymode(sys.executable, mypython)
385 385
386 386 def installhg(options):
387 387 vlog("# Performing temporary installation of HG")
388 388 installerrs = os.path.join("tests", "install.err")
389 389 pure = options.pure and "--pure" or ""
390 390
391 391 # Run installer in hg root
392 392 script = os.path.realpath(sys.argv[0])
393 393 hgroot = os.path.dirname(os.path.dirname(script))
394 394 os.chdir(hgroot)
395 395 nohome = '--home=""'
396 396 if os.name == 'nt':
397 397 # The --home="" trick works only on OS where os.sep == '/'
398 398 # because of a distutils convert_path() fast-path. Avoid it at
399 399 # least on Windows for now, deal with .pydistutils.cfg bugs
400 400 # when they happen.
401 401 nohome = ''
402 402 cmd = ('%s setup.py %s clean --all'
403 403 ' build --build-base="%s"'
404 404 ' install --force --prefix="%s" --install-lib="%s"'
405 405 ' --install-scripts="%s" %s >%s 2>&1'
406 406 % (sys.executable, pure, os.path.join(HGTMP, "build"),
407 407 INST, PYTHONDIR, BINDIR, nohome, installerrs))
408 408 vlog("# Running", cmd)
409 409 if os.system(cmd) == 0:
410 410 if not options.verbose:
411 411 os.remove(installerrs)
412 412 else:
413 413 f = open(installerrs)
414 414 for line in f:
415 415 print line,
416 416 f.close()
417 417 sys.exit(1)
418 418 os.chdir(TESTDIR)
419 419
420 420 usecorrectpython()
421 421
422 422 vlog("# Installing dummy diffstat")
423 423 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
424 424 f.write('#!' + sys.executable + '\n'
425 425 'import sys\n'
426 426 'files = 0\n'
427 427 'for line in sys.stdin:\n'
428 428 ' if line.startswith("diff "):\n'
429 429 ' files += 1\n'
430 430 'sys.stdout.write("files patched: %d\\n" % files)\n')
431 431 f.close()
432 432 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
433 433
434 434 if options.py3k_warnings and not options.anycoverage:
435 435 vlog("# Updating hg command to enable Py3k Warnings switch")
436 436 f = open(os.path.join(BINDIR, 'hg'), 'r')
437 437 lines = [line.rstrip() for line in f]
438 438 lines[0] += ' -3'
439 439 f.close()
440 440 f = open(os.path.join(BINDIR, 'hg'), 'w')
441 441 for line in lines:
442 442 f.write(line + '\n')
443 443 f.close()
444 444
445 445 if options.anycoverage:
446 446 custom = os.path.join(TESTDIR, 'sitecustomize.py')
447 447 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
448 448 vlog('# Installing coverage trigger to %s' % target)
449 449 shutil.copyfile(custom, target)
450 450 rc = os.path.join(TESTDIR, '.coveragerc')
451 451 vlog('# Installing coverage rc to %s' % rc)
452 452 os.environ['COVERAGE_PROCESS_START'] = rc
453 453 fn = os.path.join(INST, '..', '.coverage')
454 454 os.environ['COVERAGE_FILE'] = fn
455 455
456 456 def outputcoverage(options):
457 457
458 458 vlog('# Producing coverage report')
459 459 os.chdir(PYTHONDIR)
460 460
461 461 def covrun(*args):
462 462 cmd = 'coverage %s' % ' '.join(args)
463 463 vlog('# Running: %s' % cmd)
464 464 os.system(cmd)
465 465
466 466 if options.child:
467 467 return
468 468
469 469 covrun('-c')
470 470 omit = ','.join([BINDIR, TESTDIR])
471 471 covrun('-i', '-r', '"--omit=%s"' % omit) # report
472 472 if options.annotate:
473 473 adir = os.path.join(TESTDIR, 'annotated')
474 474 if not os.path.isdir(adir):
475 475 os.mkdir(adir)
476 476 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
477 477
478 478 def pytest(test, wd, options, replacements):
479 479 py3kswitch = options.py3k_warnings and ' -3' or ''
480 480 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
481 481 vlog("# Running", cmd)
482 482 return run(cmd, wd, options, replacements)
483 483
484 484 def shtest(test, wd, options, replacements):
485 485 cmd = '"%s"' % test
486 486 vlog("# Running", cmd)
487 487 return run(cmd, wd, options, replacements)
488 488
489 489 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
490 490 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
491 491 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
492 492 escapemap.update({'\\': '\\\\', '\r': r'\r'})
493 493 def escapef(m):
494 494 return escapemap[m.group(0)]
495 495 def stringescape(s):
496 496 return escapesub(escapef, s)
497 497
498 498 def tsttest(test, wd, options, replacements):
499 499 t = open(test)
500 500 out = []
501 501 script = []
502 502 salt = "SALT" + str(time.time())
503 503
504 504 pos = prepos = -1
505 505 after = {}
506 506 expected = {}
507 507 for n, l in enumerate(t):
508 508 if not l.endswith('\n'):
509 509 l += '\n'
510 510 if l.startswith(' $ '): # commands
511 511 after.setdefault(pos, []).append(l)
512 512 prepos = pos
513 513 pos = n
514 514 script.append('echo %s %s $?\n' % (salt, n))
515 515 script.append(l[4:])
516 516 elif l.startswith(' > '): # continuations
517 517 after.setdefault(prepos, []).append(l)
518 518 script.append(l[4:])
519 519 elif l.startswith(' '): # results
520 520 # queue up a list of expected results
521 521 expected.setdefault(pos, []).append(l[2:])
522 522 else:
523 523 # non-command/result - queue up for merged output
524 524 after.setdefault(pos, []).append(l)
525 525
526 526 t.close()
527 527
528 528 script.append('echo %s %s $?\n' % (salt, n + 1))
529 529
530 530 fd, name = tempfile.mkstemp(suffix='hg-tst')
531 531
532 532 try:
533 533 for l in script:
534 534 os.write(fd, l)
535 535 os.close(fd)
536 536
537 537 cmd = '"%s" "%s"' % (options.shell, name)
538 538 vlog("# Running", cmd)
539 539 exitcode, output = run(cmd, wd, options, replacements)
540 540 # do not merge output if skipped, return hghave message instead
541 541 # similarly, with --debug, output is None
542 542 if exitcode == SKIPPED_STATUS or output is None:
543 543 return exitcode, output
544 544 finally:
545 545 os.remove(name)
546 546
547 547 def rematch(el, l):
548 548 try:
549 549 # ensure that the regex matches to the end of the string
550 550 return re.match(el + r'\Z', l)
551 551 except re.error:
552 552 # el is an invalid regex
553 553 return False
554 554
555 555 def globmatch(el, l):
556 556 # The only supported special characters are * and ?. Escaping is
557 557 # supported.
558 558 i, n = 0, len(el)
559 559 res = ''
560 560 while i < n:
561 561 c = el[i]
562 562 i += 1
563 563 if c == '\\' and el[i] in '*?\\':
564 564 res += el[i - 1:i + 1]
565 565 i += 1
566 566 elif c == '*':
567 567 res += '.*'
568 568 elif c == '?':
569 569 res += '.'
570 570 else:
571 571 res += re.escape(c)
572 572 return rematch(res, l)
573 573
574 574 pos = -1
575 575 postout = []
576 576 ret = 0
577 577 for n, l in enumerate(output):
578 578 lout, lcmd = l, None
579 579 if salt in l:
580 580 lout, lcmd = l.split(salt, 1)
581 581
582 582 if lout:
583 583 if lcmd:
584 584 lout += ' (no-eol)\n'
585 585
586 586 el = None
587 587 if pos in expected and expected[pos]:
588 588 el = expected[pos].pop(0)
589 589
590 590 if el == lout: # perfect match (fast)
591 591 postout.append(" " + lout)
592 592 elif (el and
593 593 (el.endswith(" (re)\n") and rematch(el[:-6] + '\n', lout) or
594 594 el.endswith(" (glob)\n") and globmatch(el[:-8] + '\n', lout)
595 595 or el.endswith(" (esc)\n") and
596 596 el.decode('string-escape') == l)):
597 597 postout.append(" " + el) # fallback regex/glob/esc match
598 598 else:
599 599 if needescape(lout):
600 600 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
601 601 postout.append(" " + lout) # let diff deal with it
602 602
603 603 if lcmd:
604 604 # add on last return code
605 605 ret = int(lcmd.split()[1])
606 606 if ret != 0:
607 607 postout.append(" [%s]\n" % ret)
608 608 if pos in after:
609 609 postout += after.pop(pos)
610 610 pos = int(lcmd.split()[0])
611 611
612 612 if pos in after:
613 613 postout += after.pop(pos)
614 614
615 615 return exitcode, postout
616 616
617 617 wifexited = getattr(os, "WIFEXITED", lambda x: False)
618 618 def run(cmd, wd, options, replacements):
619 619 """Run command in a sub-process, capturing the output (stdout and stderr).
620 620 Return a tuple (exitcode, output). output is None in debug mode."""
621 621 # TODO: Use subprocess.Popen if we're running on Python 2.4
622 622 if options.debug:
623 623 proc = subprocess.Popen(cmd, shell=True)
624 624 ret = proc.wait()
625 625 return (ret, None)
626 626
627 627 if os.name == 'nt' or sys.platform.startswith('java'):
628 628 tochild, fromchild = os.popen4(cmd)
629 629 tochild.close()
630 630 output = fromchild.read()
631 631 ret = fromchild.close()
632 632 if ret is None:
633 633 ret = 0
634 634 else:
635 635 proc = Popen4(cmd, wd, options.timeout)
636 636 def cleanup():
637 637 try:
638 638 proc.terminate()
639 639 except OSError:
640 640 pass
641 641 ret = proc.wait()
642 642 if ret == 0:
643 643 ret = signal.SIGTERM << 8
644 644 killdaemons()
645 645 return ret
646 646
647 647 output = ''
648 648 proc.tochild.close()
649 649
650 650 try:
651 651 output = proc.fromchild.read()
652 652 except KeyboardInterrupt:
653 653 vlog('# Handling keyboard interrupt')
654 654 cleanup()
655 655 raise
656 656
657 657 ret = proc.wait()
658 658 if wifexited(ret):
659 659 ret = os.WEXITSTATUS(ret)
660 660
661 661 if proc.timeout:
662 662 ret = 'timeout'
663 663
664 664 if ret:
665 665 killdaemons()
666 666
667 667 for s, r in replacements:
668 668 output = re.sub(s, r, output)
669 669 return ret, splitnewlines(output)
670 670
671 671 def runone(options, test):
672 672 '''tristate output:
673 673 None -> skipped
674 674 True -> passed
675 675 False -> failed'''
676 676
677 677 global results, resultslock, iolock
678 678
679 679 testpath = os.path.join(TESTDIR, test)
680 680
681 681 def result(l, e):
682 682 resultslock.acquire()
683 683 results[l].append(e)
684 684 resultslock.release()
685 685
686 686 def skip(msg):
687 687 if not options.verbose:
688 688 result('s', (test, msg))
689 689 else:
690 690 iolock.acquire()
691 691 print "\nSkipping %s: %s" % (testpath, msg)
692 692 iolock.release()
693 693 return None
694 694
695 695 def fail(msg, ret):
696 696 if not options.nodiff:
697 697 iolock.acquire()
698 698 print "\nERROR: %s %s" % (testpath, msg)
699 699 iolock.release()
700 700 if (not ret and options.interactive
701 701 and os.path.exists(testpath + ".err")):
702 702 iolock.acquire()
703 703 print "Accept this change? [n] ",
704 704 answer = sys.stdin.readline().strip()
705 705 iolock.release()
706 706 if answer.lower() in "y yes".split():
707 707 if test.endswith(".t"):
708 708 rename(testpath + ".err", testpath)
709 709 else:
710 710 rename(testpath + ".err", testpath + ".out")
711 711 return
712 712 result('f', (test, msg))
713 713
714 714 def success():
715 715 result('p', test)
716 716
717 717 def ignore(msg):
718 718 result('i', (test, msg))
719 719
720 if (test.startswith("test-") and '~' not in test and
720 if (os.path.basename(test).startswith("test-") and '~' not in test and
721 721 ('.' not in test or test.endswith('.py') or
722 722 test.endswith('.bat') or test.endswith('.t'))):
723 723 if not os.path.exists(test):
724 724 skip("doesn't exist")
725 725 return None
726 726 else:
727 727 return None # not a supported test, don't record
728 728
729 729 if options.blacklist:
730 730 filename = options.blacklist.get(test)
731 731 if filename is not None:
732 732 skip("blacklisted")
733 733 return None
734 734
735 735 if options.retest and not os.path.exists(test + ".err"):
736 736 ignore("not retesting")
737 737 return None
738 738
739 739 if options.keywords:
740 740 fp = open(test)
741 741 t = fp.read().lower() + test.lower()
742 742 fp.close()
743 743 for k in options.keywords.lower().split():
744 744 if k in t:
745 745 break
746 746 else:
747 747 ignore("doesn't match keyword")
748 748 return None
749 749
750 750 vlog("# Test", test)
751 751
752 752 # create a fresh hgrc
753 753 hgrc = open(HGRCPATH, 'w+')
754 754 hgrc.write('[ui]\n')
755 755 hgrc.write('slash = True\n')
756 756 hgrc.write('[defaults]\n')
757 757 hgrc.write('backout = -d "0 0"\n')
758 758 hgrc.write('commit = -d "0 0"\n')
759 759 hgrc.write('tag = -d "0 0"\n')
760 760 if options.inotify:
761 761 hgrc.write('[extensions]\n')
762 762 hgrc.write('inotify=\n')
763 763 hgrc.write('[inotify]\n')
764 764 hgrc.write('pidfile=%s\n' % DAEMON_PIDS)
765 765 hgrc.write('appendpid=True\n')
766 766 if options.extra_config_opt:
767 767 for opt in options.extra_config_opt:
768 768 section, key = opt.split('.', 1)
769 769 assert '=' in key, ('extra config opt %s must '
770 770 'have an = for assignment' % opt)
771 771 hgrc.write('[%s]\n%s\n' % (section, key))
772 772 hgrc.close()
773 773
774 774 ref = os.path.join(TESTDIR, test+".out")
775 775 err = os.path.join(TESTDIR, test+".err")
776 776 if os.path.exists(err):
777 777 os.remove(err) # Remove any previous output files
778 778 try:
779 779 tf = open(testpath)
780 780 firstline = tf.readline().rstrip()
781 781 tf.close()
782 782 except:
783 783 firstline = ''
784 784 lctest = test.lower()
785 785
786 786 if lctest.endswith('.py') or firstline == '#!/usr/bin/env python':
787 787 runner = pytest
788 788 elif lctest.endswith('.t'):
789 789 runner = tsttest
790 790 ref = testpath
791 791 else:
792 792 # do not try to run non-executable programs
793 793 if not os.access(testpath, os.X_OK):
794 794 return skip("not executable")
795 795 runner = shtest
796 796
797 797 # Make a tmp subdirectory to work in
798 798 testtmp = os.environ["TESTTMP"] = os.environ["HOME"] = \
799 os.path.join(HGTMP, test)
799 os.path.join(HGTMP, os.path.basename(test))
800 800
801 801 os.mkdir(testtmp)
802 802 ret, out = runner(testpath, testtmp, options, [
803 803 (re.escape(testtmp), '$TESTTMP'),
804 804 (r':%s\b' % options.port, ':$HGPORT'),
805 805 (r':%s\b' % (options.port + 1), ':$HGPORT1'),
806 806 (r':%s\b' % (options.port + 2), ':$HGPORT2'),
807 807 ])
808 808 vlog("# Ret was:", ret)
809 809
810 810 mark = '.'
811 811
812 812 skipped = (ret == SKIPPED_STATUS)
813 813
814 814 # If we're not in --debug mode and reference output file exists,
815 815 # check test output against it.
816 816 if options.debug:
817 817 refout = None # to match "out is None"
818 818 elif os.path.exists(ref):
819 819 f = open(ref, "r")
820 820 refout = splitnewlines(f.read())
821 821 f.close()
822 822 else:
823 823 refout = []
824 824
825 825 if (ret != 0 or out != refout) and not skipped and not options.debug:
826 826 # Save errors to a file for diagnosis
827 827 f = open(err, "wb")
828 828 for line in out:
829 829 f.write(line)
830 830 f.close()
831 831
832 832 if skipped:
833 833 mark = 's'
834 834 if out is None: # debug mode: nothing to parse
835 835 missing = ['unknown']
836 836 failed = None
837 837 else:
838 838 missing, failed = parsehghaveoutput(out)
839 839 if not missing:
840 840 missing = ['irrelevant']
841 841 if failed:
842 842 fail("hghave failed checking for %s" % failed[-1], ret)
843 843 skipped = False
844 844 else:
845 845 skip(missing[-1])
846 846 elif ret == 'timeout':
847 847 mark = 't'
848 848 fail("timed out", ret)
849 849 elif out != refout:
850 850 mark = '!'
851 851 if not options.nodiff:
852 852 iolock.acquire()
853 853 if options.view:
854 854 os.system("%s %s %s" % (options.view, ref, err))
855 855 else:
856 856 showdiff(refout, out, ref, err)
857 857 iolock.release()
858 858 if ret:
859 859 fail("output changed and returned error code %d" % ret, ret)
860 860 else:
861 861 fail("output changed", ret)
862 862 ret = 1
863 863 elif ret:
864 864 mark = '!'
865 865 fail("returned error code %d" % ret, ret)
866 866 else:
867 867 success()
868 868
869 869 if not options.verbose:
870 870 iolock.acquire()
871 871 sys.stdout.write(mark)
872 872 sys.stdout.flush()
873 873 iolock.release()
874 874
875 875 killdaemons()
876 876
877 877 if not options.keep_tmpdir:
878 878 shutil.rmtree(testtmp, True)
879 879 if skipped:
880 880 return None
881 881 return ret == 0
882 882
883 883 _hgpath = None
884 884
885 885 def _gethgpath():
886 886 """Return the path to the mercurial package that is actually found by
887 887 the current Python interpreter."""
888 888 global _hgpath
889 889 if _hgpath is not None:
890 890 return _hgpath
891 891
892 892 cmd = '%s -c "import mercurial; print mercurial.__path__[0]"'
893 893 pipe = os.popen(cmd % PYTHON)
894 894 try:
895 895 _hgpath = pipe.read().strip()
896 896 finally:
897 897 pipe.close()
898 898 return _hgpath
899 899
900 900 def _checkhglib(verb):
901 901 """Ensure that the 'mercurial' package imported by python is
902 902 the one we expect it to be. If not, print a warning to stderr."""
903 903 expecthg = os.path.join(PYTHONDIR, 'mercurial')
904 904 actualhg = _gethgpath()
905 905 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
906 906 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
907 907 ' (expected %s)\n'
908 908 % (verb, actualhg, expecthg))
909 909
910 910 def runchildren(options, tests):
911 911 if INST:
912 912 installhg(options)
913 913 _checkhglib("Testing")
914 914
915 915 optcopy = dict(options.__dict__)
916 916 optcopy['jobs'] = 1
917 917 del optcopy['blacklist']
918 918 if optcopy['with_hg'] is None:
919 919 optcopy['with_hg'] = os.path.join(BINDIR, "hg")
920 920 optcopy.pop('anycoverage', None)
921 921
922 922 opts = []
923 923 for opt, value in optcopy.iteritems():
924 924 name = '--' + opt.replace('_', '-')
925 925 if value is True:
926 926 opts.append(name)
927 927 elif isinstance(value, list):
928 928 for v in value:
929 929 opts.append(name + '=' + str(v))
930 930 elif value is not None:
931 931 opts.append(name + '=' + str(value))
932 932
933 933 tests.reverse()
934 934 jobs = [[] for j in xrange(options.jobs)]
935 935 while tests:
936 936 for job in jobs:
937 937 if not tests:
938 938 break
939 939 job.append(tests.pop())
940 940 fps = {}
941 941
942 942 for j, job in enumerate(jobs):
943 943 if not job:
944 944 continue
945 945 rfd, wfd = os.pipe()
946 946 childopts = ['--child=%d' % wfd, '--port=%d' % (options.port + j * 3)]
947 947 childtmp = os.path.join(HGTMP, 'child%d' % j)
948 948 childopts += ['--tmpdir', childtmp]
949 949 cmdline = [PYTHON, sys.argv[0]] + opts + childopts + job
950 950 vlog(' '.join(cmdline))
951 951 fps[os.spawnvp(os.P_NOWAIT, cmdline[0], cmdline)] = os.fdopen(rfd, 'r')
952 952 os.close(wfd)
953 953 signal.signal(signal.SIGINT, signal.SIG_IGN)
954 954 failures = 0
955 955 tested, skipped, failed = 0, 0, 0
956 956 skips = []
957 957 fails = []
958 958 while fps:
959 959 pid, status = os.wait()
960 960 fp = fps.pop(pid)
961 961 l = fp.read().splitlines()
962 962 try:
963 963 test, skip, fail = map(int, l[:3])
964 964 except ValueError:
965 965 test, skip, fail = 0, 0, 0
966 966 split = -fail or len(l)
967 967 for s in l[3:split]:
968 968 skips.append(s.split(" ", 1))
969 969 for s in l[split:]:
970 970 fails.append(s.split(" ", 1))
971 971 tested += test
972 972 skipped += skip
973 973 failed += fail
974 974 vlog('pid %d exited, status %d' % (pid, status))
975 975 failures |= status
976 976 print
977 977 if not options.noskips:
978 978 for s in skips:
979 979 print "Skipped %s: %s" % (s[0], s[1])
980 980 for s in fails:
981 981 print "Failed %s: %s" % (s[0], s[1])
982 982
983 983 _checkhglib("Tested")
984 984 print "# Ran %d tests, %d skipped, %d failed." % (
985 985 tested, skipped, failed)
986 986
987 987 if options.anycoverage:
988 988 outputcoverage(options)
989 989 sys.exit(failures != 0)
990 990
991 991 results = dict(p=[], f=[], s=[], i=[])
992 992 resultslock = threading.Lock()
993 993 iolock = threading.Lock()
994 994
995 995 def runqueue(options, tests, results):
996 996 for test in tests:
997 997 ret = runone(options, test)
998 998 if options.first and ret is not None and not ret:
999 999 break
1000 1000
1001 1001 def runtests(options, tests):
1002 1002 global DAEMON_PIDS, HGRCPATH
1003 1003 DAEMON_PIDS = os.environ["DAEMON_PIDS"] = os.path.join(HGTMP, 'daemon.pids')
1004 1004 HGRCPATH = os.environ["HGRCPATH"] = os.path.join(HGTMP, '.hgrc')
1005 1005
1006 1006 try:
1007 1007 if INST:
1008 1008 installhg(options)
1009 1009 _checkhglib("Testing")
1010 1010
1011 1011 if options.restart:
1012 1012 orig = list(tests)
1013 1013 while tests:
1014 1014 if os.path.exists(tests[0] + ".err"):
1015 1015 break
1016 1016 tests.pop(0)
1017 1017 if not tests:
1018 1018 print "running all tests"
1019 1019 tests = orig
1020 1020
1021 1021 runqueue(options, tests, results)
1022 1022
1023 1023 failed = len(results['f'])
1024 1024 tested = len(results['p']) + failed
1025 1025 skipped = len(results['s'])
1026 1026 ignored = len(results['i'])
1027 1027
1028 1028 if options.child:
1029 1029 fp = os.fdopen(options.child, 'w')
1030 1030 fp.write('%d\n%d\n%d\n' % (tested, skipped, failed))
1031 1031 for s in results['s']:
1032 1032 fp.write("%s %s\n" % s)
1033 1033 for s in results['f']:
1034 1034 fp.write("%s %s\n" % s)
1035 1035 fp.close()
1036 1036 else:
1037 1037 print
1038 1038 for s in results['s']:
1039 1039 print "Skipped %s: %s" % s
1040 1040 for s in results['f']:
1041 1041 print "Failed %s: %s" % s
1042 1042 _checkhglib("Tested")
1043 1043 print "# Ran %d tests, %d skipped, %d failed." % (
1044 1044 tested, skipped + ignored, failed)
1045 1045
1046 1046 if options.anycoverage:
1047 1047 outputcoverage(options)
1048 1048 except KeyboardInterrupt:
1049 1049 failed = True
1050 1050 print "\ninterrupted!"
1051 1051
1052 1052 if failed:
1053 1053 sys.exit(1)
1054 1054
1055 1055 def main():
1056 1056 (options, args) = parseargs()
1057 1057 if not options.child:
1058 1058 os.umask(022)
1059 1059
1060 1060 checktools()
1061 1061
1062 1062 if len(args) == 0:
1063 1063 args = os.listdir(".")
1064 1064 args.sort()
1065 1065
1066 1066 tests = args
1067 1067
1068 1068 # Reset some environment variables to well-known values so that
1069 1069 # the tests produce repeatable output.
1070 1070 os.environ['LANG'] = os.environ['LC_ALL'] = os.environ['LANGUAGE'] = 'C'
1071 1071 os.environ['TZ'] = 'GMT'
1072 1072 os.environ["EMAIL"] = "Foo Bar <foo.bar@example.com>"
1073 1073 os.environ['CDPATH'] = ''
1074 1074 os.environ['COLUMNS'] = '80'
1075 1075 os.environ['GREP_OPTIONS'] = ''
1076 1076 os.environ['http_proxy'] = ''
1077 1077
1078 1078 # unset env related to hooks
1079 1079 for k in os.environ.keys():
1080 1080 if k.startswith('HG_'):
1081 1081 # can't remove on solaris
1082 1082 os.environ[k] = ''
1083 1083 del os.environ[k]
1084 1084
1085 1085 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1086 1086 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1087 1087 if options.tmpdir:
1088 1088 options.keep_tmpdir = True
1089 1089 tmpdir = options.tmpdir
1090 1090 if os.path.exists(tmpdir):
1091 1091 # Meaning of tmpdir has changed since 1.3: we used to create
1092 1092 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1093 1093 # tmpdir already exists.
1094 1094 sys.exit("error: temp dir %r already exists" % tmpdir)
1095 1095
1096 1096 # Automatically removing tmpdir sounds convenient, but could
1097 1097 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1098 1098 # or "--tmpdir=$HOME".
1099 1099 #vlog("# Removing temp dir", tmpdir)
1100 1100 #shutil.rmtree(tmpdir)
1101 1101 os.makedirs(tmpdir)
1102 1102 else:
1103 1103 tmpdir = tempfile.mkdtemp('', 'hgtests.')
1104 1104 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1105 1105 DAEMON_PIDS = None
1106 1106 HGRCPATH = None
1107 1107
1108 1108 os.environ["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
1109 1109 os.environ["HGMERGE"] = "internal:merge"
1110 1110 os.environ["HGUSER"] = "test"
1111 1111 os.environ["HGENCODING"] = "ascii"
1112 1112 os.environ["HGENCODINGMODE"] = "strict"
1113 1113 os.environ["HGPORT"] = str(options.port)
1114 1114 os.environ["HGPORT1"] = str(options.port + 1)
1115 1115 os.environ["HGPORT2"] = str(options.port + 2)
1116 1116
1117 1117 if options.with_hg:
1118 1118 INST = None
1119 1119 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1120 1120
1121 1121 # This looks redundant with how Python initializes sys.path from
1122 1122 # the location of the script being executed. Needed because the
1123 1123 # "hg" specified by --with-hg is not the only Python script
1124 1124 # executed in the test suite that needs to import 'mercurial'
1125 1125 # ... which means it's not really redundant at all.
1126 1126 PYTHONDIR = BINDIR
1127 1127 else:
1128 1128 INST = os.path.join(HGTMP, "install")
1129 1129 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1130 1130 PYTHONDIR = os.path.join(INST, "lib", "python")
1131 1131
1132 1132 os.environ["BINDIR"] = BINDIR
1133 1133 os.environ["PYTHON"] = PYTHON
1134 1134
1135 1135 if not options.child:
1136 1136 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1137 1137 os.environ["PATH"] = os.pathsep.join(path)
1138 1138
1139 1139 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1140 1140 # can run .../tests/run-tests.py test-foo where test-foo
1141 1141 # adds an extension to HGRC
1142 1142 pypath = [PYTHONDIR, TESTDIR]
1143 1143 # We have to augment PYTHONPATH, rather than simply replacing
1144 1144 # it, in case external libraries are only available via current
1145 1145 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1146 1146 # are in /opt/subversion.)
1147 1147 oldpypath = os.environ.get(IMPL_PATH)
1148 1148 if oldpypath:
1149 1149 pypath.append(oldpypath)
1150 1150 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1151 1151
1152 1152 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1153 1153
1154 1154 vlog("# Using TESTDIR", TESTDIR)
1155 1155 vlog("# Using HGTMP", HGTMP)
1156 1156 vlog("# Using PATH", os.environ["PATH"])
1157 1157 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1158 1158
1159 1159 try:
1160 1160 if len(tests) > 1 and options.jobs > 1:
1161 1161 runchildren(options, tests)
1162 1162 else:
1163 1163 runtests(options, tests)
1164 1164 finally:
1165 1165 time.sleep(1)
1166 1166 cleanup(options)
1167 1167
1168 1168 if __name__ == '__main__':
1169 1169 main()
General Comments 0
You need to be logged in to leave comments. Login now