##// END OF EJS Templates
run-tests: forward Python USER_BASE from site (issue5425)...
Augie Fackler -
r30420:270b077d default
parent child Browse files
Show More
@@ -1,2569 +1,2576 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 # 10) parallel, pure, tests that call run-tests:
39 39 # ./run-tests.py --pure `grep -l run-tests.py *.t`
40 40 #
41 41 # (You could use any subset of the tests: test-s* happens to match
42 42 # enough that it's worth doing parallel runs, few enough that it
43 43 # completes fairly quickly, includes both shell and Python scripts, and
44 44 # includes some scripts that run daemon processes.)
45 45
46 46 from __future__ import absolute_import, print_function
47 47
48 48 import difflib
49 49 import distutils.version as version
50 50 import errno
51 51 import json
52 52 import optparse
53 53 import os
54 54 import random
55 55 import re
56 56 import shutil
57 57 import signal
58 58 import socket
59 59 import subprocess
60 60 import sys
61 try:
62 import sysconfig
63 except ImportError:
64 # sysconfig doesn't exist in Python 2.6
65 sysconfig = None
61 66 import tempfile
62 67 import threading
63 68 import time
64 69 import unittest
65 70 import xml.dom.minidom as minidom
66 71
67 72 try:
68 73 import Queue as queue
69 74 except ImportError:
70 75 import queue
71 76
72 77 if os.environ.get('RTUNICODEPEDANTRY', False):
73 78 try:
74 79 reload(sys)
75 80 sys.setdefaultencoding("undefined")
76 81 except NameError:
77 82 pass
78 83
79 84 osenvironb = getattr(os, 'environb', os.environ)
80 85 processlock = threading.Lock()
81 86
82 87 if sys.version_info > (3, 5, 0):
83 88 PYTHON3 = True
84 89 xrange = range # we use xrange in one place, and we'd rather not use range
85 90 def _bytespath(p):
86 91 return p.encode('utf-8')
87 92
88 93 def _strpath(p):
89 94 return p.decode('utf-8')
90 95
91 96 elif sys.version_info >= (3, 0, 0):
92 97 print('%s is only supported on Python 3.5+ and 2.6-2.7, not %s' %
93 98 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
94 99 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
95 100 else:
96 101 PYTHON3 = False
97 102
98 103 # In python 2.x, path operations are generally done using
99 104 # bytestrings by default, so we don't have to do any extra
100 105 # fiddling there. We define the wrapper functions anyway just to
101 106 # help keep code consistent between platforms.
102 107 def _bytespath(p):
103 108 return p
104 109
105 110 _strpath = _bytespath
106 111
107 112 # For Windows support
108 113 wifexited = getattr(os, "WIFEXITED", lambda x: False)
109 114
110 115 def checkportisavailable(port):
111 116 """return true if a port seems free to bind on localhost"""
112 117 try:
113 118 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
114 119 s.bind(('localhost', port))
115 120 s.close()
116 121 return True
117 122 except socket.error as exc:
118 123 if not exc.errno == errno.EADDRINUSE:
119 124 raise
120 125 return False
121 126
122 127 closefds = os.name == 'posix'
123 128 def Popen4(cmd, wd, timeout, env=None):
124 129 processlock.acquire()
125 130 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
126 131 close_fds=closefds,
127 132 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
128 133 stderr=subprocess.STDOUT)
129 134 processlock.release()
130 135
131 136 p.fromchild = p.stdout
132 137 p.tochild = p.stdin
133 138 p.childerr = p.stderr
134 139
135 140 p.timeout = False
136 141 if timeout:
137 142 def t():
138 143 start = time.time()
139 144 while time.time() - start < timeout and p.returncode is None:
140 145 time.sleep(.1)
141 146 p.timeout = True
142 147 if p.returncode is None:
143 148 terminate(p)
144 149 threading.Thread(target=t).start()
145 150
146 151 return p
147 152
148 153 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
149 154 IMPL_PATH = b'PYTHONPATH'
150 155 if 'java' in sys.platform:
151 156 IMPL_PATH = b'JYTHONPATH'
152 157
153 158 defaults = {
154 159 'jobs': ('HGTEST_JOBS', 1),
155 160 'timeout': ('HGTEST_TIMEOUT', 180),
156 161 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
157 162 'port': ('HGTEST_PORT', 20059),
158 163 'shell': ('HGTEST_SHELL', 'sh'),
159 164 }
160 165
161 166 def canonpath(path):
162 167 return os.path.realpath(os.path.expanduser(path))
163 168
164 169 def parselistfiles(files, listtype, warn=True):
165 170 entries = dict()
166 171 for filename in files:
167 172 try:
168 173 path = os.path.expanduser(os.path.expandvars(filename))
169 174 f = open(path, "rb")
170 175 except IOError as err:
171 176 if err.errno != errno.ENOENT:
172 177 raise
173 178 if warn:
174 179 print("warning: no such %s file: %s" % (listtype, filename))
175 180 continue
176 181
177 182 for line in f.readlines():
178 183 line = line.split(b'#', 1)[0].strip()
179 184 if line:
180 185 entries[line] = filename
181 186
182 187 f.close()
183 188 return entries
184 189
185 190 def getparser():
186 191 """Obtain the OptionParser used by the CLI."""
187 192 parser = optparse.OptionParser("%prog [options] [tests]")
188 193
189 194 # keep these sorted
190 195 parser.add_option("--blacklist", action="append",
191 196 help="skip tests listed in the specified blacklist file")
192 197 parser.add_option("--whitelist", action="append",
193 198 help="always run tests listed in the specified whitelist file")
194 199 parser.add_option("--changed", type="string",
195 200 help="run tests that are changed in parent rev or working directory")
196 201 parser.add_option("-C", "--annotate", action="store_true",
197 202 help="output files annotated with coverage")
198 203 parser.add_option("-c", "--cover", action="store_true",
199 204 help="print a test coverage report")
200 205 parser.add_option("-d", "--debug", action="store_true",
201 206 help="debug mode: write output of test scripts to console"
202 207 " rather than capturing and diffing it (disables timeout)")
203 208 parser.add_option("-f", "--first", action="store_true",
204 209 help="exit on the first test failure")
205 210 parser.add_option("-H", "--htmlcov", action="store_true",
206 211 help="create an HTML report of the coverage of the files")
207 212 parser.add_option("-i", "--interactive", action="store_true",
208 213 help="prompt to accept changed output")
209 214 parser.add_option("-j", "--jobs", type="int",
210 215 help="number of jobs to run in parallel"
211 216 " (default: $%s or %d)" % defaults['jobs'])
212 217 parser.add_option("--keep-tmpdir", action="store_true",
213 218 help="keep temporary directory after running tests")
214 219 parser.add_option("-k", "--keywords",
215 220 help="run tests matching keywords")
216 221 parser.add_option("-l", "--local", action="store_true",
217 222 help="shortcut for --with-hg=<testdir>/../hg, "
218 223 "and --with-chg=<testdir>/../contrib/chg/chg if --chg is set")
219 224 parser.add_option("--loop", action="store_true",
220 225 help="loop tests repeatedly")
221 226 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
222 227 help="run each test N times (default=1)", default=1)
223 228 parser.add_option("-n", "--nodiff", action="store_true",
224 229 help="skip showing test changes")
225 230 parser.add_option("-p", "--port", type="int",
226 231 help="port on which servers should listen"
227 232 " (default: $%s or %d)" % defaults['port'])
228 233 parser.add_option("--compiler", type="string",
229 234 help="compiler to build with")
230 235 parser.add_option("--pure", action="store_true",
231 236 help="use pure Python code instead of C extensions")
232 237 parser.add_option("-R", "--restart", action="store_true",
233 238 help="restart at last error")
234 239 parser.add_option("-r", "--retest", action="store_true",
235 240 help="retest failed tests")
236 241 parser.add_option("-S", "--noskips", action="store_true",
237 242 help="don't report skip tests verbosely")
238 243 parser.add_option("--shell", type="string",
239 244 help="shell to use (default: $%s or %s)" % defaults['shell'])
240 245 parser.add_option("-t", "--timeout", type="int",
241 246 help="kill errant tests after TIMEOUT seconds"
242 247 " (default: $%s or %d)" % defaults['timeout'])
243 248 parser.add_option("--slowtimeout", type="int",
244 249 help="kill errant slow tests after SLOWTIMEOUT seconds"
245 250 " (default: $%s or %d)" % defaults['slowtimeout'])
246 251 parser.add_option("--time", action="store_true",
247 252 help="time how long each test takes")
248 253 parser.add_option("--json", action="store_true",
249 254 help="store test result data in 'report.json' file")
250 255 parser.add_option("--tmpdir", type="string",
251 256 help="run tests in the given temporary directory"
252 257 " (implies --keep-tmpdir)")
253 258 parser.add_option("-v", "--verbose", action="store_true",
254 259 help="output verbose messages")
255 260 parser.add_option("--xunit", type="string",
256 261 help="record xunit results at specified path")
257 262 parser.add_option("--view", type="string",
258 263 help="external diff viewer")
259 264 parser.add_option("--with-hg", type="string",
260 265 metavar="HG",
261 266 help="test using specified hg script rather than a "
262 267 "temporary installation")
263 268 parser.add_option("--chg", action="store_true",
264 269 help="install and use chg wrapper in place of hg")
265 270 parser.add_option("--with-chg", metavar="CHG",
266 271 help="use specified chg wrapper in place of hg")
267 272 parser.add_option("-3", "--py3k-warnings", action="store_true",
268 273 help="enable Py3k warnings on Python 2.6+")
269 274 # This option should be deleted once test-check-py3-compat.t and other
270 275 # Python 3 tests run with Python 3.
271 276 parser.add_option("--with-python3", metavar="PYTHON3",
272 277 help="Python 3 interpreter (if running under Python 2)"
273 278 " (TEMPORARY)")
274 279 parser.add_option('--extra-config-opt', action="append",
275 280 help='set the given config opt in the test hgrc')
276 281 parser.add_option('--random', action="store_true",
277 282 help='run tests in random order')
278 283 parser.add_option('--profile-runner', action='store_true',
279 284 help='run statprof on run-tests')
280 285 parser.add_option('--allow-slow-tests', action='store_true',
281 286 help='allow extremely slow tests')
282 287 parser.add_option('--showchannels', action='store_true',
283 288 help='show scheduling channels')
284 289 parser.add_option('--known-good-rev', type="string",
285 290 metavar="known_good_rev",
286 291 help=("Automatically bisect any failures using this "
287 292 "revision as a known-good revision."))
288 293
289 294 for option, (envvar, default) in defaults.items():
290 295 defaults[option] = type(default)(os.environ.get(envvar, default))
291 296 parser.set_defaults(**defaults)
292 297
293 298 return parser
294 299
295 300 def parseargs(args, parser):
296 301 """Parse arguments with our OptionParser and validate results."""
297 302 (options, args) = parser.parse_args(args)
298 303
299 304 # jython is always pure
300 305 if 'java' in sys.platform or '__pypy__' in sys.modules:
301 306 options.pure = True
302 307
303 308 if options.with_hg:
304 309 options.with_hg = canonpath(_bytespath(options.with_hg))
305 310 if not (os.path.isfile(options.with_hg) and
306 311 os.access(options.with_hg, os.X_OK)):
307 312 parser.error('--with-hg must specify an executable hg script')
308 313 if not os.path.basename(options.with_hg) == b'hg':
309 314 sys.stderr.write('warning: --with-hg should specify an hg script\n')
310 315 if options.local:
311 316 testdir = os.path.dirname(_bytespath(canonpath(sys.argv[0])))
312 317 reporootdir = os.path.dirname(testdir)
313 318 pathandattrs = [(b'hg', 'with_hg')]
314 319 if options.chg:
315 320 pathandattrs.append((b'contrib/chg/chg', 'with_chg'))
316 321 for relpath, attr in pathandattrs:
317 322 binpath = os.path.join(reporootdir, relpath)
318 323 if os.name != 'nt' and not os.access(binpath, os.X_OK):
319 324 parser.error('--local specified, but %r not found or '
320 325 'not executable' % binpath)
321 326 setattr(options, attr, binpath)
322 327
323 328 if (options.chg or options.with_chg) and os.name == 'nt':
324 329 parser.error('chg does not work on %s' % os.name)
325 330 if options.with_chg:
326 331 options.chg = False # no installation to temporary location
327 332 options.with_chg = canonpath(_bytespath(options.with_chg))
328 333 if not (os.path.isfile(options.with_chg) and
329 334 os.access(options.with_chg, os.X_OK)):
330 335 parser.error('--with-chg must specify a chg executable')
331 336 if options.chg and options.with_hg:
332 337 # chg shares installation location with hg
333 338 parser.error('--chg does not work when --with-hg is specified '
334 339 '(use --with-chg instead)')
335 340
336 341 options.anycoverage = options.cover or options.annotate or options.htmlcov
337 342 if options.anycoverage:
338 343 try:
339 344 import coverage
340 345 covver = version.StrictVersion(coverage.__version__).version
341 346 if covver < (3, 3):
342 347 parser.error('coverage options require coverage 3.3 or later')
343 348 except ImportError:
344 349 parser.error('coverage options now require the coverage package')
345 350
346 351 if options.anycoverage and options.local:
347 352 # this needs some path mangling somewhere, I guess
348 353 parser.error("sorry, coverage options do not work when --local "
349 354 "is specified")
350 355
351 356 if options.anycoverage and options.with_hg:
352 357 parser.error("sorry, coverage options do not work when --with-hg "
353 358 "is specified")
354 359
355 360 global verbose
356 361 if options.verbose:
357 362 verbose = ''
358 363
359 364 if options.tmpdir:
360 365 options.tmpdir = canonpath(options.tmpdir)
361 366
362 367 if options.jobs < 1:
363 368 parser.error('--jobs must be positive')
364 369 if options.interactive and options.debug:
365 370 parser.error("-i/--interactive and -d/--debug are incompatible")
366 371 if options.debug:
367 372 if options.timeout != defaults['timeout']:
368 373 sys.stderr.write(
369 374 'warning: --timeout option ignored with --debug\n')
370 375 if options.slowtimeout != defaults['slowtimeout']:
371 376 sys.stderr.write(
372 377 'warning: --slowtimeout option ignored with --debug\n')
373 378 options.timeout = 0
374 379 options.slowtimeout = 0
375 380 if options.py3k_warnings:
376 381 if PYTHON3:
377 382 parser.error(
378 383 '--py3k-warnings can only be used on Python 2.6 and 2.7')
379 384 if options.with_python3:
380 385 if PYTHON3:
381 386 parser.error('--with-python3 cannot be used when executing with '
382 387 'Python 3')
383 388
384 389 options.with_python3 = canonpath(options.with_python3)
385 390 # Verify Python3 executable is acceptable.
386 391 proc = subprocess.Popen([options.with_python3, b'--version'],
387 392 stdout=subprocess.PIPE,
388 393 stderr=subprocess.STDOUT)
389 394 out, _err = proc.communicate()
390 395 ret = proc.wait()
391 396 if ret != 0:
392 397 parser.error('could not determine version of python 3')
393 398 if not out.startswith('Python '):
394 399 parser.error('unexpected output from python3 --version: %s' %
395 400 out)
396 401 vers = version.LooseVersion(out[len('Python '):])
397 402 if vers < version.LooseVersion('3.5.0'):
398 403 parser.error('--with-python3 version must be 3.5.0 or greater; '
399 404 'got %s' % out)
400 405
401 406 if options.blacklist:
402 407 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
403 408 if options.whitelist:
404 409 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
405 410 else:
406 411 options.whitelisted = {}
407 412
408 413 if options.showchannels:
409 414 options.nodiff = True
410 415
411 416 return (options, args)
412 417
413 418 def rename(src, dst):
414 419 """Like os.rename(), trade atomicity and opened files friendliness
415 420 for existing destination support.
416 421 """
417 422 shutil.copy(src, dst)
418 423 os.remove(src)
419 424
420 425 _unified_diff = difflib.unified_diff
421 426 if PYTHON3:
422 427 import functools
423 428 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
424 429
425 430 def getdiff(expected, output, ref, err):
426 431 servefail = False
427 432 lines = []
428 433 for line in _unified_diff(expected, output, ref, err):
429 434 if line.startswith(b'+++') or line.startswith(b'---'):
430 435 line = line.replace(b'\\', b'/')
431 436 if line.endswith(b' \n'):
432 437 line = line[:-2] + b'\n'
433 438 lines.append(line)
434 439 if not servefail and line.startswith(
435 440 b'+ abort: child process failed to start'):
436 441 servefail = True
437 442
438 443 return servefail, lines
439 444
440 445 verbose = False
441 446 def vlog(*msg):
442 447 """Log only when in verbose mode."""
443 448 if verbose is False:
444 449 return
445 450
446 451 return log(*msg)
447 452
448 453 # Bytes that break XML even in a CDATA block: control characters 0-31
449 454 # sans \t, \n and \r
450 455 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
451 456
452 457 def cdatasafe(data):
453 458 """Make a string safe to include in a CDATA block.
454 459
455 460 Certain control characters are illegal in a CDATA block, and
456 461 there's no way to include a ]]> in a CDATA either. This function
457 462 replaces illegal bytes with ? and adds a space between the ]] so
458 463 that it won't break the CDATA block.
459 464 """
460 465 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
461 466
462 467 def log(*msg):
463 468 """Log something to stdout.
464 469
465 470 Arguments are strings to print.
466 471 """
467 472 with iolock:
468 473 if verbose:
469 474 print(verbose, end=' ')
470 475 for m in msg:
471 476 print(m, end=' ')
472 477 print()
473 478 sys.stdout.flush()
474 479
475 480 def terminate(proc):
476 481 """Terminate subprocess (with fallback for Python versions < 2.6)"""
477 482 vlog('# Terminating process %d' % proc.pid)
478 483 try:
479 484 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
480 485 except OSError:
481 486 pass
482 487
483 488 def killdaemons(pidfile):
484 489 import killdaemons as killmod
485 490 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
486 491 logfn=vlog)
487 492
488 493 class Test(unittest.TestCase):
489 494 """Encapsulates a single, runnable test.
490 495
491 496 While this class conforms to the unittest.TestCase API, it differs in that
492 497 instances need to be instantiated manually. (Typically, unittest.TestCase
493 498 classes are instantiated automatically by scanning modules.)
494 499 """
495 500
496 501 # Status code reserved for skipped tests (used by hghave).
497 502 SKIPPED_STATUS = 80
498 503
499 504 def __init__(self, path, tmpdir, keeptmpdir=False,
500 505 debug=False,
501 506 timeout=defaults['timeout'],
502 507 startport=defaults['port'], extraconfigopts=None,
503 508 py3kwarnings=False, shell=None, hgcommand=None,
504 509 slowtimeout=defaults['slowtimeout'], usechg=False):
505 510 """Create a test from parameters.
506 511
507 512 path is the full path to the file defining the test.
508 513
509 514 tmpdir is the main temporary directory to use for this test.
510 515
511 516 keeptmpdir determines whether to keep the test's temporary directory
512 517 after execution. It defaults to removal (False).
513 518
514 519 debug mode will make the test execute verbosely, with unfiltered
515 520 output.
516 521
517 522 timeout controls the maximum run time of the test. It is ignored when
518 523 debug is True. See slowtimeout for tests with #require slow.
519 524
520 525 slowtimeout overrides timeout if the test has #require slow.
521 526
522 527 startport controls the starting port number to use for this test. Each
523 528 test will reserve 3 port numbers for execution. It is the caller's
524 529 responsibility to allocate a non-overlapping port range to Test
525 530 instances.
526 531
527 532 extraconfigopts is an iterable of extra hgrc config options. Values
528 533 must have the form "key=value" (something understood by hgrc). Values
529 534 of the form "foo.key=value" will result in "[foo] key=value".
530 535
531 536 py3kwarnings enables Py3k warnings.
532 537
533 538 shell is the shell to execute tests in.
534 539 """
535 540 self.path = path
536 541 self.bname = os.path.basename(path)
537 542 self.name = _strpath(self.bname)
538 543 self._testdir = os.path.dirname(path)
539 544 self.errpath = os.path.join(self._testdir, b'%s.err' % self.bname)
540 545
541 546 self._threadtmp = tmpdir
542 547 self._keeptmpdir = keeptmpdir
543 548 self._debug = debug
544 549 self._timeout = timeout
545 550 self._slowtimeout = slowtimeout
546 551 self._startport = startport
547 552 self._extraconfigopts = extraconfigopts or []
548 553 self._py3kwarnings = py3kwarnings
549 554 self._shell = _bytespath(shell)
550 555 self._hgcommand = hgcommand or b'hg'
551 556 self._usechg = usechg
552 557
553 558 self._aborted = False
554 559 self._daemonpids = []
555 560 self._finished = None
556 561 self._ret = None
557 562 self._out = None
558 563 self._skipped = None
559 564 self._testtmp = None
560 565 self._chgsockdir = None
561 566
562 567 # If we're not in --debug mode and reference output file exists,
563 568 # check test output against it.
564 569 if debug:
565 570 self._refout = None # to match "out is None"
566 571 elif os.path.exists(self.refpath):
567 572 f = open(self.refpath, 'rb')
568 573 self._refout = f.read().splitlines(True)
569 574 f.close()
570 575 else:
571 576 self._refout = []
572 577
573 578 # needed to get base class __repr__ running
574 579 @property
575 580 def _testMethodName(self):
576 581 return self.name
577 582
578 583 def __str__(self):
579 584 return self.name
580 585
581 586 def shortDescription(self):
582 587 return self.name
583 588
584 589 def setUp(self):
585 590 """Tasks to perform before run()."""
586 591 self._finished = False
587 592 self._ret = None
588 593 self._out = None
589 594 self._skipped = None
590 595
591 596 try:
592 597 os.mkdir(self._threadtmp)
593 598 except OSError as e:
594 599 if e.errno != errno.EEXIST:
595 600 raise
596 601
597 602 name = os.path.basename(self.path)
598 603 self._testtmp = os.path.join(self._threadtmp, name)
599 604 os.mkdir(self._testtmp)
600 605
601 606 # Remove any previous output files.
602 607 if os.path.exists(self.errpath):
603 608 try:
604 609 os.remove(self.errpath)
605 610 except OSError as e:
606 611 # We might have raced another test to clean up a .err
607 612 # file, so ignore ENOENT when removing a previous .err
608 613 # file.
609 614 if e.errno != errno.ENOENT:
610 615 raise
611 616
612 617 if self._usechg:
613 618 self._chgsockdir = os.path.join(self._threadtmp,
614 619 b'%s.chgsock' % name)
615 620 os.mkdir(self._chgsockdir)
616 621
617 622 def run(self, result):
618 623 """Run this test and report results against a TestResult instance."""
619 624 # This function is extremely similar to unittest.TestCase.run(). Once
620 625 # we require Python 2.7 (or at least its version of unittest), this
621 626 # function can largely go away.
622 627 self._result = result
623 628 result.startTest(self)
624 629 try:
625 630 try:
626 631 self.setUp()
627 632 except (KeyboardInterrupt, SystemExit):
628 633 self._aborted = True
629 634 raise
630 635 except Exception:
631 636 result.addError(self, sys.exc_info())
632 637 return
633 638
634 639 success = False
635 640 try:
636 641 self.runTest()
637 642 except KeyboardInterrupt:
638 643 self._aborted = True
639 644 raise
640 645 except SkipTest as e:
641 646 result.addSkip(self, str(e))
642 647 # The base class will have already counted this as a
643 648 # test we "ran", but we want to exclude skipped tests
644 649 # from those we count towards those run.
645 650 result.testsRun -= 1
646 651 except IgnoreTest as e:
647 652 result.addIgnore(self, str(e))
648 653 # As with skips, ignores also should be excluded from
649 654 # the number of tests executed.
650 655 result.testsRun -= 1
651 656 except WarnTest as e:
652 657 result.addWarn(self, str(e))
653 658 except ReportedTest as e:
654 659 pass
655 660 except self.failureException as e:
656 661 # This differs from unittest in that we don't capture
657 662 # the stack trace. This is for historical reasons and
658 663 # this decision could be revisited in the future,
659 664 # especially for PythonTest instances.
660 665 if result.addFailure(self, str(e)):
661 666 success = True
662 667 except Exception:
663 668 result.addError(self, sys.exc_info())
664 669 else:
665 670 success = True
666 671
667 672 try:
668 673 self.tearDown()
669 674 except (KeyboardInterrupt, SystemExit):
670 675 self._aborted = True
671 676 raise
672 677 except Exception:
673 678 result.addError(self, sys.exc_info())
674 679 success = False
675 680
676 681 if success:
677 682 result.addSuccess(self)
678 683 finally:
679 684 result.stopTest(self, interrupted=self._aborted)
680 685
681 686 def runTest(self):
682 687 """Run this test instance.
683 688
684 689 This will return a tuple describing the result of the test.
685 690 """
686 691 env = self._getenv()
687 692 self._daemonpids.append(env['DAEMON_PIDS'])
688 693 self._createhgrc(env['HGRCPATH'])
689 694
690 695 vlog('# Test', self.name)
691 696
692 697 ret, out = self._run(env)
693 698 self._finished = True
694 699 self._ret = ret
695 700 self._out = out
696 701
697 702 def describe(ret):
698 703 if ret < 0:
699 704 return 'killed by signal: %d' % -ret
700 705 return 'returned error code %d' % ret
701 706
702 707 self._skipped = False
703 708
704 709 if ret == self.SKIPPED_STATUS:
705 710 if out is None: # Debug mode, nothing to parse.
706 711 missing = ['unknown']
707 712 failed = None
708 713 else:
709 714 missing, failed = TTest.parsehghaveoutput(out)
710 715
711 716 if not missing:
712 717 missing = ['skipped']
713 718
714 719 if failed:
715 720 self.fail('hg have failed checking for %s' % failed[-1])
716 721 else:
717 722 self._skipped = True
718 723 raise SkipTest(missing[-1])
719 724 elif ret == 'timeout':
720 725 self.fail('timed out')
721 726 elif ret is False:
722 727 raise WarnTest('no result code from test')
723 728 elif out != self._refout:
724 729 # Diff generation may rely on written .err file.
725 730 if (ret != 0 or out != self._refout) and not self._skipped \
726 731 and not self._debug:
727 732 f = open(self.errpath, 'wb')
728 733 for line in out:
729 734 f.write(line)
730 735 f.close()
731 736
732 737 # The result object handles diff calculation for us.
733 738 if self._result.addOutputMismatch(self, ret, out, self._refout):
734 739 # change was accepted, skip failing
735 740 return
736 741
737 742 if ret:
738 743 msg = 'output changed and ' + describe(ret)
739 744 else:
740 745 msg = 'output changed'
741 746
742 747 self.fail(msg)
743 748 elif ret:
744 749 self.fail(describe(ret))
745 750
746 751 def tearDown(self):
747 752 """Tasks to perform after run()."""
748 753 for entry in self._daemonpids:
749 754 killdaemons(entry)
750 755 self._daemonpids = []
751 756
752 757 if self._keeptmpdir:
753 758 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
754 759 (self._testtmp.decode('utf-8'),
755 760 self._threadtmp.decode('utf-8')))
756 761 else:
757 762 shutil.rmtree(self._testtmp, True)
758 763 shutil.rmtree(self._threadtmp, True)
759 764
760 765 if self._usechg:
761 766 # chgservers will stop automatically after they find the socket
762 767 # files are deleted
763 768 shutil.rmtree(self._chgsockdir, True)
764 769
765 770 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
766 771 and not self._debug and self._out:
767 772 f = open(self.errpath, 'wb')
768 773 for line in self._out:
769 774 f.write(line)
770 775 f.close()
771 776
772 777 vlog("# Ret was:", self._ret, '(%s)' % self.name)
773 778
774 779 def _run(self, env):
775 780 # This should be implemented in child classes to run tests.
776 781 raise SkipTest('unknown test type')
777 782
778 783 def abort(self):
779 784 """Terminate execution of this test."""
780 785 self._aborted = True
781 786
782 787 def _portmap(self, i):
783 788 offset = b'' if i == 0 else b'%d' % i
784 789 return (br':%d\b' % (self._startport + i), b':$HGPORT%s' % offset)
785 790
786 791 def _getreplacements(self):
787 792 """Obtain a mapping of text replacements to apply to test output.
788 793
789 794 Test output needs to be normalized so it can be compared to expected
790 795 output. This function defines how some of that normalization will
791 796 occur.
792 797 """
793 798 r = [
794 799 # This list should be parallel to defineport in _getenv
795 800 self._portmap(0),
796 801 self._portmap(1),
797 802 self._portmap(2),
798 803 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
799 804 br'\1 (glob)'),
800 805 ]
801 806 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
802 807
803 808 return r
804 809
805 810 def _escapepath(self, p):
806 811 if os.name == 'nt':
807 812 return (
808 813 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
809 814 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
810 815 for c in p))
811 816 )
812 817 else:
813 818 return re.escape(p)
814 819
815 820 def _getenv(self):
816 821 """Obtain environment variables to use during test execution."""
817 822 def defineport(i):
818 823 offset = '' if i == 0 else '%s' % i
819 824 env["HGPORT%s" % offset] = '%s' % (self._startport + i)
820 825 env = os.environ.copy()
826 if sysconfig is not None:
827 env['PYTHONUSERBASE'] = sysconfig.get_config_var('userbase')
821 828 env['TESTTMP'] = self._testtmp
822 829 env['HOME'] = self._testtmp
823 830 # This number should match portneeded in _getport
824 831 for port in xrange(3):
825 832 # This list should be parallel to _portmap in _getreplacements
826 833 defineport(port)
827 834 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
828 835 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
829 836 env["HGEDITOR"] = ('"' + sys.executable + '"'
830 837 + ' -c "import sys; sys.exit(0)"')
831 838 env["HGMERGE"] = "internal:merge"
832 839 env["HGUSER"] = "test"
833 840 env["HGENCODING"] = "ascii"
834 841 env["HGENCODINGMODE"] = "strict"
835 842
836 843 # Reset some environment variables to well-known values so that
837 844 # the tests produce repeatable output.
838 845 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
839 846 env['TZ'] = 'GMT'
840 847 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
841 848 env['COLUMNS'] = '80'
842 849 env['TERM'] = 'xterm'
843 850
844 851 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
845 852 'NO_PROXY').split():
846 853 if k in env:
847 854 del env[k]
848 855
849 856 # unset env related to hooks
850 857 for k in env.keys():
851 858 if k.startswith('HG_'):
852 859 del env[k]
853 860
854 861 if self._usechg:
855 862 env['CHGSOCKNAME'] = os.path.join(self._chgsockdir, b'server')
856 863
857 864 return env
858 865
859 866 def _createhgrc(self, path):
860 867 """Create an hgrc file for this test."""
861 868 hgrc = open(path, 'wb')
862 869 hgrc.write(b'[ui]\n')
863 870 hgrc.write(b'slash = True\n')
864 871 hgrc.write(b'interactive = False\n')
865 872 hgrc.write(b'mergemarkers = detailed\n')
866 873 hgrc.write(b'promptecho = True\n')
867 874 hgrc.write(b'[defaults]\n')
868 875 hgrc.write(b'backout = -d "0 0"\n')
869 876 hgrc.write(b'commit = -d "0 0"\n')
870 877 hgrc.write(b'shelve = --date "0 0"\n')
871 878 hgrc.write(b'tag = -d "0 0"\n')
872 879 hgrc.write(b'[devel]\n')
873 880 hgrc.write(b'all-warnings = true\n')
874 881 hgrc.write(b'[largefiles]\n')
875 882 hgrc.write(b'usercache = %s\n' %
876 883 (os.path.join(self._testtmp, b'.cache/largefiles')))
877 884
878 885 for opt in self._extraconfigopts:
879 886 section, key = opt.split('.', 1)
880 887 assert '=' in key, ('extra config opt %s must '
881 888 'have an = for assignment' % opt)
882 889 hgrc.write(b'[%s]\n%s\n' % (section, key))
883 890 hgrc.close()
884 891
885 892 def fail(self, msg):
886 893 # unittest differentiates between errored and failed.
887 894 # Failed is denoted by AssertionError (by default at least).
888 895 raise AssertionError(msg)
889 896
890 897 def _runcommand(self, cmd, env, normalizenewlines=False):
891 898 """Run command in a sub-process, capturing the output (stdout and
892 899 stderr).
893 900
894 901 Return a tuple (exitcode, output). output is None in debug mode.
895 902 """
896 903 if self._debug:
897 904 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
898 905 env=env)
899 906 ret = proc.wait()
900 907 return (ret, None)
901 908
902 909 proc = Popen4(cmd, self._testtmp, self._timeout, env)
903 910 def cleanup():
904 911 terminate(proc)
905 912 ret = proc.wait()
906 913 if ret == 0:
907 914 ret = signal.SIGTERM << 8
908 915 killdaemons(env['DAEMON_PIDS'])
909 916 return ret
910 917
911 918 output = ''
912 919 proc.tochild.close()
913 920
914 921 try:
915 922 output = proc.fromchild.read()
916 923 except KeyboardInterrupt:
917 924 vlog('# Handling keyboard interrupt')
918 925 cleanup()
919 926 raise
920 927
921 928 ret = proc.wait()
922 929 if wifexited(ret):
923 930 ret = os.WEXITSTATUS(ret)
924 931
925 932 if proc.timeout:
926 933 ret = 'timeout'
927 934
928 935 if ret:
929 936 killdaemons(env['DAEMON_PIDS'])
930 937
931 938 for s, r in self._getreplacements():
932 939 output = re.sub(s, r, output)
933 940
934 941 if normalizenewlines:
935 942 output = output.replace('\r\n', '\n')
936 943
937 944 return ret, output.splitlines(True)
938 945
939 946 class PythonTest(Test):
940 947 """A Python-based test."""
941 948
942 949 @property
943 950 def refpath(self):
944 951 return os.path.join(self._testdir, b'%s.out' % self.bname)
945 952
946 953 def _run(self, env):
947 954 py3kswitch = self._py3kwarnings and b' -3' or b''
948 955 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
949 956 vlog("# Running", cmd)
950 957 normalizenewlines = os.name == 'nt'
951 958 result = self._runcommand(cmd, env,
952 959 normalizenewlines=normalizenewlines)
953 960 if self._aborted:
954 961 raise KeyboardInterrupt()
955 962
956 963 return result
957 964
958 965 # Some glob patterns apply only in some circumstances, so the script
959 966 # might want to remove (glob) annotations that otherwise should be
960 967 # retained.
961 968 checkcodeglobpats = [
962 969 # On Windows it looks like \ doesn't require a (glob), but we know
963 970 # better.
964 971 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
965 972 re.compile(br'^moving \S+/.*[^)]$'),
966 973 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
967 974 # Not all platforms have 127.0.0.1 as loopback (though most do),
968 975 # so we always glob that too.
969 976 re.compile(br'.*127.0.0.1.*$'),
970 977 ]
971 978
972 979 bchr = chr
973 980 if PYTHON3:
974 981 bchr = lambda x: bytes([x])
975 982
976 983 class TTest(Test):
977 984 """A "t test" is a test backed by a .t file."""
978 985
979 986 SKIPPED_PREFIX = b'skipped: '
980 987 FAILED_PREFIX = b'hghave check failed: '
981 988 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
982 989
983 990 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
984 991 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
985 992 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
986 993
987 994 @property
988 995 def refpath(self):
989 996 return os.path.join(self._testdir, self.bname)
990 997
991 998 def _run(self, env):
992 999 f = open(self.path, 'rb')
993 1000 lines = f.readlines()
994 1001 f.close()
995 1002
996 1003 salt, script, after, expected = self._parsetest(lines)
997 1004
998 1005 # Write out the generated script.
999 1006 fname = b'%s.sh' % self._testtmp
1000 1007 f = open(fname, 'wb')
1001 1008 for l in script:
1002 1009 f.write(l)
1003 1010 f.close()
1004 1011
1005 1012 cmd = b'%s "%s"' % (self._shell, fname)
1006 1013 vlog("# Running", cmd)
1007 1014
1008 1015 exitcode, output = self._runcommand(cmd, env)
1009 1016
1010 1017 if self._aborted:
1011 1018 raise KeyboardInterrupt()
1012 1019
1013 1020 # Do not merge output if skipped. Return hghave message instead.
1014 1021 # Similarly, with --debug, output is None.
1015 1022 if exitcode == self.SKIPPED_STATUS or output is None:
1016 1023 return exitcode, output
1017 1024
1018 1025 return self._processoutput(exitcode, output, salt, after, expected)
1019 1026
1020 1027 def _hghave(self, reqs):
1021 1028 # TODO do something smarter when all other uses of hghave are gone.
1022 1029 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1023 1030 tdir = runtestdir.replace(b'\\', b'/')
1024 1031 proc = Popen4(b'%s -c "%s/hghave %s"' %
1025 1032 (self._shell, tdir, b' '.join(reqs)),
1026 1033 self._testtmp, 0, self._getenv())
1027 1034 stdout, stderr = proc.communicate()
1028 1035 ret = proc.wait()
1029 1036 if wifexited(ret):
1030 1037 ret = os.WEXITSTATUS(ret)
1031 1038 if ret == 2:
1032 1039 print(stdout.decode('utf-8'))
1033 1040 sys.exit(1)
1034 1041
1035 1042 if ret != 0:
1036 1043 return False, stdout
1037 1044
1038 1045 if 'slow' in reqs:
1039 1046 self._timeout = self._slowtimeout
1040 1047 return True, None
1041 1048
1042 1049 def _parsetest(self, lines):
1043 1050 # We generate a shell script which outputs unique markers to line
1044 1051 # up script results with our source. These markers include input
1045 1052 # line number and the last return code.
1046 1053 salt = b"SALT%d" % time.time()
1047 1054 def addsalt(line, inpython):
1048 1055 if inpython:
1049 1056 script.append(b'%s %d 0\n' % (salt, line))
1050 1057 else:
1051 1058 script.append(b'echo %s %d $?\n' % (salt, line))
1052 1059
1053 1060 script = []
1054 1061
1055 1062 # After we run the shell script, we re-unify the script output
1056 1063 # with non-active parts of the source, with synchronization by our
1057 1064 # SALT line number markers. The after table contains the non-active
1058 1065 # components, ordered by line number.
1059 1066 after = {}
1060 1067
1061 1068 # Expected shell script output.
1062 1069 expected = {}
1063 1070
1064 1071 pos = prepos = -1
1065 1072
1066 1073 # True or False when in a true or false conditional section
1067 1074 skipping = None
1068 1075
1069 1076 # We keep track of whether or not we're in a Python block so we
1070 1077 # can generate the surrounding doctest magic.
1071 1078 inpython = False
1072 1079
1073 1080 if self._debug:
1074 1081 script.append(b'set -x\n')
1075 1082 if self._hgcommand != b'hg':
1076 1083 script.append(b'alias hg="%s"\n' % self._hgcommand)
1077 1084 if os.getenv('MSYSTEM'):
1078 1085 script.append(b'alias pwd="pwd -W"\n')
1079 1086
1080 1087 n = 0
1081 1088 for n, l in enumerate(lines):
1082 1089 if not l.endswith(b'\n'):
1083 1090 l += b'\n'
1084 1091 if l.startswith(b'#require'):
1085 1092 lsplit = l.split()
1086 1093 if len(lsplit) < 2 or lsplit[0] != b'#require':
1087 1094 after.setdefault(pos, []).append(' !!! invalid #require\n')
1088 1095 haveresult, message = self._hghave(lsplit[1:])
1089 1096 if not haveresult:
1090 1097 script = [b'echo "%s"\nexit 80\n' % message]
1091 1098 break
1092 1099 after.setdefault(pos, []).append(l)
1093 1100 elif l.startswith(b'#if'):
1094 1101 lsplit = l.split()
1095 1102 if len(lsplit) < 2 or lsplit[0] != b'#if':
1096 1103 after.setdefault(pos, []).append(' !!! invalid #if\n')
1097 1104 if skipping is not None:
1098 1105 after.setdefault(pos, []).append(' !!! nested #if\n')
1099 1106 skipping = not self._hghave(lsplit[1:])[0]
1100 1107 after.setdefault(pos, []).append(l)
1101 1108 elif l.startswith(b'#else'):
1102 1109 if skipping is None:
1103 1110 after.setdefault(pos, []).append(' !!! missing #if\n')
1104 1111 skipping = not skipping
1105 1112 after.setdefault(pos, []).append(l)
1106 1113 elif l.startswith(b'#endif'):
1107 1114 if skipping is None:
1108 1115 after.setdefault(pos, []).append(' !!! missing #if\n')
1109 1116 skipping = None
1110 1117 after.setdefault(pos, []).append(l)
1111 1118 elif skipping:
1112 1119 after.setdefault(pos, []).append(l)
1113 1120 elif l.startswith(b' >>> '): # python inlines
1114 1121 after.setdefault(pos, []).append(l)
1115 1122 prepos = pos
1116 1123 pos = n
1117 1124 if not inpython:
1118 1125 # We've just entered a Python block. Add the header.
1119 1126 inpython = True
1120 1127 addsalt(prepos, False) # Make sure we report the exit code.
1121 1128 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1122 1129 addsalt(n, True)
1123 1130 script.append(l[2:])
1124 1131 elif l.startswith(b' ... '): # python inlines
1125 1132 after.setdefault(prepos, []).append(l)
1126 1133 script.append(l[2:])
1127 1134 elif l.startswith(b' $ '): # commands
1128 1135 if inpython:
1129 1136 script.append(b'EOF\n')
1130 1137 inpython = False
1131 1138 after.setdefault(pos, []).append(l)
1132 1139 prepos = pos
1133 1140 pos = n
1134 1141 addsalt(n, False)
1135 1142 cmd = l[4:].split()
1136 1143 if len(cmd) == 2 and cmd[0] == b'cd':
1137 1144 l = b' $ cd %s || exit 1\n' % cmd[1]
1138 1145 script.append(l[4:])
1139 1146 elif l.startswith(b' > '): # continuations
1140 1147 after.setdefault(prepos, []).append(l)
1141 1148 script.append(l[4:])
1142 1149 elif l.startswith(b' '): # results
1143 1150 # Queue up a list of expected results.
1144 1151 expected.setdefault(pos, []).append(l[2:])
1145 1152 else:
1146 1153 if inpython:
1147 1154 script.append(b'EOF\n')
1148 1155 inpython = False
1149 1156 # Non-command/result. Queue up for merged output.
1150 1157 after.setdefault(pos, []).append(l)
1151 1158
1152 1159 if inpython:
1153 1160 script.append(b'EOF\n')
1154 1161 if skipping is not None:
1155 1162 after.setdefault(pos, []).append(' !!! missing #endif\n')
1156 1163 addsalt(n + 1, False)
1157 1164
1158 1165 return salt, script, after, expected
1159 1166
1160 1167 def _processoutput(self, exitcode, output, salt, after, expected):
1161 1168 # Merge the script output back into a unified test.
1162 1169 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1163 1170 if exitcode != 0:
1164 1171 warnonly = 3
1165 1172
1166 1173 pos = -1
1167 1174 postout = []
1168 1175 for l in output:
1169 1176 lout, lcmd = l, None
1170 1177 if salt in l:
1171 1178 lout, lcmd = l.split(salt, 1)
1172 1179
1173 1180 while lout:
1174 1181 if not lout.endswith(b'\n'):
1175 1182 lout += b' (no-eol)\n'
1176 1183
1177 1184 # Find the expected output at the current position.
1178 1185 els = [None]
1179 1186 if expected.get(pos, None):
1180 1187 els = expected[pos]
1181 1188
1182 1189 i = 0
1183 1190 optional = []
1184 1191 while i < len(els):
1185 1192 el = els[i]
1186 1193
1187 1194 r = TTest.linematch(el, lout)
1188 1195 if isinstance(r, str):
1189 1196 if r == '+glob':
1190 1197 lout = el[:-1] + ' (glob)\n'
1191 1198 r = '' # Warn only this line.
1192 1199 elif r == '-glob':
1193 1200 lout = ''.join(el.rsplit(' (glob)', 1))
1194 1201 r = '' # Warn only this line.
1195 1202 elif r == "retry":
1196 1203 postout.append(b' ' + el)
1197 1204 els.pop(i)
1198 1205 break
1199 1206 else:
1200 1207 log('\ninfo, unknown linematch result: %r\n' % r)
1201 1208 r = False
1202 1209 if r:
1203 1210 els.pop(i)
1204 1211 break
1205 1212 if el and el.endswith(b" (?)\n"):
1206 1213 optional.append(i)
1207 1214 i += 1
1208 1215
1209 1216 if r:
1210 1217 if r == "retry":
1211 1218 continue
1212 1219 # clean up any optional leftovers
1213 1220 for i in optional:
1214 1221 postout.append(b' ' + els[i])
1215 1222 for i in reversed(optional):
1216 1223 del els[i]
1217 1224 postout.append(b' ' + el)
1218 1225 else:
1219 1226 if self.NEEDESCAPE(lout):
1220 1227 lout = TTest._stringescape(b'%s (esc)\n' %
1221 1228 lout.rstrip(b'\n'))
1222 1229 postout.append(b' ' + lout) # Let diff deal with it.
1223 1230 if r != '': # If line failed.
1224 1231 warnonly = 3 # for sure not
1225 1232 elif warnonly == 1: # Is "not yet" and line is warn only.
1226 1233 warnonly = 2 # Yes do warn.
1227 1234 break
1228 1235 else:
1229 1236 # clean up any optional leftovers
1230 1237 while expected.get(pos, None):
1231 1238 el = expected[pos].pop(0)
1232 1239 if el and not el.endswith(b" (?)\n"):
1233 1240 break
1234 1241 postout.append(b' ' + el)
1235 1242
1236 1243 if lcmd:
1237 1244 # Add on last return code.
1238 1245 ret = int(lcmd.split()[1])
1239 1246 if ret != 0:
1240 1247 postout.append(b' [%d]\n' % ret)
1241 1248 if pos in after:
1242 1249 # Merge in non-active test bits.
1243 1250 postout += after.pop(pos)
1244 1251 pos = int(lcmd.split()[0])
1245 1252
1246 1253 if pos in after:
1247 1254 postout += after.pop(pos)
1248 1255
1249 1256 if warnonly == 2:
1250 1257 exitcode = False # Set exitcode to warned.
1251 1258
1252 1259 return exitcode, postout
1253 1260
1254 1261 @staticmethod
1255 1262 def rematch(el, l):
1256 1263 try:
1257 1264 # use \Z to ensure that the regex matches to the end of the string
1258 1265 if os.name == 'nt':
1259 1266 return re.match(el + br'\r?\n\Z', l)
1260 1267 return re.match(el + br'\n\Z', l)
1261 1268 except re.error:
1262 1269 # el is an invalid regex
1263 1270 return False
1264 1271
1265 1272 @staticmethod
1266 1273 def globmatch(el, l):
1267 1274 # The only supported special characters are * and ? plus / which also
1268 1275 # matches \ on windows. Escaping of these characters is supported.
1269 1276 if el + b'\n' == l:
1270 1277 if os.altsep:
1271 1278 # matching on "/" is not needed for this line
1272 1279 for pat in checkcodeglobpats:
1273 1280 if pat.match(el):
1274 1281 return True
1275 1282 return b'-glob'
1276 1283 return True
1277 1284 el = el.replace(b'127.0.0.1', b'*')
1278 1285 i, n = 0, len(el)
1279 1286 res = b''
1280 1287 while i < n:
1281 1288 c = el[i:i + 1]
1282 1289 i += 1
1283 1290 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1284 1291 res += el[i - 1:i + 1]
1285 1292 i += 1
1286 1293 elif c == b'*':
1287 1294 res += b'.*'
1288 1295 elif c == b'?':
1289 1296 res += b'.'
1290 1297 elif c == b'/' and os.altsep:
1291 1298 res += b'[/\\\\]'
1292 1299 else:
1293 1300 res += re.escape(c)
1294 1301 return TTest.rematch(res, l)
1295 1302
1296 1303 @staticmethod
1297 1304 def linematch(el, l):
1298 1305 retry = False
1299 1306 if el == l: # perfect match (fast)
1300 1307 return True
1301 1308 if el:
1302 1309 if el.endswith(b" (?)\n"):
1303 1310 retry = "retry"
1304 1311 el = el[:-5] + b"\n"
1305 1312 if el.endswith(b" (esc)\n"):
1306 1313 if PYTHON3:
1307 1314 el = el[:-7].decode('unicode_escape') + '\n'
1308 1315 el = el.encode('utf-8')
1309 1316 else:
1310 1317 el = el[:-7].decode('string-escape') + '\n'
1311 1318 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1312 1319 return True
1313 1320 if el.endswith(b" (re)\n"):
1314 1321 return TTest.rematch(el[:-6], l) or retry
1315 1322 if el.endswith(b" (glob)\n"):
1316 1323 # ignore '(glob)' added to l by 'replacements'
1317 1324 if l.endswith(b" (glob)\n"):
1318 1325 l = l[:-8] + b"\n"
1319 1326 return TTest.globmatch(el[:-8], l)
1320 1327 if os.altsep and l.replace(b'\\', b'/') == el:
1321 1328 return b'+glob'
1322 1329 return retry
1323 1330
1324 1331 @staticmethod
1325 1332 def parsehghaveoutput(lines):
1326 1333 '''Parse hghave log lines.
1327 1334
1328 1335 Return tuple of lists (missing, failed):
1329 1336 * the missing/unknown features
1330 1337 * the features for which existence check failed'''
1331 1338 missing = []
1332 1339 failed = []
1333 1340 for line in lines:
1334 1341 if line.startswith(TTest.SKIPPED_PREFIX):
1335 1342 line = line.splitlines()[0]
1336 1343 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1337 1344 elif line.startswith(TTest.FAILED_PREFIX):
1338 1345 line = line.splitlines()[0]
1339 1346 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1340 1347
1341 1348 return missing, failed
1342 1349
1343 1350 @staticmethod
1344 1351 def _escapef(m):
1345 1352 return TTest.ESCAPEMAP[m.group(0)]
1346 1353
1347 1354 @staticmethod
1348 1355 def _stringescape(s):
1349 1356 return TTest.ESCAPESUB(TTest._escapef, s)
1350 1357
1351 1358 iolock = threading.RLock()
1352 1359
1353 1360 class SkipTest(Exception):
1354 1361 """Raised to indicate that a test is to be skipped."""
1355 1362
1356 1363 class IgnoreTest(Exception):
1357 1364 """Raised to indicate that a test is to be ignored."""
1358 1365
1359 1366 class WarnTest(Exception):
1360 1367 """Raised to indicate that a test warned."""
1361 1368
1362 1369 class ReportedTest(Exception):
1363 1370 """Raised to indicate that a test already reported."""
1364 1371
1365 1372 class TestResult(unittest._TextTestResult):
1366 1373 """Holds results when executing via unittest."""
1367 1374 # Don't worry too much about accessing the non-public _TextTestResult.
1368 1375 # It is relatively common in Python testing tools.
1369 1376 def __init__(self, options, *args, **kwargs):
1370 1377 super(TestResult, self).__init__(*args, **kwargs)
1371 1378
1372 1379 self._options = options
1373 1380
1374 1381 # unittest.TestResult didn't have skipped until 2.7. We need to
1375 1382 # polyfill it.
1376 1383 self.skipped = []
1377 1384
1378 1385 # We have a custom "ignored" result that isn't present in any Python
1379 1386 # unittest implementation. It is very similar to skipped. It may make
1380 1387 # sense to map it into skip some day.
1381 1388 self.ignored = []
1382 1389
1383 1390 # We have a custom "warned" result that isn't present in any Python
1384 1391 # unittest implementation. It is very similar to failed. It may make
1385 1392 # sense to map it into fail some day.
1386 1393 self.warned = []
1387 1394
1388 1395 self.times = []
1389 1396 self._firststarttime = None
1390 1397 # Data stored for the benefit of generating xunit reports.
1391 1398 self.successes = []
1392 1399 self.faildata = {}
1393 1400
1394 1401 def addFailure(self, test, reason):
1395 1402 self.failures.append((test, reason))
1396 1403
1397 1404 if self._options.first:
1398 1405 self.stop()
1399 1406 else:
1400 1407 with iolock:
1401 1408 if reason == "timed out":
1402 1409 self.stream.write('t')
1403 1410 else:
1404 1411 if not self._options.nodiff:
1405 1412 self.stream.write('\nERROR: %s output changed\n' % test)
1406 1413 self.stream.write('!')
1407 1414
1408 1415 self.stream.flush()
1409 1416
1410 1417 def addSuccess(self, test):
1411 1418 with iolock:
1412 1419 super(TestResult, self).addSuccess(test)
1413 1420 self.successes.append(test)
1414 1421
1415 1422 def addError(self, test, err):
1416 1423 super(TestResult, self).addError(test, err)
1417 1424 if self._options.first:
1418 1425 self.stop()
1419 1426
1420 1427 # Polyfill.
1421 1428 def addSkip(self, test, reason):
1422 1429 self.skipped.append((test, reason))
1423 1430 with iolock:
1424 1431 if self.showAll:
1425 1432 self.stream.writeln('skipped %s' % reason)
1426 1433 else:
1427 1434 self.stream.write('s')
1428 1435 self.stream.flush()
1429 1436
1430 1437 def addIgnore(self, test, reason):
1431 1438 self.ignored.append((test, reason))
1432 1439 with iolock:
1433 1440 if self.showAll:
1434 1441 self.stream.writeln('ignored %s' % reason)
1435 1442 else:
1436 1443 if reason not in ('not retesting', "doesn't match keyword"):
1437 1444 self.stream.write('i')
1438 1445 else:
1439 1446 self.testsRun += 1
1440 1447 self.stream.flush()
1441 1448
1442 1449 def addWarn(self, test, reason):
1443 1450 self.warned.append((test, reason))
1444 1451
1445 1452 if self._options.first:
1446 1453 self.stop()
1447 1454
1448 1455 with iolock:
1449 1456 if self.showAll:
1450 1457 self.stream.writeln('warned %s' % reason)
1451 1458 else:
1452 1459 self.stream.write('~')
1453 1460 self.stream.flush()
1454 1461
1455 1462 def addOutputMismatch(self, test, ret, got, expected):
1456 1463 """Record a mismatch in test output for a particular test."""
1457 1464 if self.shouldStop:
1458 1465 # don't print, some other test case already failed and
1459 1466 # printed, we're just stale and probably failed due to our
1460 1467 # temp dir getting cleaned up.
1461 1468 return
1462 1469
1463 1470 accepted = False
1464 1471 lines = []
1465 1472
1466 1473 with iolock:
1467 1474 if self._options.nodiff:
1468 1475 pass
1469 1476 elif self._options.view:
1470 1477 v = self._options.view
1471 1478 if PYTHON3:
1472 1479 v = _bytespath(v)
1473 1480 os.system(b"%s %s %s" %
1474 1481 (v, test.refpath, test.errpath))
1475 1482 else:
1476 1483 servefail, lines = getdiff(expected, got,
1477 1484 test.refpath, test.errpath)
1478 1485 if servefail:
1479 1486 self.addFailure(
1480 1487 test,
1481 1488 'server failed to start (HGPORT=%s)' % test._startport)
1482 1489 raise ReportedTest('server failed to start')
1483 1490 else:
1484 1491 self.stream.write('\n')
1485 1492 for line in lines:
1486 1493 if PYTHON3:
1487 1494 self.stream.flush()
1488 1495 self.stream.buffer.write(line)
1489 1496 self.stream.buffer.flush()
1490 1497 else:
1491 1498 self.stream.write(line)
1492 1499 self.stream.flush()
1493 1500
1494 1501 # handle interactive prompt without releasing iolock
1495 1502 if self._options.interactive:
1496 1503 self.stream.write('Accept this change? [n] ')
1497 1504 answer = sys.stdin.readline().strip()
1498 1505 if answer.lower() in ('y', 'yes'):
1499 1506 if test.name.endswith('.t'):
1500 1507 rename(test.errpath, test.path)
1501 1508 else:
1502 1509 rename(test.errpath, '%s.out' % test.path)
1503 1510 accepted = True
1504 1511 if not accepted:
1505 1512 self.faildata[test.name] = b''.join(lines)
1506 1513
1507 1514 return accepted
1508 1515
1509 1516 def startTest(self, test):
1510 1517 super(TestResult, self).startTest(test)
1511 1518
1512 1519 # os.times module computes the user time and system time spent by
1513 1520 # child's processes along with real elapsed time taken by a process.
1514 1521 # This module has one limitation. It can only work for Linux user
1515 1522 # and not for Windows.
1516 1523 test.started = os.times()
1517 1524 if self._firststarttime is None: # thread racy but irrelevant
1518 1525 self._firststarttime = test.started[4]
1519 1526
1520 1527 def stopTest(self, test, interrupted=False):
1521 1528 super(TestResult, self).stopTest(test)
1522 1529
1523 1530 test.stopped = os.times()
1524 1531
1525 1532 starttime = test.started
1526 1533 endtime = test.stopped
1527 1534 origin = self._firststarttime
1528 1535 self.times.append((test.name,
1529 1536 endtime[2] - starttime[2], # user space CPU time
1530 1537 endtime[3] - starttime[3], # sys space CPU time
1531 1538 endtime[4] - starttime[4], # real time
1532 1539 starttime[4] - origin, # start date in run context
1533 1540 endtime[4] - origin, # end date in run context
1534 1541 ))
1535 1542
1536 1543 if interrupted:
1537 1544 with iolock:
1538 1545 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1539 1546 test.name, self.times[-1][3]))
1540 1547
1541 1548 class TestSuite(unittest.TestSuite):
1542 1549 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1543 1550
1544 1551 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1545 1552 retest=False, keywords=None, loop=False, runs_per_test=1,
1546 1553 loadtest=None, showchannels=False,
1547 1554 *args, **kwargs):
1548 1555 """Create a new instance that can run tests with a configuration.
1549 1556
1550 1557 testdir specifies the directory where tests are executed from. This
1551 1558 is typically the ``tests`` directory from Mercurial's source
1552 1559 repository.
1553 1560
1554 1561 jobs specifies the number of jobs to run concurrently. Each test
1555 1562 executes on its own thread. Tests actually spawn new processes, so
1556 1563 state mutation should not be an issue.
1557 1564
1558 1565 If there is only one job, it will use the main thread.
1559 1566
1560 1567 whitelist and blacklist denote tests that have been whitelisted and
1561 1568 blacklisted, respectively. These arguments don't belong in TestSuite.
1562 1569 Instead, whitelist and blacklist should be handled by the thing that
1563 1570 populates the TestSuite with tests. They are present to preserve
1564 1571 backwards compatible behavior which reports skipped tests as part
1565 1572 of the results.
1566 1573
1567 1574 retest denotes whether to retest failed tests. This arguably belongs
1568 1575 outside of TestSuite.
1569 1576
1570 1577 keywords denotes key words that will be used to filter which tests
1571 1578 to execute. This arguably belongs outside of TestSuite.
1572 1579
1573 1580 loop denotes whether to loop over tests forever.
1574 1581 """
1575 1582 super(TestSuite, self).__init__(*args, **kwargs)
1576 1583
1577 1584 self._jobs = jobs
1578 1585 self._whitelist = whitelist
1579 1586 self._blacklist = blacklist
1580 1587 self._retest = retest
1581 1588 self._keywords = keywords
1582 1589 self._loop = loop
1583 1590 self._runs_per_test = runs_per_test
1584 1591 self._loadtest = loadtest
1585 1592 self._showchannels = showchannels
1586 1593
1587 1594 def run(self, result):
1588 1595 # We have a number of filters that need to be applied. We do this
1589 1596 # here instead of inside Test because it makes the running logic for
1590 1597 # Test simpler.
1591 1598 tests = []
1592 1599 num_tests = [0]
1593 1600 for test in self._tests:
1594 1601 def get():
1595 1602 num_tests[0] += 1
1596 1603 if getattr(test, 'should_reload', False):
1597 1604 return self._loadtest(test.path, num_tests[0])
1598 1605 return test
1599 1606 if not os.path.exists(test.path):
1600 1607 result.addSkip(test, "Doesn't exist")
1601 1608 continue
1602 1609
1603 1610 if not (self._whitelist and test.name in self._whitelist):
1604 1611 if self._blacklist and test.bname in self._blacklist:
1605 1612 result.addSkip(test, 'blacklisted')
1606 1613 continue
1607 1614
1608 1615 if self._retest and not os.path.exists(test.errpath):
1609 1616 result.addIgnore(test, 'not retesting')
1610 1617 continue
1611 1618
1612 1619 if self._keywords:
1613 1620 f = open(test.path, 'rb')
1614 1621 t = f.read().lower() + test.bname.lower()
1615 1622 f.close()
1616 1623 ignored = False
1617 1624 for k in self._keywords.lower().split():
1618 1625 if k not in t:
1619 1626 result.addIgnore(test, "doesn't match keyword")
1620 1627 ignored = True
1621 1628 break
1622 1629
1623 1630 if ignored:
1624 1631 continue
1625 1632 for _ in xrange(self._runs_per_test):
1626 1633 tests.append(get())
1627 1634
1628 1635 runtests = list(tests)
1629 1636 done = queue.Queue()
1630 1637 running = 0
1631 1638
1632 1639 channels = [""] * self._jobs
1633 1640
1634 1641 def job(test, result):
1635 1642 for n, v in enumerate(channels):
1636 1643 if not v:
1637 1644 channel = n
1638 1645 break
1639 1646 channels[channel] = "=" + test.name[5:].split(".")[0]
1640 1647 try:
1641 1648 test(result)
1642 1649 done.put(None)
1643 1650 except KeyboardInterrupt:
1644 1651 pass
1645 1652 except: # re-raises
1646 1653 done.put(('!', test, 'run-test raised an error, see traceback'))
1647 1654 raise
1648 1655 try:
1649 1656 channels[channel] = ''
1650 1657 except IndexError:
1651 1658 pass
1652 1659
1653 1660 def stat():
1654 1661 count = 0
1655 1662 while channels:
1656 1663 d = '\n%03s ' % count
1657 1664 for n, v in enumerate(channels):
1658 1665 if v:
1659 1666 d += v[0]
1660 1667 channels[n] = v[1:] or '.'
1661 1668 else:
1662 1669 d += ' '
1663 1670 d += ' '
1664 1671 with iolock:
1665 1672 sys.stdout.write(d + ' ')
1666 1673 sys.stdout.flush()
1667 1674 for x in xrange(10):
1668 1675 if channels:
1669 1676 time.sleep(.1)
1670 1677 count += 1
1671 1678
1672 1679 stoppedearly = False
1673 1680
1674 1681 if self._showchannels:
1675 1682 statthread = threading.Thread(target=stat, name="stat")
1676 1683 statthread.start()
1677 1684
1678 1685 try:
1679 1686 while tests or running:
1680 1687 if not done.empty() or running == self._jobs or not tests:
1681 1688 try:
1682 1689 done.get(True, 1)
1683 1690 running -= 1
1684 1691 if result and result.shouldStop:
1685 1692 stoppedearly = True
1686 1693 break
1687 1694 except queue.Empty:
1688 1695 continue
1689 1696 if tests and not running == self._jobs:
1690 1697 test = tests.pop(0)
1691 1698 if self._loop:
1692 1699 if getattr(test, 'should_reload', False):
1693 1700 num_tests[0] += 1
1694 1701 tests.append(
1695 1702 self._loadtest(test.name, num_tests[0]))
1696 1703 else:
1697 1704 tests.append(test)
1698 1705 if self._jobs == 1:
1699 1706 job(test, result)
1700 1707 else:
1701 1708 t = threading.Thread(target=job, name=test.name,
1702 1709 args=(test, result))
1703 1710 t.start()
1704 1711 running += 1
1705 1712
1706 1713 # If we stop early we still need to wait on started tests to
1707 1714 # finish. Otherwise, there is a race between the test completing
1708 1715 # and the test's cleanup code running. This could result in the
1709 1716 # test reporting incorrect.
1710 1717 if stoppedearly:
1711 1718 while running:
1712 1719 try:
1713 1720 done.get(True, 1)
1714 1721 running -= 1
1715 1722 except queue.Empty:
1716 1723 continue
1717 1724 except KeyboardInterrupt:
1718 1725 for test in runtests:
1719 1726 test.abort()
1720 1727
1721 1728 channels = []
1722 1729
1723 1730 return result
1724 1731
1725 1732 # Save the most recent 5 wall-clock runtimes of each test to a
1726 1733 # human-readable text file named .testtimes. Tests are sorted
1727 1734 # alphabetically, while times for each test are listed from oldest to
1728 1735 # newest.
1729 1736
1730 1737 def loadtimes(testdir):
1731 1738 times = []
1732 1739 try:
1733 1740 with open(os.path.join(testdir, b'.testtimes-')) as fp:
1734 1741 for line in fp:
1735 1742 ts = line.split()
1736 1743 times.append((ts[0], [float(t) for t in ts[1:]]))
1737 1744 except IOError as err:
1738 1745 if err.errno != errno.ENOENT:
1739 1746 raise
1740 1747 return times
1741 1748
1742 1749 def savetimes(testdir, result):
1743 1750 saved = dict(loadtimes(testdir))
1744 1751 maxruns = 5
1745 1752 skipped = set([str(t[0]) for t in result.skipped])
1746 1753 for tdata in result.times:
1747 1754 test, real = tdata[0], tdata[3]
1748 1755 if test not in skipped:
1749 1756 ts = saved.setdefault(test, [])
1750 1757 ts.append(real)
1751 1758 ts[:] = ts[-maxruns:]
1752 1759
1753 1760 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
1754 1761 dir=testdir, text=True)
1755 1762 with os.fdopen(fd, 'w') as fp:
1756 1763 for name, ts in sorted(saved.items()):
1757 1764 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
1758 1765 timepath = os.path.join(testdir, b'.testtimes')
1759 1766 try:
1760 1767 os.unlink(timepath)
1761 1768 except OSError:
1762 1769 pass
1763 1770 try:
1764 1771 os.rename(tmpname, timepath)
1765 1772 except OSError:
1766 1773 pass
1767 1774
1768 1775 class TextTestRunner(unittest.TextTestRunner):
1769 1776 """Custom unittest test runner that uses appropriate settings."""
1770 1777
1771 1778 def __init__(self, runner, *args, **kwargs):
1772 1779 super(TextTestRunner, self).__init__(*args, **kwargs)
1773 1780
1774 1781 self._runner = runner
1775 1782
1776 1783 def run(self, test):
1777 1784 result = TestResult(self._runner.options, self.stream,
1778 1785 self.descriptions, self.verbosity)
1779 1786
1780 1787 test(result)
1781 1788
1782 1789 failed = len(result.failures)
1783 1790 warned = len(result.warned)
1784 1791 skipped = len(result.skipped)
1785 1792 ignored = len(result.ignored)
1786 1793
1787 1794 with iolock:
1788 1795 self.stream.writeln('')
1789 1796
1790 1797 if not self._runner.options.noskips:
1791 1798 for test, msg in result.skipped:
1792 1799 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1793 1800 for test, msg in result.warned:
1794 1801 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1795 1802 for test, msg in result.failures:
1796 1803 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1797 1804 for test, msg in result.errors:
1798 1805 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1799 1806
1800 1807 if self._runner.options.xunit:
1801 1808 with open(self._runner.options.xunit, 'wb') as xuf:
1802 1809 timesd = dict((t[0], t[3]) for t in result.times)
1803 1810 doc = minidom.Document()
1804 1811 s = doc.createElement('testsuite')
1805 1812 s.setAttribute('name', 'run-tests')
1806 1813 s.setAttribute('tests', str(result.testsRun))
1807 1814 s.setAttribute('errors', "0") # TODO
1808 1815 s.setAttribute('failures', str(failed))
1809 1816 s.setAttribute('skipped', str(skipped + ignored))
1810 1817 doc.appendChild(s)
1811 1818 for tc in result.successes:
1812 1819 t = doc.createElement('testcase')
1813 1820 t.setAttribute('name', tc.name)
1814 1821 t.setAttribute('time', '%.3f' % timesd[tc.name])
1815 1822 s.appendChild(t)
1816 1823 for tc, err in sorted(result.faildata.items()):
1817 1824 t = doc.createElement('testcase')
1818 1825 t.setAttribute('name', tc)
1819 1826 t.setAttribute('time', '%.3f' % timesd[tc])
1820 1827 # createCDATASection expects a unicode or it will
1821 1828 # convert using default conversion rules, which will
1822 1829 # fail if string isn't ASCII.
1823 1830 err = cdatasafe(err).decode('utf-8', 'replace')
1824 1831 cd = doc.createCDATASection(err)
1825 1832 t.appendChild(cd)
1826 1833 s.appendChild(t)
1827 1834 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1828 1835
1829 1836 if self._runner.options.json:
1830 1837 jsonpath = os.path.join(self._runner._testdir, b'report.json')
1831 1838 with open(jsonpath, 'w') as fp:
1832 1839 timesd = {}
1833 1840 for tdata in result.times:
1834 1841 test = tdata[0]
1835 1842 timesd[test] = tdata[1:]
1836 1843
1837 1844 outcome = {}
1838 1845 groups = [('success', ((tc, None)
1839 1846 for tc in result.successes)),
1840 1847 ('failure', result.failures),
1841 1848 ('skip', result.skipped)]
1842 1849 for res, testcases in groups:
1843 1850 for tc, __ in testcases:
1844 1851 if tc.name in timesd:
1845 1852 diff = result.faildata.get(tc.name, b'')
1846 1853 tres = {'result': res,
1847 1854 'time': ('%0.3f' % timesd[tc.name][2]),
1848 1855 'cuser': ('%0.3f' % timesd[tc.name][0]),
1849 1856 'csys': ('%0.3f' % timesd[tc.name][1]),
1850 1857 'start': ('%0.3f' % timesd[tc.name][3]),
1851 1858 'end': ('%0.3f' % timesd[tc.name][4]),
1852 1859 'diff': diff.decode('unicode_escape'),
1853 1860 }
1854 1861 else:
1855 1862 # blacklisted test
1856 1863 tres = {'result': res}
1857 1864
1858 1865 outcome[tc.name] = tres
1859 1866 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
1860 1867 separators=(',', ': '))
1861 1868 fp.writelines(("testreport =", jsonout))
1862 1869
1863 1870 self._runner._checkhglib('Tested')
1864 1871
1865 1872 savetimes(self._runner._testdir, result)
1866 1873
1867 1874 if failed and self._runner.options.known_good_rev:
1868 1875 def nooutput(args):
1869 1876 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
1870 1877 stdout=subprocess.PIPE)
1871 1878 p.stdout.read()
1872 1879 p.wait()
1873 1880 for test, msg in result.failures:
1874 1881 nooutput(['hg', 'bisect', '--reset']),
1875 1882 nooutput(['hg', 'bisect', '--bad', '.'])
1876 1883 nooutput(['hg', 'bisect', '--good',
1877 1884 self._runner.options.known_good_rev])
1878 1885 # TODO: we probably need to forward some options
1879 1886 # that alter hg's behavior inside the tests.
1880 1887 rtc = '%s %s %s' % (sys.executable, sys.argv[0], test)
1881 1888 sub = subprocess.Popen(['hg', 'bisect', '--command', rtc],
1882 1889 stderr=subprocess.STDOUT,
1883 1890 stdout=subprocess.PIPE)
1884 1891 data = sub.stdout.read()
1885 1892 sub.wait()
1886 1893 m = re.search(
1887 1894 (r'\nThe first (?P<goodbad>bad|good) revision '
1888 1895 r'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
1889 1896 r'summary: +(?P<summary>[^\n]+)\n'),
1890 1897 data, (re.MULTILINE | re.DOTALL))
1891 1898 if m is None:
1892 1899 self.stream.writeln(
1893 1900 'Failed to identify failure point for %s' % test)
1894 1901 continue
1895 1902 dat = m.groupdict()
1896 1903 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
1897 1904 self.stream.writeln(
1898 1905 '%s %s by %s (%s)' % (
1899 1906 test, verb, dat['node'], dat['summary']))
1900 1907 self.stream.writeln(
1901 1908 '# Ran %d tests, %d skipped, %d warned, %d failed.'
1902 1909 % (result.testsRun,
1903 1910 skipped + ignored, warned, failed))
1904 1911 if failed:
1905 1912 self.stream.writeln('python hash seed: %s' %
1906 1913 os.environ['PYTHONHASHSEED'])
1907 1914 if self._runner.options.time:
1908 1915 self.printtimes(result.times)
1909 1916
1910 1917 return result
1911 1918
1912 1919 def printtimes(self, times):
1913 1920 # iolock held by run
1914 1921 self.stream.writeln('# Producing time report')
1915 1922 times.sort(key=lambda t: (t[3]))
1916 1923 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
1917 1924 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
1918 1925 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
1919 1926 for tdata in times:
1920 1927 test = tdata[0]
1921 1928 cuser, csys, real, start, end = tdata[1:6]
1922 1929 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
1923 1930
1924 1931 class TestRunner(object):
1925 1932 """Holds context for executing tests.
1926 1933
1927 1934 Tests rely on a lot of state. This object holds it for them.
1928 1935 """
1929 1936
1930 1937 # Programs required to run tests.
1931 1938 REQUIREDTOOLS = [
1932 1939 os.path.basename(_bytespath(sys.executable)),
1933 1940 b'diff',
1934 1941 b'grep',
1935 1942 b'unzip',
1936 1943 b'gunzip',
1937 1944 b'bunzip2',
1938 1945 b'sed',
1939 1946 ]
1940 1947
1941 1948 # Maps file extensions to test class.
1942 1949 TESTTYPES = [
1943 1950 (b'.py', PythonTest),
1944 1951 (b'.t', TTest),
1945 1952 ]
1946 1953
1947 1954 def __init__(self):
1948 1955 self.options = None
1949 1956 self._hgroot = None
1950 1957 self._testdir = None
1951 1958 self._hgtmp = None
1952 1959 self._installdir = None
1953 1960 self._bindir = None
1954 1961 self._tmpbinddir = None
1955 1962 self._pythondir = None
1956 1963 self._coveragefile = None
1957 1964 self._createdfiles = []
1958 1965 self._hgcommand = None
1959 1966 self._hgpath = None
1960 1967 self._portoffset = 0
1961 1968 self._ports = {}
1962 1969
1963 1970 def run(self, args, parser=None):
1964 1971 """Run the test suite."""
1965 1972 oldmask = os.umask(0o22)
1966 1973 try:
1967 1974 parser = parser or getparser()
1968 1975 options, args = parseargs(args, parser)
1969 1976 # positional arguments are paths to test files to run, so
1970 1977 # we make sure they're all bytestrings
1971 1978 args = [_bytespath(a) for a in args]
1972 1979 self.options = options
1973 1980
1974 1981 self._checktools()
1975 1982 tests = self.findtests(args)
1976 1983 if options.profile_runner:
1977 1984 import statprof
1978 1985 statprof.start()
1979 1986 result = self._run(tests)
1980 1987 if options.profile_runner:
1981 1988 statprof.stop()
1982 1989 statprof.display()
1983 1990 return result
1984 1991
1985 1992 finally:
1986 1993 os.umask(oldmask)
1987 1994
1988 1995 def _run(self, tests):
1989 1996 if self.options.random:
1990 1997 random.shuffle(tests)
1991 1998 else:
1992 1999 # keywords for slow tests
1993 2000 slow = {b'svn': 10,
1994 2001 b'cvs': 10,
1995 2002 b'hghave': 10,
1996 2003 b'largefiles-update': 10,
1997 2004 b'run-tests': 10,
1998 2005 b'corruption': 10,
1999 2006 b'race': 10,
2000 2007 b'i18n': 10,
2001 2008 b'check': 100,
2002 2009 b'gendoc': 100,
2003 2010 b'contrib-perf': 200,
2004 2011 }
2005 2012 perf = {}
2006 2013 def sortkey(f):
2007 2014 # run largest tests first, as they tend to take the longest
2008 2015 try:
2009 2016 return perf[f]
2010 2017 except KeyError:
2011 2018 try:
2012 2019 val = -os.stat(f).st_size
2013 2020 except OSError as e:
2014 2021 if e.errno != errno.ENOENT:
2015 2022 raise
2016 2023 perf[f] = -1e9 # file does not exist, tell early
2017 2024 return -1e9
2018 2025 for kw, mul in slow.items():
2019 2026 if kw in f:
2020 2027 val *= mul
2021 2028 if f.endswith(b'.py'):
2022 2029 val /= 10.0
2023 2030 perf[f] = val / 1000.0
2024 2031 return perf[f]
2025 2032 tests.sort(key=sortkey)
2026 2033
2027 2034 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2028 2035 os, 'getcwdb', os.getcwd)()
2029 2036
2030 2037 if 'PYTHONHASHSEED' not in os.environ:
2031 2038 # use a random python hash seed all the time
2032 2039 # we do the randomness ourself to know what seed is used
2033 2040 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2034 2041
2035 2042 if self.options.tmpdir:
2036 2043 self.options.keep_tmpdir = True
2037 2044 tmpdir = _bytespath(self.options.tmpdir)
2038 2045 if os.path.exists(tmpdir):
2039 2046 # Meaning of tmpdir has changed since 1.3: we used to create
2040 2047 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2041 2048 # tmpdir already exists.
2042 2049 print("error: temp dir %r already exists" % tmpdir)
2043 2050 return 1
2044 2051
2045 2052 # Automatically removing tmpdir sounds convenient, but could
2046 2053 # really annoy anyone in the habit of using "--tmpdir=/tmp"
2047 2054 # or "--tmpdir=$HOME".
2048 2055 #vlog("# Removing temp dir", tmpdir)
2049 2056 #shutil.rmtree(tmpdir)
2050 2057 os.makedirs(tmpdir)
2051 2058 else:
2052 2059 d = None
2053 2060 if os.name == 'nt':
2054 2061 # without this, we get the default temp dir location, but
2055 2062 # in all lowercase, which causes troubles with paths (issue3490)
2056 2063 d = osenvironb.get(b'TMP', None)
2057 2064 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2058 2065
2059 2066 self._hgtmp = osenvironb[b'HGTMP'] = (
2060 2067 os.path.realpath(tmpdir))
2061 2068
2062 2069 if self.options.with_hg:
2063 2070 self._installdir = None
2064 2071 whg = self.options.with_hg
2065 2072 self._bindir = os.path.dirname(os.path.realpath(whg))
2066 2073 assert isinstance(self._bindir, bytes)
2067 2074 self._hgcommand = os.path.basename(whg)
2068 2075 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2069 2076 os.makedirs(self._tmpbindir)
2070 2077
2071 2078 # This looks redundant with how Python initializes sys.path from
2072 2079 # the location of the script being executed. Needed because the
2073 2080 # "hg" specified by --with-hg is not the only Python script
2074 2081 # executed in the test suite that needs to import 'mercurial'
2075 2082 # ... which means it's not really redundant at all.
2076 2083 self._pythondir = self._bindir
2077 2084 else:
2078 2085 self._installdir = os.path.join(self._hgtmp, b"install")
2079 2086 self._bindir = os.path.join(self._installdir, b"bin")
2080 2087 self._hgcommand = b'hg'
2081 2088 self._tmpbindir = self._bindir
2082 2089 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2083 2090
2084 2091 # set CHGHG, then replace "hg" command by "chg"
2085 2092 chgbindir = self._bindir
2086 2093 if self.options.chg or self.options.with_chg:
2087 2094 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2088 2095 else:
2089 2096 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2090 2097 if self.options.chg:
2091 2098 self._hgcommand = b'chg'
2092 2099 elif self.options.with_chg:
2093 2100 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2094 2101 self._hgcommand = os.path.basename(self.options.with_chg)
2095 2102
2096 2103 osenvironb[b"BINDIR"] = self._bindir
2097 2104 osenvironb[b"PYTHON"] = PYTHON
2098 2105
2099 2106 if self.options.with_python3:
2100 2107 osenvironb[b'PYTHON3'] = self.options.with_python3
2101 2108
2102 2109 fileb = _bytespath(__file__)
2103 2110 runtestdir = os.path.abspath(os.path.dirname(fileb))
2104 2111 osenvironb[b'RUNTESTDIR'] = runtestdir
2105 2112 if PYTHON3:
2106 2113 sepb = _bytespath(os.pathsep)
2107 2114 else:
2108 2115 sepb = os.pathsep
2109 2116 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2110 2117 if os.path.islink(__file__):
2111 2118 # test helper will likely be at the end of the symlink
2112 2119 realfile = os.path.realpath(fileb)
2113 2120 realdir = os.path.abspath(os.path.dirname(realfile))
2114 2121 path.insert(2, realdir)
2115 2122 if chgbindir != self._bindir:
2116 2123 path.insert(1, chgbindir)
2117 2124 if self._testdir != runtestdir:
2118 2125 path = [self._testdir] + path
2119 2126 if self._tmpbindir != self._bindir:
2120 2127 path = [self._tmpbindir] + path
2121 2128 osenvironb[b"PATH"] = sepb.join(path)
2122 2129
2123 2130 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2124 2131 # can run .../tests/run-tests.py test-foo where test-foo
2125 2132 # adds an extension to HGRC. Also include run-test.py directory to
2126 2133 # import modules like heredoctest.
2127 2134 pypath = [self._pythondir, self._testdir, runtestdir]
2128 2135 # We have to augment PYTHONPATH, rather than simply replacing
2129 2136 # it, in case external libraries are only available via current
2130 2137 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2131 2138 # are in /opt/subversion.)
2132 2139 oldpypath = osenvironb.get(IMPL_PATH)
2133 2140 if oldpypath:
2134 2141 pypath.append(oldpypath)
2135 2142 osenvironb[IMPL_PATH] = sepb.join(pypath)
2136 2143
2137 2144 if self.options.pure:
2138 2145 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2139 2146 os.environ["HGMODULEPOLICY"] = "py"
2140 2147
2141 2148 if self.options.allow_slow_tests:
2142 2149 os.environ["HGTEST_SLOW"] = "slow"
2143 2150 elif 'HGTEST_SLOW' in os.environ:
2144 2151 del os.environ['HGTEST_SLOW']
2145 2152
2146 2153 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2147 2154
2148 2155 vlog("# Using TESTDIR", self._testdir)
2149 2156 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2150 2157 vlog("# Using HGTMP", self._hgtmp)
2151 2158 vlog("# Using PATH", os.environ["PATH"])
2152 2159 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2153 2160
2154 2161 try:
2155 2162 return self._runtests(tests) or 0
2156 2163 finally:
2157 2164 time.sleep(.1)
2158 2165 self._cleanup()
2159 2166
2160 2167 def findtests(self, args):
2161 2168 """Finds possible test files from arguments.
2162 2169
2163 2170 If you wish to inject custom tests into the test harness, this would
2164 2171 be a good function to monkeypatch or override in a derived class.
2165 2172 """
2166 2173 if not args:
2167 2174 if self.options.changed:
2168 2175 proc = Popen4('hg st --rev "%s" -man0 .' %
2169 2176 self.options.changed, None, 0)
2170 2177 stdout, stderr = proc.communicate()
2171 2178 args = stdout.strip(b'\0').split(b'\0')
2172 2179 else:
2173 2180 args = os.listdir(b'.')
2174 2181
2175 2182 return [t for t in args
2176 2183 if os.path.basename(t).startswith(b'test-')
2177 2184 and (t.endswith(b'.py') or t.endswith(b'.t'))]
2178 2185
2179 2186 def _runtests(self, tests):
2180 2187 try:
2181 2188 if self._installdir:
2182 2189 self._installhg()
2183 2190 self._checkhglib("Testing")
2184 2191 else:
2185 2192 self._usecorrectpython()
2186 2193 if self.options.chg:
2187 2194 assert self._installdir
2188 2195 self._installchg()
2189 2196
2190 2197 if self.options.restart:
2191 2198 orig = list(tests)
2192 2199 while tests:
2193 2200 if os.path.exists(tests[0] + ".err"):
2194 2201 break
2195 2202 tests.pop(0)
2196 2203 if not tests:
2197 2204 print("running all tests")
2198 2205 tests = orig
2199 2206
2200 2207 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
2201 2208
2202 2209 failed = False
2203 2210 warned = False
2204 2211 kws = self.options.keywords
2205 2212 if kws is not None and PYTHON3:
2206 2213 kws = kws.encode('utf-8')
2207 2214
2208 2215 suite = TestSuite(self._testdir,
2209 2216 jobs=self.options.jobs,
2210 2217 whitelist=self.options.whitelisted,
2211 2218 blacklist=self.options.blacklist,
2212 2219 retest=self.options.retest,
2213 2220 keywords=kws,
2214 2221 loop=self.options.loop,
2215 2222 runs_per_test=self.options.runs_per_test,
2216 2223 showchannels=self.options.showchannels,
2217 2224 tests=tests, loadtest=self._gettest)
2218 2225 verbosity = 1
2219 2226 if self.options.verbose:
2220 2227 verbosity = 2
2221 2228 runner = TextTestRunner(self, verbosity=verbosity)
2222 2229 result = runner.run(suite)
2223 2230
2224 2231 if result.failures:
2225 2232 failed = True
2226 2233 if result.warned:
2227 2234 warned = True
2228 2235
2229 2236 if self.options.anycoverage:
2230 2237 self._outputcoverage()
2231 2238 except KeyboardInterrupt:
2232 2239 failed = True
2233 2240 print("\ninterrupted!")
2234 2241
2235 2242 if failed:
2236 2243 return 1
2237 2244 if warned:
2238 2245 return 80
2239 2246
2240 2247 def _getport(self, count):
2241 2248 port = self._ports.get(count) # do we have a cached entry?
2242 2249 if port is None:
2243 2250 portneeded = 3
2244 2251 # above 100 tries we just give up and let test reports failure
2245 2252 for tries in xrange(100):
2246 2253 allfree = True
2247 2254 port = self.options.port + self._portoffset
2248 2255 for idx in xrange(portneeded):
2249 2256 if not checkportisavailable(port + idx):
2250 2257 allfree = False
2251 2258 break
2252 2259 self._portoffset += portneeded
2253 2260 if allfree:
2254 2261 break
2255 2262 self._ports[count] = port
2256 2263 return port
2257 2264
2258 2265 def _gettest(self, test, count):
2259 2266 """Obtain a Test by looking at its filename.
2260 2267
2261 2268 Returns a Test instance. The Test may not be runnable if it doesn't
2262 2269 map to a known type.
2263 2270 """
2264 2271 lctest = test.lower()
2265 2272 testcls = Test
2266 2273
2267 2274 for ext, cls in self.TESTTYPES:
2268 2275 if lctest.endswith(ext):
2269 2276 testcls = cls
2270 2277 break
2271 2278
2272 2279 refpath = os.path.join(self._testdir, test)
2273 2280 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2274 2281
2275 2282 t = testcls(refpath, tmpdir,
2276 2283 keeptmpdir=self.options.keep_tmpdir,
2277 2284 debug=self.options.debug,
2278 2285 timeout=self.options.timeout,
2279 2286 startport=self._getport(count),
2280 2287 extraconfigopts=self.options.extra_config_opt,
2281 2288 py3kwarnings=self.options.py3k_warnings,
2282 2289 shell=self.options.shell,
2283 2290 hgcommand=self._hgcommand,
2284 2291 usechg=bool(self.options.with_chg or self.options.chg))
2285 2292 t.should_reload = True
2286 2293 return t
2287 2294
2288 2295 def _cleanup(self):
2289 2296 """Clean up state from this test invocation."""
2290 2297 if self.options.keep_tmpdir:
2291 2298 return
2292 2299
2293 2300 vlog("# Cleaning up HGTMP", self._hgtmp)
2294 2301 shutil.rmtree(self._hgtmp, True)
2295 2302 for f in self._createdfiles:
2296 2303 try:
2297 2304 os.remove(f)
2298 2305 except OSError:
2299 2306 pass
2300 2307
2301 2308 def _usecorrectpython(self):
2302 2309 """Configure the environment to use the appropriate Python in tests."""
2303 2310 # Tests must use the same interpreter as us or bad things will happen.
2304 2311 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2305 2312 if getattr(os, 'symlink', None):
2306 2313 vlog("# Making python executable in test path a symlink to '%s'" %
2307 2314 sys.executable)
2308 2315 mypython = os.path.join(self._tmpbindir, pyexename)
2309 2316 try:
2310 2317 if os.readlink(mypython) == sys.executable:
2311 2318 return
2312 2319 os.unlink(mypython)
2313 2320 except OSError as err:
2314 2321 if err.errno != errno.ENOENT:
2315 2322 raise
2316 2323 if self._findprogram(pyexename) != sys.executable:
2317 2324 try:
2318 2325 os.symlink(sys.executable, mypython)
2319 2326 self._createdfiles.append(mypython)
2320 2327 except OSError as err:
2321 2328 # child processes may race, which is harmless
2322 2329 if err.errno != errno.EEXIST:
2323 2330 raise
2324 2331 else:
2325 2332 exedir, exename = os.path.split(sys.executable)
2326 2333 vlog("# Modifying search path to find %s as %s in '%s'" %
2327 2334 (exename, pyexename, exedir))
2328 2335 path = os.environ['PATH'].split(os.pathsep)
2329 2336 while exedir in path:
2330 2337 path.remove(exedir)
2331 2338 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2332 2339 if not self._findprogram(pyexename):
2333 2340 print("WARNING: Cannot find %s in search path" % pyexename)
2334 2341
2335 2342 def _installhg(self):
2336 2343 """Install hg into the test environment.
2337 2344
2338 2345 This will also configure hg with the appropriate testing settings.
2339 2346 """
2340 2347 vlog("# Performing temporary installation of HG")
2341 2348 installerrs = os.path.join(self._hgtmp, b"install.err")
2342 2349 compiler = ''
2343 2350 if self.options.compiler:
2344 2351 compiler = '--compiler ' + self.options.compiler
2345 2352 if self.options.pure:
2346 2353 pure = b"--pure"
2347 2354 else:
2348 2355 pure = b""
2349 2356
2350 2357 # Run installer in hg root
2351 2358 script = os.path.realpath(sys.argv[0])
2352 2359 exe = sys.executable
2353 2360 if PYTHON3:
2354 2361 compiler = _bytespath(compiler)
2355 2362 script = _bytespath(script)
2356 2363 exe = _bytespath(exe)
2357 2364 hgroot = os.path.dirname(os.path.dirname(script))
2358 2365 self._hgroot = hgroot
2359 2366 os.chdir(hgroot)
2360 2367 nohome = b'--home=""'
2361 2368 if os.name == 'nt':
2362 2369 # The --home="" trick works only on OS where os.sep == '/'
2363 2370 # because of a distutils convert_path() fast-path. Avoid it at
2364 2371 # least on Windows for now, deal with .pydistutils.cfg bugs
2365 2372 # when they happen.
2366 2373 nohome = b''
2367 2374 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2368 2375 b' build %(compiler)s --build-base="%(base)s"'
2369 2376 b' install --force --prefix="%(prefix)s"'
2370 2377 b' --install-lib="%(libdir)s"'
2371 2378 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2372 2379 % {b'exe': exe, b'pure': pure,
2373 2380 b'compiler': compiler,
2374 2381 b'base': os.path.join(self._hgtmp, b"build"),
2375 2382 b'prefix': self._installdir, b'libdir': self._pythondir,
2376 2383 b'bindir': self._bindir,
2377 2384 b'nohome': nohome, b'logfile': installerrs})
2378 2385
2379 2386 # setuptools requires install directories to exist.
2380 2387 def makedirs(p):
2381 2388 try:
2382 2389 os.makedirs(p)
2383 2390 except OSError as e:
2384 2391 if e.errno != errno.EEXIST:
2385 2392 raise
2386 2393 makedirs(self._pythondir)
2387 2394 makedirs(self._bindir)
2388 2395
2389 2396 vlog("# Running", cmd)
2390 2397 if os.system(cmd) == 0:
2391 2398 if not self.options.verbose:
2392 2399 try:
2393 2400 os.remove(installerrs)
2394 2401 except OSError as e:
2395 2402 if e.errno != errno.ENOENT:
2396 2403 raise
2397 2404 else:
2398 2405 f = open(installerrs, 'rb')
2399 2406 for line in f:
2400 2407 if PYTHON3:
2401 2408 sys.stdout.buffer.write(line)
2402 2409 else:
2403 2410 sys.stdout.write(line)
2404 2411 f.close()
2405 2412 sys.exit(1)
2406 2413 os.chdir(self._testdir)
2407 2414
2408 2415 self._usecorrectpython()
2409 2416
2410 2417 if self.options.py3k_warnings and not self.options.anycoverage:
2411 2418 vlog("# Updating hg command to enable Py3k Warnings switch")
2412 2419 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2413 2420 lines = [line.rstrip() for line in f]
2414 2421 lines[0] += ' -3'
2415 2422 f.close()
2416 2423 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2417 2424 for line in lines:
2418 2425 f.write(line + '\n')
2419 2426 f.close()
2420 2427
2421 2428 hgbat = os.path.join(self._bindir, b'hg.bat')
2422 2429 if os.path.isfile(hgbat):
2423 2430 # hg.bat expects to be put in bin/scripts while run-tests.py
2424 2431 # installation layout put it in bin/ directly. Fix it
2425 2432 f = open(hgbat, 'rb')
2426 2433 data = f.read()
2427 2434 f.close()
2428 2435 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2429 2436 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2430 2437 b'"%~dp0python" "%~dp0hg" %*')
2431 2438 f = open(hgbat, 'wb')
2432 2439 f.write(data)
2433 2440 f.close()
2434 2441 else:
2435 2442 print('WARNING: cannot fix hg.bat reference to python.exe')
2436 2443
2437 2444 if self.options.anycoverage:
2438 2445 custom = os.path.join(self._testdir, 'sitecustomize.py')
2439 2446 target = os.path.join(self._pythondir, 'sitecustomize.py')
2440 2447 vlog('# Installing coverage trigger to %s' % target)
2441 2448 shutil.copyfile(custom, target)
2442 2449 rc = os.path.join(self._testdir, '.coveragerc')
2443 2450 vlog('# Installing coverage rc to %s' % rc)
2444 2451 os.environ['COVERAGE_PROCESS_START'] = rc
2445 2452 covdir = os.path.join(self._installdir, '..', 'coverage')
2446 2453 try:
2447 2454 os.mkdir(covdir)
2448 2455 except OSError as e:
2449 2456 if e.errno != errno.EEXIST:
2450 2457 raise
2451 2458
2452 2459 os.environ['COVERAGE_DIR'] = covdir
2453 2460
2454 2461 def _checkhglib(self, verb):
2455 2462 """Ensure that the 'mercurial' package imported by python is
2456 2463 the one we expect it to be. If not, print a warning to stderr."""
2457 2464 if ((self._bindir == self._pythondir) and
2458 2465 (self._bindir != self._tmpbindir)):
2459 2466 # The pythondir has been inferred from --with-hg flag.
2460 2467 # We cannot expect anything sensible here.
2461 2468 return
2462 2469 expecthg = os.path.join(self._pythondir, b'mercurial')
2463 2470 actualhg = self._gethgpath()
2464 2471 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2465 2472 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2466 2473 ' (expected %s)\n'
2467 2474 % (verb, actualhg, expecthg))
2468 2475 def _gethgpath(self):
2469 2476 """Return the path to the mercurial package that is actually found by
2470 2477 the current Python interpreter."""
2471 2478 if self._hgpath is not None:
2472 2479 return self._hgpath
2473 2480
2474 2481 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2475 2482 cmd = cmd % PYTHON
2476 2483 if PYTHON3:
2477 2484 cmd = _strpath(cmd)
2478 2485 pipe = os.popen(cmd)
2479 2486 try:
2480 2487 self._hgpath = _bytespath(pipe.read().strip())
2481 2488 finally:
2482 2489 pipe.close()
2483 2490
2484 2491 return self._hgpath
2485 2492
2486 2493 def _installchg(self):
2487 2494 """Install chg into the test environment"""
2488 2495 vlog('# Performing temporary installation of CHG')
2489 2496 assert os.path.dirname(self._bindir) == self._installdir
2490 2497 assert self._hgroot, 'must be called after _installhg()'
2491 2498 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2492 2499 % {b'make': 'make', # TODO: switch by option or environment?
2493 2500 b'prefix': self._installdir})
2494 2501 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2495 2502 vlog("# Running", cmd)
2496 2503 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2497 2504 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2498 2505 stderr=subprocess.STDOUT)
2499 2506 out, _err = proc.communicate()
2500 2507 if proc.returncode != 0:
2501 2508 if PYTHON3:
2502 2509 sys.stdout.buffer.write(out)
2503 2510 else:
2504 2511 sys.stdout.write(out)
2505 2512 sys.exit(1)
2506 2513
2507 2514 def _outputcoverage(self):
2508 2515 """Produce code coverage output."""
2509 2516 import coverage
2510 2517 coverage = coverage.coverage
2511 2518
2512 2519 vlog('# Producing coverage report')
2513 2520 # chdir is the easiest way to get short, relative paths in the
2514 2521 # output.
2515 2522 os.chdir(self._hgroot)
2516 2523 covdir = os.path.join(self._installdir, '..', 'coverage')
2517 2524 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2518 2525
2519 2526 # Map install directory paths back to source directory.
2520 2527 cov.config.paths['srcdir'] = ['.', self._pythondir]
2521 2528
2522 2529 cov.combine()
2523 2530
2524 2531 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2525 2532 cov.report(ignore_errors=True, omit=omit)
2526 2533
2527 2534 if self.options.htmlcov:
2528 2535 htmldir = os.path.join(self._testdir, 'htmlcov')
2529 2536 cov.html_report(directory=htmldir, omit=omit)
2530 2537 if self.options.annotate:
2531 2538 adir = os.path.join(self._testdir, 'annotated')
2532 2539 if not os.path.isdir(adir):
2533 2540 os.mkdir(adir)
2534 2541 cov.annotate(directory=adir, omit=omit)
2535 2542
2536 2543 def _findprogram(self, program):
2537 2544 """Search PATH for a executable program"""
2538 2545 dpb = _bytespath(os.defpath)
2539 2546 sepb = _bytespath(os.pathsep)
2540 2547 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2541 2548 name = os.path.join(p, program)
2542 2549 if os.name == 'nt' or os.access(name, os.X_OK):
2543 2550 return name
2544 2551 return None
2545 2552
2546 2553 def _checktools(self):
2547 2554 """Ensure tools required to run tests are present."""
2548 2555 for p in self.REQUIREDTOOLS:
2549 2556 if os.name == 'nt' and not p.endswith('.exe'):
2550 2557 p += '.exe'
2551 2558 found = self._findprogram(p)
2552 2559 if found:
2553 2560 vlog("# Found prerequisite", p, "at", found)
2554 2561 else:
2555 2562 print("WARNING: Did not find prerequisite tool: %s " %
2556 2563 p.decode("utf-8"))
2557 2564
2558 2565 if __name__ == '__main__':
2559 2566 runner = TestRunner()
2560 2567
2561 2568 try:
2562 2569 import msvcrt
2563 2570 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2564 2571 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2565 2572 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2566 2573 except ImportError:
2567 2574 pass
2568 2575
2569 2576 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now