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