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