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