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