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