##// END OF EJS Templates
run-tests: allow hg executable to be hg.exe...
Kostia Balytskyi -
r32328:531e6a57 default
parent child Browse files
Show More
@@ -1,2734 +1,2734 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.6-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.6+")
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 if not os.path.basename(options.with_hg) == b'hg':
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.6 and 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'backout = -d "0 0"\n')
954 954 hgrc.write(b'commit = -d "0 0"\n')
955 955 hgrc.write(b'shelve = --date "0 0"\n')
956 956 hgrc.write(b'tag = -d "0 0"\n')
957 957 hgrc.write(b'[devel]\n')
958 958 hgrc.write(b'all-warnings = true\n')
959 959 hgrc.write(b'[largefiles]\n')
960 960 hgrc.write(b'usercache = %s\n' %
961 961 (os.path.join(self._testtmp, b'.cache/largefiles')))
962 962 hgrc.write(b'[web]\n')
963 963 hgrc.write(b'address = localhost\n')
964 964 hgrc.write(b'ipv6 = %s\n' % str(self._useipv6).encode('ascii'))
965 965
966 966 for opt in self._extraconfigopts:
967 967 section, key = opt.split('.', 1)
968 968 assert '=' in key, ('extra config opt %s must '
969 969 'have an = for assignment' % opt)
970 970 hgrc.write(b'[%s]\n%s\n' % (section, key))
971 971 hgrc.close()
972 972
973 973 def fail(self, msg):
974 974 # unittest differentiates between errored and failed.
975 975 # Failed is denoted by AssertionError (by default at least).
976 976 raise AssertionError(msg)
977 977
978 978 def _runcommand(self, cmd, env, normalizenewlines=False):
979 979 """Run command in a sub-process, capturing the output (stdout and
980 980 stderr).
981 981
982 982 Return a tuple (exitcode, output). output is None in debug mode.
983 983 """
984 984 if self._debug:
985 985 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
986 986 env=env)
987 987 ret = proc.wait()
988 988 return (ret, None)
989 989
990 990 proc = Popen4(cmd, self._testtmp, self._timeout, env)
991 991 def cleanup():
992 992 terminate(proc)
993 993 ret = proc.wait()
994 994 if ret == 0:
995 995 ret = signal.SIGTERM << 8
996 996 killdaemons(env['DAEMON_PIDS'])
997 997 return ret
998 998
999 999 output = ''
1000 1000 proc.tochild.close()
1001 1001
1002 1002 try:
1003 1003 output = proc.fromchild.read()
1004 1004 except KeyboardInterrupt:
1005 1005 vlog('# Handling keyboard interrupt')
1006 1006 cleanup()
1007 1007 raise
1008 1008
1009 1009 ret = proc.wait()
1010 1010 if wifexited(ret):
1011 1011 ret = os.WEXITSTATUS(ret)
1012 1012
1013 1013 if proc.timeout:
1014 1014 ret = 'timeout'
1015 1015
1016 1016 if ret:
1017 1017 killdaemons(env['DAEMON_PIDS'])
1018 1018
1019 1019 for s, r in self._getreplacements():
1020 1020 output = re.sub(s, r, output)
1021 1021
1022 1022 if normalizenewlines:
1023 1023 output = output.replace('\r\n', '\n')
1024 1024
1025 1025 return ret, output.splitlines(True)
1026 1026
1027 1027 class PythonTest(Test):
1028 1028 """A Python-based test."""
1029 1029
1030 1030 @property
1031 1031 def refpath(self):
1032 1032 return os.path.join(self._testdir, b'%s.out' % self.bname)
1033 1033
1034 1034 def _run(self, env):
1035 1035 py3kswitch = self._py3kwarnings and b' -3' or b''
1036 1036 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
1037 1037 vlog("# Running", cmd)
1038 1038 normalizenewlines = os.name == 'nt'
1039 1039 result = self._runcommand(cmd, env,
1040 1040 normalizenewlines=normalizenewlines)
1041 1041 if self._aborted:
1042 1042 raise KeyboardInterrupt()
1043 1043
1044 1044 return result
1045 1045
1046 1046 # Some glob patterns apply only in some circumstances, so the script
1047 1047 # might want to remove (glob) annotations that otherwise should be
1048 1048 # retained.
1049 1049 checkcodeglobpats = [
1050 1050 # On Windows it looks like \ doesn't require a (glob), but we know
1051 1051 # better.
1052 1052 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
1053 1053 re.compile(br'^moving \S+/.*[^)]$'),
1054 1054 re.compile(br'^pulling from \$TESTTMP/.*[^)]$'),
1055 1055 # Not all platforms have 127.0.0.1 as loopback (though most do),
1056 1056 # so we always glob that too.
1057 1057 re.compile(br'.*\$LOCALIP.*$'),
1058 1058 ]
1059 1059
1060 1060 bchr = chr
1061 1061 if PYTHON3:
1062 1062 bchr = lambda x: bytes([x])
1063 1063
1064 1064 class TTest(Test):
1065 1065 """A "t test" is a test backed by a .t file."""
1066 1066
1067 1067 SKIPPED_PREFIX = b'skipped: '
1068 1068 FAILED_PREFIX = b'hghave check failed: '
1069 1069 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
1070 1070
1071 1071 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
1072 1072 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
1073 1073 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
1074 1074
1075 1075 def __init__(self, path, *args, **kwds):
1076 1076 # accept an extra "case" parameter
1077 1077 case = None
1078 1078 if 'case' in kwds:
1079 1079 case = kwds.pop('case')
1080 1080 self._case = case
1081 1081 self._allcases = parsettestcases(path)
1082 1082 super(TTest, self).__init__(path, *args, **kwds)
1083 1083 if case:
1084 1084 self.name = '%s (case %s)' % (self.name, _strpath(case))
1085 1085 self.errpath = b'%s.%s.err' % (self.errpath[:-4], case)
1086 1086 self._tmpname += b'-%s' % case
1087 1087
1088 1088 @property
1089 1089 def refpath(self):
1090 1090 return os.path.join(self._testdir, self.bname)
1091 1091
1092 1092 def _run(self, env):
1093 1093 f = open(self.path, 'rb')
1094 1094 lines = f.readlines()
1095 1095 f.close()
1096 1096
1097 1097 salt, script, after, expected = self._parsetest(lines)
1098 1098
1099 1099 # Write out the generated script.
1100 1100 fname = b'%s.sh' % self._testtmp
1101 1101 f = open(fname, 'wb')
1102 1102 for l in script:
1103 1103 f.write(l)
1104 1104 f.close()
1105 1105
1106 1106 cmd = b'%s "%s"' % (self._shell, fname)
1107 1107 vlog("# Running", cmd)
1108 1108
1109 1109 exitcode, output = self._runcommand(cmd, env)
1110 1110
1111 1111 if self._aborted:
1112 1112 raise KeyboardInterrupt()
1113 1113
1114 1114 # Do not merge output if skipped. Return hghave message instead.
1115 1115 # Similarly, with --debug, output is None.
1116 1116 if exitcode == self.SKIPPED_STATUS or output is None:
1117 1117 return exitcode, output
1118 1118
1119 1119 return self._processoutput(exitcode, output, salt, after, expected)
1120 1120
1121 1121 def _hghave(self, reqs):
1122 1122 # TODO do something smarter when all other uses of hghave are gone.
1123 1123 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
1124 1124 tdir = runtestdir.replace(b'\\', b'/')
1125 1125 proc = Popen4(b'%s -c "%s/hghave %s"' %
1126 1126 (self._shell, tdir, b' '.join(reqs)),
1127 1127 self._testtmp, 0, self._getenv())
1128 1128 stdout, stderr = proc.communicate()
1129 1129 ret = proc.wait()
1130 1130 if wifexited(ret):
1131 1131 ret = os.WEXITSTATUS(ret)
1132 1132 if ret == 2:
1133 1133 print(stdout.decode('utf-8'))
1134 1134 sys.exit(1)
1135 1135
1136 1136 if ret != 0:
1137 1137 return False, stdout
1138 1138
1139 1139 if 'slow' in reqs:
1140 1140 self._timeout = self._slowtimeout
1141 1141 return True, None
1142 1142
1143 1143 def _iftest(self, args):
1144 1144 # implements "#if"
1145 1145 reqs = []
1146 1146 for arg in args:
1147 1147 if arg.startswith(b'no-') and arg[3:] in self._allcases:
1148 1148 if arg[3:] == self._case:
1149 1149 return False
1150 1150 elif arg in self._allcases:
1151 1151 if arg != self._case:
1152 1152 return False
1153 1153 else:
1154 1154 reqs.append(arg)
1155 1155 return self._hghave(reqs)[0]
1156 1156
1157 1157 def _parsetest(self, lines):
1158 1158 # We generate a shell script which outputs unique markers to line
1159 1159 # up script results with our source. These markers include input
1160 1160 # line number and the last return code.
1161 1161 salt = b"SALT%d" % time.time()
1162 1162 def addsalt(line, inpython):
1163 1163 if inpython:
1164 1164 script.append(b'%s %d 0\n' % (salt, line))
1165 1165 else:
1166 1166 script.append(b'echo %s %d $?\n' % (salt, line))
1167 1167
1168 1168 script = []
1169 1169
1170 1170 # After we run the shell script, we re-unify the script output
1171 1171 # with non-active parts of the source, with synchronization by our
1172 1172 # SALT line number markers. The after table contains the non-active
1173 1173 # components, ordered by line number.
1174 1174 after = {}
1175 1175
1176 1176 # Expected shell script output.
1177 1177 expected = {}
1178 1178
1179 1179 pos = prepos = -1
1180 1180
1181 1181 # True or False when in a true or false conditional section
1182 1182 skipping = None
1183 1183
1184 1184 # We keep track of whether or not we're in a Python block so we
1185 1185 # can generate the surrounding doctest magic.
1186 1186 inpython = False
1187 1187
1188 1188 if self._debug:
1189 1189 script.append(b'set -x\n')
1190 1190 if self._hgcommand != b'hg':
1191 1191 script.append(b'alias hg="%s"\n' % self._hgcommand)
1192 1192 if os.getenv('MSYSTEM'):
1193 1193 script.append(b'alias pwd="pwd -W"\n')
1194 1194
1195 1195 n = 0
1196 1196 for n, l in enumerate(lines):
1197 1197 if not l.endswith(b'\n'):
1198 1198 l += b'\n'
1199 1199 if l.startswith(b'#require'):
1200 1200 lsplit = l.split()
1201 1201 if len(lsplit) < 2 or lsplit[0] != b'#require':
1202 1202 after.setdefault(pos, []).append(' !!! invalid #require\n')
1203 1203 haveresult, message = self._hghave(lsplit[1:])
1204 1204 if not haveresult:
1205 1205 script = [b'echo "%s"\nexit 80\n' % message]
1206 1206 break
1207 1207 after.setdefault(pos, []).append(l)
1208 1208 elif l.startswith(b'#if'):
1209 1209 lsplit = l.split()
1210 1210 if len(lsplit) < 2 or lsplit[0] != b'#if':
1211 1211 after.setdefault(pos, []).append(' !!! invalid #if\n')
1212 1212 if skipping is not None:
1213 1213 after.setdefault(pos, []).append(' !!! nested #if\n')
1214 1214 skipping = not self._iftest(lsplit[1:])
1215 1215 after.setdefault(pos, []).append(l)
1216 1216 elif l.startswith(b'#else'):
1217 1217 if skipping is None:
1218 1218 after.setdefault(pos, []).append(' !!! missing #if\n')
1219 1219 skipping = not skipping
1220 1220 after.setdefault(pos, []).append(l)
1221 1221 elif l.startswith(b'#endif'):
1222 1222 if skipping is None:
1223 1223 after.setdefault(pos, []).append(' !!! missing #if\n')
1224 1224 skipping = None
1225 1225 after.setdefault(pos, []).append(l)
1226 1226 elif skipping:
1227 1227 after.setdefault(pos, []).append(l)
1228 1228 elif l.startswith(b' >>> '): # python inlines
1229 1229 after.setdefault(pos, []).append(l)
1230 1230 prepos = pos
1231 1231 pos = n
1232 1232 if not inpython:
1233 1233 # We've just entered a Python block. Add the header.
1234 1234 inpython = True
1235 1235 addsalt(prepos, False) # Make sure we report the exit code.
1236 1236 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1237 1237 addsalt(n, True)
1238 1238 script.append(l[2:])
1239 1239 elif l.startswith(b' ... '): # python inlines
1240 1240 after.setdefault(prepos, []).append(l)
1241 1241 script.append(l[2:])
1242 1242 elif l.startswith(b' $ '): # commands
1243 1243 if inpython:
1244 1244 script.append(b'EOF\n')
1245 1245 inpython = False
1246 1246 after.setdefault(pos, []).append(l)
1247 1247 prepos = pos
1248 1248 pos = n
1249 1249 addsalt(n, False)
1250 1250 cmd = l[4:].split()
1251 1251 if len(cmd) == 2 and cmd[0] == b'cd':
1252 1252 l = b' $ cd %s || exit 1\n' % cmd[1]
1253 1253 script.append(l[4:])
1254 1254 elif l.startswith(b' > '): # continuations
1255 1255 after.setdefault(prepos, []).append(l)
1256 1256 script.append(l[4:])
1257 1257 elif l.startswith(b' '): # results
1258 1258 # Queue up a list of expected results.
1259 1259 expected.setdefault(pos, []).append(l[2:])
1260 1260 else:
1261 1261 if inpython:
1262 1262 script.append(b'EOF\n')
1263 1263 inpython = False
1264 1264 # Non-command/result. Queue up for merged output.
1265 1265 after.setdefault(pos, []).append(l)
1266 1266
1267 1267 if inpython:
1268 1268 script.append(b'EOF\n')
1269 1269 if skipping is not None:
1270 1270 after.setdefault(pos, []).append(' !!! missing #endif\n')
1271 1271 addsalt(n + 1, False)
1272 1272
1273 1273 return salt, script, after, expected
1274 1274
1275 1275 def _processoutput(self, exitcode, output, salt, after, expected):
1276 1276 # Merge the script output back into a unified test.
1277 1277 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1278 1278 if exitcode != 0:
1279 1279 warnonly = 3
1280 1280
1281 1281 pos = -1
1282 1282 postout = []
1283 1283 for l in output:
1284 1284 lout, lcmd = l, None
1285 1285 if salt in l:
1286 1286 lout, lcmd = l.split(salt, 1)
1287 1287
1288 1288 while lout:
1289 1289 if not lout.endswith(b'\n'):
1290 1290 lout += b' (no-eol)\n'
1291 1291
1292 1292 # Find the expected output at the current position.
1293 1293 els = [None]
1294 1294 if expected.get(pos, None):
1295 1295 els = expected[pos]
1296 1296
1297 1297 i = 0
1298 1298 optional = []
1299 1299 while i < len(els):
1300 1300 el = els[i]
1301 1301
1302 1302 r = TTest.linematch(el, lout)
1303 1303 if isinstance(r, str):
1304 1304 if r == '+glob':
1305 1305 lout = el[:-1] + ' (glob)\n'
1306 1306 r = '' # Warn only this line.
1307 1307 elif r == '-glob':
1308 1308 lout = ''.join(el.rsplit(' (glob)', 1))
1309 1309 r = '' # Warn only this line.
1310 1310 elif r == "retry":
1311 1311 postout.append(b' ' + el)
1312 1312 els.pop(i)
1313 1313 break
1314 1314 else:
1315 1315 log('\ninfo, unknown linematch result: %r\n' % r)
1316 1316 r = False
1317 1317 if r:
1318 1318 els.pop(i)
1319 1319 break
1320 1320 if el:
1321 1321 if el.endswith(b" (?)\n"):
1322 1322 optional.append(i)
1323 1323 else:
1324 1324 m = optline.match(el)
1325 1325 if m:
1326 1326 conditions = [c for c in m.group(2).split(' ')]
1327 1327
1328 1328 if self._hghave(conditions)[0]:
1329 1329 lout = el
1330 1330 else:
1331 1331 optional.append(i)
1332 1332
1333 1333 i += 1
1334 1334
1335 1335 if r:
1336 1336 if r == "retry":
1337 1337 continue
1338 1338 # clean up any optional leftovers
1339 1339 for i in optional:
1340 1340 postout.append(b' ' + els[i])
1341 1341 for i in reversed(optional):
1342 1342 del els[i]
1343 1343 postout.append(b' ' + el)
1344 1344 else:
1345 1345 if self.NEEDESCAPE(lout):
1346 1346 lout = TTest._stringescape(b'%s (esc)\n' %
1347 1347 lout.rstrip(b'\n'))
1348 1348 postout.append(b' ' + lout) # Let diff deal with it.
1349 1349 if r != '': # If line failed.
1350 1350 warnonly = 3 # for sure not
1351 1351 elif warnonly == 1: # Is "not yet" and line is warn only.
1352 1352 warnonly = 2 # Yes do warn.
1353 1353 break
1354 1354 else:
1355 1355 # clean up any optional leftovers
1356 1356 while expected.get(pos, None):
1357 1357 el = expected[pos].pop(0)
1358 1358 if el:
1359 1359 if (not optline.match(el)
1360 1360 and not el.endswith(b" (?)\n")):
1361 1361 break
1362 1362 postout.append(b' ' + el)
1363 1363
1364 1364 if lcmd:
1365 1365 # Add on last return code.
1366 1366 ret = int(lcmd.split()[1])
1367 1367 if ret != 0:
1368 1368 postout.append(b' [%d]\n' % ret)
1369 1369 if pos in after:
1370 1370 # Merge in non-active test bits.
1371 1371 postout += after.pop(pos)
1372 1372 pos = int(lcmd.split()[0])
1373 1373
1374 1374 if pos in after:
1375 1375 postout += after.pop(pos)
1376 1376
1377 1377 if warnonly == 2:
1378 1378 exitcode = False # Set exitcode to warned.
1379 1379
1380 1380 return exitcode, postout
1381 1381
1382 1382 @staticmethod
1383 1383 def rematch(el, l):
1384 1384 try:
1385 1385 # use \Z to ensure that the regex matches to the end of the string
1386 1386 if os.name == 'nt':
1387 1387 return re.match(el + br'\r?\n\Z', l)
1388 1388 return re.match(el + br'\n\Z', l)
1389 1389 except re.error:
1390 1390 # el is an invalid regex
1391 1391 return False
1392 1392
1393 1393 @staticmethod
1394 1394 def globmatch(el, l):
1395 1395 # The only supported special characters are * and ? plus / which also
1396 1396 # matches \ on windows. Escaping of these characters is supported.
1397 1397 if el + b'\n' == l:
1398 1398 if os.altsep:
1399 1399 # matching on "/" is not needed for this line
1400 1400 for pat in checkcodeglobpats:
1401 1401 if pat.match(el):
1402 1402 return True
1403 1403 return b'-glob'
1404 1404 return True
1405 1405 el = el.replace(b'$LOCALIP', b'*')
1406 1406 i, n = 0, len(el)
1407 1407 res = b''
1408 1408 while i < n:
1409 1409 c = el[i:i + 1]
1410 1410 i += 1
1411 1411 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1412 1412 res += el[i - 1:i + 1]
1413 1413 i += 1
1414 1414 elif c == b'*':
1415 1415 res += b'.*'
1416 1416 elif c == b'?':
1417 1417 res += b'.'
1418 1418 elif c == b'/' and os.altsep:
1419 1419 res += b'[/\\\\]'
1420 1420 else:
1421 1421 res += re.escape(c)
1422 1422 return TTest.rematch(res, l)
1423 1423
1424 1424 @staticmethod
1425 1425 def linematch(el, l):
1426 1426 retry = False
1427 1427 if el == l: # perfect match (fast)
1428 1428 return True
1429 1429 if el:
1430 1430 if el.endswith(b" (?)\n"):
1431 1431 retry = "retry"
1432 1432 el = el[:-5] + b"\n"
1433 1433 else:
1434 1434 m = optline.match(el)
1435 1435 if m:
1436 1436 el = m.group(1) + b"\n"
1437 1437 retry = "retry"
1438 1438
1439 1439 if el.endswith(b" (esc)\n"):
1440 1440 if PYTHON3:
1441 1441 el = el[:-7].decode('unicode_escape') + '\n'
1442 1442 el = el.encode('utf-8')
1443 1443 else:
1444 1444 el = el[:-7].decode('string-escape') + '\n'
1445 1445 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1446 1446 return True
1447 1447 if el.endswith(b" (re)\n"):
1448 1448 return TTest.rematch(el[:-6], l) or retry
1449 1449 if el.endswith(b" (glob)\n"):
1450 1450 # ignore '(glob)' added to l by 'replacements'
1451 1451 if l.endswith(b" (glob)\n"):
1452 1452 l = l[:-8] + b"\n"
1453 1453 return TTest.globmatch(el[:-8], l) or retry
1454 1454 if os.altsep and l.replace(b'\\', b'/') == el:
1455 1455 return b'+glob'
1456 1456 return retry
1457 1457
1458 1458 @staticmethod
1459 1459 def parsehghaveoutput(lines):
1460 1460 '''Parse hghave log lines.
1461 1461
1462 1462 Return tuple of lists (missing, failed):
1463 1463 * the missing/unknown features
1464 1464 * the features for which existence check failed'''
1465 1465 missing = []
1466 1466 failed = []
1467 1467 for line in lines:
1468 1468 if line.startswith(TTest.SKIPPED_PREFIX):
1469 1469 line = line.splitlines()[0]
1470 1470 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1471 1471 elif line.startswith(TTest.FAILED_PREFIX):
1472 1472 line = line.splitlines()[0]
1473 1473 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1474 1474
1475 1475 return missing, failed
1476 1476
1477 1477 @staticmethod
1478 1478 def _escapef(m):
1479 1479 return TTest.ESCAPEMAP[m.group(0)]
1480 1480
1481 1481 @staticmethod
1482 1482 def _stringescape(s):
1483 1483 return TTest.ESCAPESUB(TTest._escapef, s)
1484 1484
1485 1485 iolock = threading.RLock()
1486 1486
1487 1487 class SkipTest(Exception):
1488 1488 """Raised to indicate that a test is to be skipped."""
1489 1489
1490 1490 class IgnoreTest(Exception):
1491 1491 """Raised to indicate that a test is to be ignored."""
1492 1492
1493 1493 class WarnTest(Exception):
1494 1494 """Raised to indicate that a test warned."""
1495 1495
1496 1496 class ReportedTest(Exception):
1497 1497 """Raised to indicate that a test already reported."""
1498 1498
1499 1499 class TestResult(unittest._TextTestResult):
1500 1500 """Holds results when executing via unittest."""
1501 1501 # Don't worry too much about accessing the non-public _TextTestResult.
1502 1502 # It is relatively common in Python testing tools.
1503 1503 def __init__(self, options, *args, **kwargs):
1504 1504 super(TestResult, self).__init__(*args, **kwargs)
1505 1505
1506 1506 self._options = options
1507 1507
1508 1508 # unittest.TestResult didn't have skipped until 2.7. We need to
1509 1509 # polyfill it.
1510 1510 self.skipped = []
1511 1511
1512 1512 # We have a custom "ignored" result that isn't present in any Python
1513 1513 # unittest implementation. It is very similar to skipped. It may make
1514 1514 # sense to map it into skip some day.
1515 1515 self.ignored = []
1516 1516
1517 1517 # We have a custom "warned" result that isn't present in any Python
1518 1518 # unittest implementation. It is very similar to failed. It may make
1519 1519 # sense to map it into fail some day.
1520 1520 self.warned = []
1521 1521
1522 1522 self.times = []
1523 1523 self._firststarttime = None
1524 1524 # Data stored for the benefit of generating xunit reports.
1525 1525 self.successes = []
1526 1526 self.faildata = {}
1527 1527
1528 1528 def addFailure(self, test, reason):
1529 1529 self.failures.append((test, reason))
1530 1530
1531 1531 if self._options.first:
1532 1532 self.stop()
1533 1533 else:
1534 1534 with iolock:
1535 1535 if reason == "timed out":
1536 1536 self.stream.write('t')
1537 1537 else:
1538 1538 if not self._options.nodiff:
1539 1539 self.stream.write('\nERROR: %s output changed\n' % test)
1540 1540 self.stream.write('!')
1541 1541
1542 1542 self.stream.flush()
1543 1543
1544 1544 def addSuccess(self, test):
1545 1545 with iolock:
1546 1546 super(TestResult, self).addSuccess(test)
1547 1547 self.successes.append(test)
1548 1548
1549 1549 def addError(self, test, err):
1550 1550 super(TestResult, self).addError(test, err)
1551 1551 if self._options.first:
1552 1552 self.stop()
1553 1553
1554 1554 # Polyfill.
1555 1555 def addSkip(self, test, reason):
1556 1556 self.skipped.append((test, reason))
1557 1557 with iolock:
1558 1558 if self.showAll:
1559 1559 self.stream.writeln('skipped %s' % reason)
1560 1560 else:
1561 1561 self.stream.write('s')
1562 1562 self.stream.flush()
1563 1563
1564 1564 def addIgnore(self, test, reason):
1565 1565 self.ignored.append((test, reason))
1566 1566 with iolock:
1567 1567 if self.showAll:
1568 1568 self.stream.writeln('ignored %s' % reason)
1569 1569 else:
1570 1570 if reason not in ('not retesting', "doesn't match keyword"):
1571 1571 self.stream.write('i')
1572 1572 else:
1573 1573 self.testsRun += 1
1574 1574 self.stream.flush()
1575 1575
1576 1576 def addWarn(self, test, reason):
1577 1577 self.warned.append((test, reason))
1578 1578
1579 1579 if self._options.first:
1580 1580 self.stop()
1581 1581
1582 1582 with iolock:
1583 1583 if self.showAll:
1584 1584 self.stream.writeln('warned %s' % reason)
1585 1585 else:
1586 1586 self.stream.write('~')
1587 1587 self.stream.flush()
1588 1588
1589 1589 def addOutputMismatch(self, test, ret, got, expected):
1590 1590 """Record a mismatch in test output for a particular test."""
1591 1591 if self.shouldStop:
1592 1592 # don't print, some other test case already failed and
1593 1593 # printed, we're just stale and probably failed due to our
1594 1594 # temp dir getting cleaned up.
1595 1595 return
1596 1596
1597 1597 accepted = False
1598 1598 lines = []
1599 1599
1600 1600 with iolock:
1601 1601 if self._options.nodiff:
1602 1602 pass
1603 1603 elif self._options.view:
1604 1604 v = self._options.view
1605 1605 if PYTHON3:
1606 1606 v = _bytespath(v)
1607 1607 os.system(b"%s %s %s" %
1608 1608 (v, test.refpath, test.errpath))
1609 1609 else:
1610 1610 servefail, lines = getdiff(expected, got,
1611 1611 test.refpath, test.errpath)
1612 1612 if servefail:
1613 1613 self.addFailure(
1614 1614 test,
1615 1615 'server failed to start (HGPORT=%s)' % test._startport)
1616 1616 raise ReportedTest('server failed to start')
1617 1617 else:
1618 1618 self.stream.write('\n')
1619 1619 for line in lines:
1620 1620 if PYTHON3:
1621 1621 self.stream.flush()
1622 1622 self.stream.buffer.write(line)
1623 1623 self.stream.buffer.flush()
1624 1624 else:
1625 1625 self.stream.write(line)
1626 1626 self.stream.flush()
1627 1627
1628 1628 # handle interactive prompt without releasing iolock
1629 1629 if self._options.interactive:
1630 1630 self.stream.write('Accept this change? [n] ')
1631 1631 answer = sys.stdin.readline().strip()
1632 1632 if answer.lower() in ('y', 'yes'):
1633 1633 if test.name.endswith('.t'):
1634 1634 rename(test.errpath, test.path)
1635 1635 else:
1636 1636 rename(test.errpath, '%s.out' % test.path)
1637 1637 accepted = True
1638 1638 if not accepted:
1639 1639 self.faildata[test.name] = b''.join(lines)
1640 1640
1641 1641 return accepted
1642 1642
1643 1643 def startTest(self, test):
1644 1644 super(TestResult, self).startTest(test)
1645 1645
1646 1646 # os.times module computes the user time and system time spent by
1647 1647 # child's processes along with real elapsed time taken by a process.
1648 1648 # This module has one limitation. It can only work for Linux user
1649 1649 # and not for Windows.
1650 1650 test.started = os.times()
1651 1651 if self._firststarttime is None: # thread racy but irrelevant
1652 1652 self._firststarttime = test.started[4]
1653 1653
1654 1654 def stopTest(self, test, interrupted=False):
1655 1655 super(TestResult, self).stopTest(test)
1656 1656
1657 1657 test.stopped = os.times()
1658 1658
1659 1659 starttime = test.started
1660 1660 endtime = test.stopped
1661 1661 origin = self._firststarttime
1662 1662 self.times.append((test.name,
1663 1663 endtime[2] - starttime[2], # user space CPU time
1664 1664 endtime[3] - starttime[3], # sys space CPU time
1665 1665 endtime[4] - starttime[4], # real time
1666 1666 starttime[4] - origin, # start date in run context
1667 1667 endtime[4] - origin, # end date in run context
1668 1668 ))
1669 1669
1670 1670 if interrupted:
1671 1671 with iolock:
1672 1672 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1673 1673 test.name, self.times[-1][3]))
1674 1674
1675 1675 class TestSuite(unittest.TestSuite):
1676 1676 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1677 1677
1678 1678 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1679 1679 retest=False, keywords=None, loop=False, runs_per_test=1,
1680 1680 loadtest=None, showchannels=False,
1681 1681 *args, **kwargs):
1682 1682 """Create a new instance that can run tests with a configuration.
1683 1683
1684 1684 testdir specifies the directory where tests are executed from. This
1685 1685 is typically the ``tests`` directory from Mercurial's source
1686 1686 repository.
1687 1687
1688 1688 jobs specifies the number of jobs to run concurrently. Each test
1689 1689 executes on its own thread. Tests actually spawn new processes, so
1690 1690 state mutation should not be an issue.
1691 1691
1692 1692 If there is only one job, it will use the main thread.
1693 1693
1694 1694 whitelist and blacklist denote tests that have been whitelisted and
1695 1695 blacklisted, respectively. These arguments don't belong in TestSuite.
1696 1696 Instead, whitelist and blacklist should be handled by the thing that
1697 1697 populates the TestSuite with tests. They are present to preserve
1698 1698 backwards compatible behavior which reports skipped tests as part
1699 1699 of the results.
1700 1700
1701 1701 retest denotes whether to retest failed tests. This arguably belongs
1702 1702 outside of TestSuite.
1703 1703
1704 1704 keywords denotes key words that will be used to filter which tests
1705 1705 to execute. This arguably belongs outside of TestSuite.
1706 1706
1707 1707 loop denotes whether to loop over tests forever.
1708 1708 """
1709 1709 super(TestSuite, self).__init__(*args, **kwargs)
1710 1710
1711 1711 self._jobs = jobs
1712 1712 self._whitelist = whitelist
1713 1713 self._blacklist = blacklist
1714 1714 self._retest = retest
1715 1715 self._keywords = keywords
1716 1716 self._loop = loop
1717 1717 self._runs_per_test = runs_per_test
1718 1718 self._loadtest = loadtest
1719 1719 self._showchannels = showchannels
1720 1720
1721 1721 def run(self, result):
1722 1722 # We have a number of filters that need to be applied. We do this
1723 1723 # here instead of inside Test because it makes the running logic for
1724 1724 # Test simpler.
1725 1725 tests = []
1726 1726 num_tests = [0]
1727 1727 for test in self._tests:
1728 1728 def get():
1729 1729 num_tests[0] += 1
1730 1730 if getattr(test, 'should_reload', False):
1731 1731 return self._loadtest(test, num_tests[0])
1732 1732 return test
1733 1733 if not os.path.exists(test.path):
1734 1734 result.addSkip(test, "Doesn't exist")
1735 1735 continue
1736 1736
1737 1737 if not (self._whitelist and test.name in self._whitelist):
1738 1738 if self._blacklist and test.bname in self._blacklist:
1739 1739 result.addSkip(test, 'blacklisted')
1740 1740 continue
1741 1741
1742 1742 if self._retest and not os.path.exists(test.errpath):
1743 1743 result.addIgnore(test, 'not retesting')
1744 1744 continue
1745 1745
1746 1746 if self._keywords:
1747 1747 f = open(test.path, 'rb')
1748 1748 t = f.read().lower() + test.bname.lower()
1749 1749 f.close()
1750 1750 ignored = False
1751 1751 for k in self._keywords.lower().split():
1752 1752 if k not in t:
1753 1753 result.addIgnore(test, "doesn't match keyword")
1754 1754 ignored = True
1755 1755 break
1756 1756
1757 1757 if ignored:
1758 1758 continue
1759 1759 for _ in xrange(self._runs_per_test):
1760 1760 tests.append(get())
1761 1761
1762 1762 runtests = list(tests)
1763 1763 done = queue.Queue()
1764 1764 running = 0
1765 1765
1766 1766 channels = [""] * self._jobs
1767 1767
1768 1768 def job(test, result):
1769 1769 for n, v in enumerate(channels):
1770 1770 if not v:
1771 1771 channel = n
1772 1772 break
1773 1773 channels[channel] = "=" + test.name[5:].split(".")[0]
1774 1774 try:
1775 1775 test(result)
1776 1776 done.put(None)
1777 1777 except KeyboardInterrupt:
1778 1778 pass
1779 1779 except: # re-raises
1780 1780 done.put(('!', test, 'run-test raised an error, see traceback'))
1781 1781 raise
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 timesd = dict((t[0], t[3]) for t in result.times)
1937 1937 doc = minidom.Document()
1938 1938 s = doc.createElement('testsuite')
1939 1939 s.setAttribute('name', 'run-tests')
1940 1940 s.setAttribute('tests', str(result.testsRun))
1941 1941 s.setAttribute('errors', "0") # TODO
1942 1942 s.setAttribute('failures', str(failed))
1943 1943 s.setAttribute('skipped', str(skipped + ignored))
1944 1944 doc.appendChild(s)
1945 1945 for tc in result.successes:
1946 1946 t = doc.createElement('testcase')
1947 1947 t.setAttribute('name', tc.name)
1948 1948 t.setAttribute('time', '%.3f' % timesd[tc.name])
1949 1949 s.appendChild(t)
1950 1950 for tc, err in sorted(result.faildata.items()):
1951 1951 t = doc.createElement('testcase')
1952 1952 t.setAttribute('name', tc)
1953 1953 t.setAttribute('time', '%.3f' % timesd[tc])
1954 1954 # createCDATASection expects a unicode or it will
1955 1955 # convert using default conversion rules, which will
1956 1956 # fail if string isn't ASCII.
1957 1957 err = cdatasafe(err).decode('utf-8', 'replace')
1958 1958 cd = doc.createCDATASection(err)
1959 1959 t.appendChild(cd)
1960 1960 s.appendChild(t)
1961 1961 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1962 1962
1963 1963 if self._runner.options.json:
1964 1964 jsonpath = os.path.join(self._runner._testdir, b'report.json')
1965 1965 with open(jsonpath, 'w') as fp:
1966 1966 timesd = {}
1967 1967 for tdata in result.times:
1968 1968 test = tdata[0]
1969 1969 timesd[test] = tdata[1:]
1970 1970
1971 1971 outcome = {}
1972 1972 groups = [('success', ((tc, None)
1973 1973 for tc in result.successes)),
1974 1974 ('failure', result.failures),
1975 1975 ('skip', result.skipped)]
1976 1976 for res, testcases in groups:
1977 1977 for tc, __ in testcases:
1978 1978 if tc.name in timesd:
1979 1979 diff = result.faildata.get(tc.name, b'')
1980 1980 tres = {'result': res,
1981 1981 'time': ('%0.3f' % timesd[tc.name][2]),
1982 1982 'cuser': ('%0.3f' % timesd[tc.name][0]),
1983 1983 'csys': ('%0.3f' % timesd[tc.name][1]),
1984 1984 'start': ('%0.3f' % timesd[tc.name][3]),
1985 1985 'end': ('%0.3f' % timesd[tc.name][4]),
1986 1986 'diff': diff.decode('unicode_escape'),
1987 1987 }
1988 1988 else:
1989 1989 # blacklisted test
1990 1990 tres = {'result': res}
1991 1991
1992 1992 outcome[tc.name] = tres
1993 1993 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
1994 1994 separators=(',', ': '))
1995 1995 fp.writelines(("testreport =", jsonout))
1996 1996
1997 1997 self._runner._checkhglib('Tested')
1998 1998
1999 1999 savetimes(self._runner._testdir, result)
2000 2000
2001 2001 if failed and self._runner.options.known_good_rev:
2002 2002 def nooutput(args):
2003 2003 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2004 2004 stdout=subprocess.PIPE)
2005 2005 p.stdout.read()
2006 2006 p.wait()
2007 2007 for test, msg in result.failures:
2008 2008 nooutput(['hg', 'bisect', '--reset']),
2009 2009 nooutput(['hg', 'bisect', '--bad', '.'])
2010 2010 nooutput(['hg', 'bisect', '--good',
2011 2011 self._runner.options.known_good_rev])
2012 2012 # TODO: we probably need to forward some options
2013 2013 # that alter hg's behavior inside the tests.
2014 2014 rtc = '%s %s %s' % (sys.executable, sys.argv[0], test)
2015 2015 sub = subprocess.Popen(['hg', 'bisect', '--command', rtc],
2016 2016 stderr=subprocess.STDOUT,
2017 2017 stdout=subprocess.PIPE)
2018 2018 data = sub.stdout.read()
2019 2019 sub.wait()
2020 2020 m = re.search(
2021 2021 (r'\nThe first (?P<goodbad>bad|good) revision '
2022 2022 r'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2023 2023 r'summary: +(?P<summary>[^\n]+)\n'),
2024 2024 data, (re.MULTILINE | re.DOTALL))
2025 2025 if m is None:
2026 2026 self.stream.writeln(
2027 2027 'Failed to identify failure point for %s' % test)
2028 2028 continue
2029 2029 dat = m.groupdict()
2030 2030 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2031 2031 self.stream.writeln(
2032 2032 '%s %s by %s (%s)' % (
2033 2033 test, verb, dat['node'], dat['summary']))
2034 2034 self.stream.writeln(
2035 2035 '# Ran %d tests, %d skipped, %d warned, %d failed.'
2036 2036 % (result.testsRun,
2037 2037 skipped + ignored, warned, failed))
2038 2038 if failed:
2039 2039 self.stream.writeln('python hash seed: %s' %
2040 2040 os.environ['PYTHONHASHSEED'])
2041 2041 if self._runner.options.time:
2042 2042 self.printtimes(result.times)
2043 2043
2044 2044 return result
2045 2045
2046 2046 def printtimes(self, times):
2047 2047 # iolock held by run
2048 2048 self.stream.writeln('# Producing time report')
2049 2049 times.sort(key=lambda t: (t[3]))
2050 2050 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2051 2051 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2052 2052 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2053 2053 for tdata in times:
2054 2054 test = tdata[0]
2055 2055 cuser, csys, real, start, end = tdata[1:6]
2056 2056 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2057 2057
2058 2058 class TestRunner(object):
2059 2059 """Holds context for executing tests.
2060 2060
2061 2061 Tests rely on a lot of state. This object holds it for them.
2062 2062 """
2063 2063
2064 2064 # Programs required to run tests.
2065 2065 REQUIREDTOOLS = [
2066 2066 os.path.basename(_bytespath(sys.executable)),
2067 2067 b'diff',
2068 2068 b'grep',
2069 2069 b'unzip',
2070 2070 b'gunzip',
2071 2071 b'bunzip2',
2072 2072 b'sed',
2073 2073 ]
2074 2074
2075 2075 # Maps file extensions to test class.
2076 2076 TESTTYPES = [
2077 2077 (b'.py', PythonTest),
2078 2078 (b'.t', TTest),
2079 2079 ]
2080 2080
2081 2081 def __init__(self):
2082 2082 self.options = None
2083 2083 self._hgroot = None
2084 2084 self._testdir = None
2085 2085 self._hgtmp = None
2086 2086 self._installdir = None
2087 2087 self._bindir = None
2088 2088 self._tmpbinddir = None
2089 2089 self._pythondir = None
2090 2090 self._coveragefile = None
2091 2091 self._createdfiles = []
2092 2092 self._hgcommand = None
2093 2093 self._hgpath = None
2094 2094 self._portoffset = 0
2095 2095 self._ports = {}
2096 2096
2097 2097 def run(self, args, parser=None):
2098 2098 """Run the test suite."""
2099 2099 oldmask = os.umask(0o22)
2100 2100 try:
2101 2101 parser = parser or getparser()
2102 2102 options, args = parseargs(args, parser)
2103 2103 # positional arguments are paths to test files to run, so
2104 2104 # we make sure they're all bytestrings
2105 2105 args = [_bytespath(a) for a in args]
2106 2106 self.options = options
2107 2107
2108 2108 self._checktools()
2109 2109 testdescs = self.findtests(args)
2110 2110 if options.profile_runner:
2111 2111 import statprof
2112 2112 statprof.start()
2113 2113 result = self._run(testdescs)
2114 2114 if options.profile_runner:
2115 2115 statprof.stop()
2116 2116 statprof.display()
2117 2117 return result
2118 2118
2119 2119 finally:
2120 2120 os.umask(oldmask)
2121 2121
2122 2122 def _run(self, testdescs):
2123 2123 if self.options.random:
2124 2124 random.shuffle(testdescs)
2125 2125 else:
2126 2126 # keywords for slow tests
2127 2127 slow = {b'svn': 10,
2128 2128 b'cvs': 10,
2129 2129 b'hghave': 10,
2130 2130 b'largefiles-update': 10,
2131 2131 b'run-tests': 10,
2132 2132 b'corruption': 10,
2133 2133 b'race': 10,
2134 2134 b'i18n': 10,
2135 2135 b'check': 100,
2136 2136 b'gendoc': 100,
2137 2137 b'contrib-perf': 200,
2138 2138 }
2139 2139 perf = {}
2140 2140 def sortkey(f):
2141 2141 # run largest tests first, as they tend to take the longest
2142 2142 f = f['path']
2143 2143 try:
2144 2144 return perf[f]
2145 2145 except KeyError:
2146 2146 try:
2147 2147 val = -os.stat(f).st_size
2148 2148 except OSError as e:
2149 2149 if e.errno != errno.ENOENT:
2150 2150 raise
2151 2151 perf[f] = -1e9 # file does not exist, tell early
2152 2152 return -1e9
2153 2153 for kw, mul in slow.items():
2154 2154 if kw in f:
2155 2155 val *= mul
2156 2156 if f.endswith(b'.py'):
2157 2157 val /= 10.0
2158 2158 perf[f] = val / 1000.0
2159 2159 return perf[f]
2160 2160 testdescs.sort(key=sortkey)
2161 2161
2162 2162 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2163 2163 os, 'getcwdb', os.getcwd)()
2164 2164
2165 2165 if 'PYTHONHASHSEED' not in os.environ:
2166 2166 # use a random python hash seed all the time
2167 2167 # we do the randomness ourself to know what seed is used
2168 2168 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2169 2169
2170 2170 if self.options.tmpdir:
2171 2171 self.options.keep_tmpdir = True
2172 2172 tmpdir = _bytespath(self.options.tmpdir)
2173 2173 if os.path.exists(tmpdir):
2174 2174 # Meaning of tmpdir has changed since 1.3: we used to create
2175 2175 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2176 2176 # tmpdir already exists.
2177 2177 print("error: temp dir %r already exists" % tmpdir)
2178 2178 return 1
2179 2179
2180 2180 # Automatically removing tmpdir sounds convenient, but could
2181 2181 # really annoy anyone in the habit of using "--tmpdir=/tmp"
2182 2182 # or "--tmpdir=$HOME".
2183 2183 #vlog("# Removing temp dir", tmpdir)
2184 2184 #shutil.rmtree(tmpdir)
2185 2185 os.makedirs(tmpdir)
2186 2186 else:
2187 2187 d = None
2188 2188 if os.name == 'nt':
2189 2189 # without this, we get the default temp dir location, but
2190 2190 # in all lowercase, which causes troubles with paths (issue3490)
2191 2191 d = osenvironb.get(b'TMP', None)
2192 2192 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2193 2193
2194 2194 self._hgtmp = osenvironb[b'HGTMP'] = (
2195 2195 os.path.realpath(tmpdir))
2196 2196
2197 2197 if self.options.with_hg:
2198 2198 self._installdir = None
2199 2199 whg = self.options.with_hg
2200 2200 self._bindir = os.path.dirname(os.path.realpath(whg))
2201 2201 assert isinstance(self._bindir, bytes)
2202 2202 self._hgcommand = os.path.basename(whg)
2203 2203 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2204 2204 os.makedirs(self._tmpbindir)
2205 2205
2206 2206 # This looks redundant with how Python initializes sys.path from
2207 2207 # the location of the script being executed. Needed because the
2208 2208 # "hg" specified by --with-hg is not the only Python script
2209 2209 # executed in the test suite that needs to import 'mercurial'
2210 2210 # ... which means it's not really redundant at all.
2211 2211 self._pythondir = self._bindir
2212 2212 else:
2213 2213 self._installdir = os.path.join(self._hgtmp, b"install")
2214 2214 self._bindir = os.path.join(self._installdir, b"bin")
2215 2215 self._hgcommand = b'hg'
2216 2216 self._tmpbindir = self._bindir
2217 2217 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2218 2218
2219 2219 # set CHGHG, then replace "hg" command by "chg"
2220 2220 chgbindir = self._bindir
2221 2221 if self.options.chg or self.options.with_chg:
2222 2222 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2223 2223 else:
2224 2224 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2225 2225 if self.options.chg:
2226 2226 self._hgcommand = b'chg'
2227 2227 elif self.options.with_chg:
2228 2228 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2229 2229 self._hgcommand = os.path.basename(self.options.with_chg)
2230 2230
2231 2231 osenvironb[b"BINDIR"] = self._bindir
2232 2232 osenvironb[b"PYTHON"] = PYTHON
2233 2233
2234 2234 if self.options.with_python3:
2235 2235 osenvironb[b'PYTHON3'] = self.options.with_python3
2236 2236
2237 2237 fileb = _bytespath(__file__)
2238 2238 runtestdir = os.path.abspath(os.path.dirname(fileb))
2239 2239 osenvironb[b'RUNTESTDIR'] = runtestdir
2240 2240 if PYTHON3:
2241 2241 sepb = _bytespath(os.pathsep)
2242 2242 else:
2243 2243 sepb = os.pathsep
2244 2244 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2245 2245 if os.path.islink(__file__):
2246 2246 # test helper will likely be at the end of the symlink
2247 2247 realfile = os.path.realpath(fileb)
2248 2248 realdir = os.path.abspath(os.path.dirname(realfile))
2249 2249 path.insert(2, realdir)
2250 2250 if chgbindir != self._bindir:
2251 2251 path.insert(1, chgbindir)
2252 2252 if self._testdir != runtestdir:
2253 2253 path = [self._testdir] + path
2254 2254 if self._tmpbindir != self._bindir:
2255 2255 path = [self._tmpbindir] + path
2256 2256 osenvironb[b"PATH"] = sepb.join(path)
2257 2257
2258 2258 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2259 2259 # can run .../tests/run-tests.py test-foo where test-foo
2260 2260 # adds an extension to HGRC. Also include run-test.py directory to
2261 2261 # import modules like heredoctest.
2262 2262 pypath = [self._pythondir, self._testdir, runtestdir]
2263 2263 # We have to augment PYTHONPATH, rather than simply replacing
2264 2264 # it, in case external libraries are only available via current
2265 2265 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2266 2266 # are in /opt/subversion.)
2267 2267 oldpypath = osenvironb.get(IMPL_PATH)
2268 2268 if oldpypath:
2269 2269 pypath.append(oldpypath)
2270 2270 osenvironb[IMPL_PATH] = sepb.join(pypath)
2271 2271
2272 2272 if self.options.pure:
2273 2273 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2274 2274 os.environ["HGMODULEPOLICY"] = "py"
2275 2275
2276 2276 if self.options.allow_slow_tests:
2277 2277 os.environ["HGTEST_SLOW"] = "slow"
2278 2278 elif 'HGTEST_SLOW' in os.environ:
2279 2279 del os.environ['HGTEST_SLOW']
2280 2280
2281 2281 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2282 2282
2283 2283 vlog("# Using TESTDIR", self._testdir)
2284 2284 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2285 2285 vlog("# Using HGTMP", self._hgtmp)
2286 2286 vlog("# Using PATH", os.environ["PATH"])
2287 2287 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2288 2288
2289 2289 try:
2290 2290 return self._runtests(testdescs) or 0
2291 2291 finally:
2292 2292 time.sleep(.1)
2293 2293 self._cleanup()
2294 2294
2295 2295 def findtests(self, args):
2296 2296 """Finds possible test files from arguments.
2297 2297
2298 2298 If you wish to inject custom tests into the test harness, this would
2299 2299 be a good function to monkeypatch or override in a derived class.
2300 2300 """
2301 2301 if not args:
2302 2302 if self.options.changed:
2303 2303 proc = Popen4('hg st --rev "%s" -man0 .' %
2304 2304 self.options.changed, None, 0)
2305 2305 stdout, stderr = proc.communicate()
2306 2306 args = stdout.strip(b'\0').split(b'\0')
2307 2307 else:
2308 2308 args = os.listdir(b'.')
2309 2309
2310 2310 tests = []
2311 2311 for t in args:
2312 2312 if not (os.path.basename(t).startswith(b'test-')
2313 2313 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2314 2314 continue
2315 2315 if t.endswith(b'.t'):
2316 2316 # .t file may contain multiple test cases
2317 2317 cases = sorted(parsettestcases(t))
2318 2318 if cases:
2319 2319 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2320 2320 else:
2321 2321 tests.append({'path': t})
2322 2322 else:
2323 2323 tests.append({'path': t})
2324 2324 return tests
2325 2325
2326 2326 def _runtests(self, testdescs):
2327 2327 def _reloadtest(test, i):
2328 2328 # convert a test back to its description dict
2329 2329 desc = {'path': test.path}
2330 2330 case = getattr(test, '_case', None)
2331 2331 if case:
2332 2332 desc['case'] = case
2333 2333 return self._gettest(desc, i)
2334 2334
2335 2335 try:
2336 2336 if self._installdir:
2337 2337 self._installhg()
2338 2338 self._checkhglib("Testing")
2339 2339 else:
2340 2340 self._usecorrectpython()
2341 2341 if self.options.chg:
2342 2342 assert self._installdir
2343 2343 self._installchg()
2344 2344
2345 2345 if self.options.restart:
2346 2346 orig = list(testdescs)
2347 2347 while testdescs:
2348 2348 desc = testdescs[0]
2349 2349 if 'case' in desc:
2350 2350 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2351 2351 else:
2352 2352 errpath = b'%s.err' % desc['path']
2353 2353 if os.path.exists(errpath):
2354 2354 break
2355 2355 testdescs.pop(0)
2356 2356 if not testdescs:
2357 2357 print("running all tests")
2358 2358 testdescs = orig
2359 2359
2360 2360 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2361 2361
2362 2362 failed = False
2363 2363 warned = False
2364 2364 kws = self.options.keywords
2365 2365 if kws is not None and PYTHON3:
2366 2366 kws = kws.encode('utf-8')
2367 2367
2368 2368 suite = TestSuite(self._testdir,
2369 2369 jobs=self.options.jobs,
2370 2370 whitelist=self.options.whitelisted,
2371 2371 blacklist=self.options.blacklist,
2372 2372 retest=self.options.retest,
2373 2373 keywords=kws,
2374 2374 loop=self.options.loop,
2375 2375 runs_per_test=self.options.runs_per_test,
2376 2376 showchannels=self.options.showchannels,
2377 2377 tests=tests, loadtest=_reloadtest)
2378 2378 verbosity = 1
2379 2379 if self.options.verbose:
2380 2380 verbosity = 2
2381 2381 runner = TextTestRunner(self, verbosity=verbosity)
2382 2382 result = runner.run(suite)
2383 2383
2384 2384 if result.failures:
2385 2385 failed = True
2386 2386 if result.warned:
2387 2387 warned = True
2388 2388
2389 2389 if self.options.anycoverage:
2390 2390 self._outputcoverage()
2391 2391 except KeyboardInterrupt:
2392 2392 failed = True
2393 2393 print("\ninterrupted!")
2394 2394
2395 2395 if failed:
2396 2396 return 1
2397 2397 if warned:
2398 2398 return 80
2399 2399
2400 2400 def _getport(self, count):
2401 2401 port = self._ports.get(count) # do we have a cached entry?
2402 2402 if port is None:
2403 2403 portneeded = 3
2404 2404 # above 100 tries we just give up and let test reports failure
2405 2405 for tries in xrange(100):
2406 2406 allfree = True
2407 2407 port = self.options.port + self._portoffset
2408 2408 for idx in xrange(portneeded):
2409 2409 if not checkportisavailable(port + idx):
2410 2410 allfree = False
2411 2411 break
2412 2412 self._portoffset += portneeded
2413 2413 if allfree:
2414 2414 break
2415 2415 self._ports[count] = port
2416 2416 return port
2417 2417
2418 2418 def _gettest(self, testdesc, count):
2419 2419 """Obtain a Test by looking at its filename.
2420 2420
2421 2421 Returns a Test instance. The Test may not be runnable if it doesn't
2422 2422 map to a known type.
2423 2423 """
2424 2424 path = testdesc['path']
2425 2425 lctest = path.lower()
2426 2426 testcls = Test
2427 2427
2428 2428 for ext, cls in self.TESTTYPES:
2429 2429 if lctest.endswith(ext):
2430 2430 testcls = cls
2431 2431 break
2432 2432
2433 2433 refpath = os.path.join(self._testdir, path)
2434 2434 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2435 2435
2436 2436 # extra keyword parameters. 'case' is used by .t tests
2437 2437 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2438 2438
2439 2439 t = testcls(refpath, tmpdir,
2440 2440 keeptmpdir=self.options.keep_tmpdir,
2441 2441 debug=self.options.debug,
2442 2442 timeout=self.options.timeout,
2443 2443 startport=self._getport(count),
2444 2444 extraconfigopts=self.options.extra_config_opt,
2445 2445 py3kwarnings=self.options.py3k_warnings,
2446 2446 shell=self.options.shell,
2447 2447 hgcommand=self._hgcommand,
2448 2448 usechg=bool(self.options.with_chg or self.options.chg),
2449 2449 useipv6=useipv6, **kwds)
2450 2450 t.should_reload = True
2451 2451 return t
2452 2452
2453 2453 def _cleanup(self):
2454 2454 """Clean up state from this test invocation."""
2455 2455 if self.options.keep_tmpdir:
2456 2456 return
2457 2457
2458 2458 vlog("# Cleaning up HGTMP", self._hgtmp)
2459 2459 shutil.rmtree(self._hgtmp, True)
2460 2460 for f in self._createdfiles:
2461 2461 try:
2462 2462 os.remove(f)
2463 2463 except OSError:
2464 2464 pass
2465 2465
2466 2466 def _usecorrectpython(self):
2467 2467 """Configure the environment to use the appropriate Python in tests."""
2468 2468 # Tests must use the same interpreter as us or bad things will happen.
2469 2469 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2470 2470 if getattr(os, 'symlink', None):
2471 2471 vlog("# Making python executable in test path a symlink to '%s'" %
2472 2472 sys.executable)
2473 2473 mypython = os.path.join(self._tmpbindir, pyexename)
2474 2474 try:
2475 2475 if os.readlink(mypython) == sys.executable:
2476 2476 return
2477 2477 os.unlink(mypython)
2478 2478 except OSError as err:
2479 2479 if err.errno != errno.ENOENT:
2480 2480 raise
2481 2481 if self._findprogram(pyexename) != sys.executable:
2482 2482 try:
2483 2483 os.symlink(sys.executable, mypython)
2484 2484 self._createdfiles.append(mypython)
2485 2485 except OSError as err:
2486 2486 # child processes may race, which is harmless
2487 2487 if err.errno != errno.EEXIST:
2488 2488 raise
2489 2489 else:
2490 2490 exedir, exename = os.path.split(sys.executable)
2491 2491 vlog("# Modifying search path to find %s as %s in '%s'" %
2492 2492 (exename, pyexename, exedir))
2493 2493 path = os.environ['PATH'].split(os.pathsep)
2494 2494 while exedir in path:
2495 2495 path.remove(exedir)
2496 2496 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2497 2497 if not self._findprogram(pyexename):
2498 2498 print("WARNING: Cannot find %s in search path" % pyexename)
2499 2499
2500 2500 def _installhg(self):
2501 2501 """Install hg into the test environment.
2502 2502
2503 2503 This will also configure hg with the appropriate testing settings.
2504 2504 """
2505 2505 vlog("# Performing temporary installation of HG")
2506 2506 installerrs = os.path.join(self._hgtmp, b"install.err")
2507 2507 compiler = ''
2508 2508 if self.options.compiler:
2509 2509 compiler = '--compiler ' + self.options.compiler
2510 2510 if self.options.pure:
2511 2511 pure = b"--pure"
2512 2512 else:
2513 2513 pure = b""
2514 2514
2515 2515 # Run installer in hg root
2516 2516 script = os.path.realpath(sys.argv[0])
2517 2517 exe = sys.executable
2518 2518 if PYTHON3:
2519 2519 compiler = _bytespath(compiler)
2520 2520 script = _bytespath(script)
2521 2521 exe = _bytespath(exe)
2522 2522 hgroot = os.path.dirname(os.path.dirname(script))
2523 2523 self._hgroot = hgroot
2524 2524 os.chdir(hgroot)
2525 2525 nohome = b'--home=""'
2526 2526 if os.name == 'nt':
2527 2527 # The --home="" trick works only on OS where os.sep == '/'
2528 2528 # because of a distutils convert_path() fast-path. Avoid it at
2529 2529 # least on Windows for now, deal with .pydistutils.cfg bugs
2530 2530 # when they happen.
2531 2531 nohome = b''
2532 2532 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2533 2533 b' build %(compiler)s --build-base="%(base)s"'
2534 2534 b' install --force --prefix="%(prefix)s"'
2535 2535 b' --install-lib="%(libdir)s"'
2536 2536 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2537 2537 % {b'exe': exe, b'pure': pure,
2538 2538 b'compiler': compiler,
2539 2539 b'base': os.path.join(self._hgtmp, b"build"),
2540 2540 b'prefix': self._installdir, b'libdir': self._pythondir,
2541 2541 b'bindir': self._bindir,
2542 2542 b'nohome': nohome, b'logfile': installerrs})
2543 2543
2544 2544 # setuptools requires install directories to exist.
2545 2545 def makedirs(p):
2546 2546 try:
2547 2547 os.makedirs(p)
2548 2548 except OSError as e:
2549 2549 if e.errno != errno.EEXIST:
2550 2550 raise
2551 2551 makedirs(self._pythondir)
2552 2552 makedirs(self._bindir)
2553 2553
2554 2554 vlog("# Running", cmd)
2555 2555 if os.system(cmd) == 0:
2556 2556 if not self.options.verbose:
2557 2557 try:
2558 2558 os.remove(installerrs)
2559 2559 except OSError as e:
2560 2560 if e.errno != errno.ENOENT:
2561 2561 raise
2562 2562 else:
2563 2563 f = open(installerrs, 'rb')
2564 2564 for line in f:
2565 2565 if PYTHON3:
2566 2566 sys.stdout.buffer.write(line)
2567 2567 else:
2568 2568 sys.stdout.write(line)
2569 2569 f.close()
2570 2570 sys.exit(1)
2571 2571 os.chdir(self._testdir)
2572 2572
2573 2573 self._usecorrectpython()
2574 2574
2575 2575 if self.options.py3k_warnings and not self.options.anycoverage:
2576 2576 vlog("# Updating hg command to enable Py3k Warnings switch")
2577 2577 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2578 2578 lines = [line.rstrip() for line in f]
2579 2579 lines[0] += ' -3'
2580 2580 f.close()
2581 2581 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2582 2582 for line in lines:
2583 2583 f.write(line + '\n')
2584 2584 f.close()
2585 2585
2586 2586 hgbat = os.path.join(self._bindir, b'hg.bat')
2587 2587 if os.path.isfile(hgbat):
2588 2588 # hg.bat expects to be put in bin/scripts while run-tests.py
2589 2589 # installation layout put it in bin/ directly. Fix it
2590 2590 f = open(hgbat, 'rb')
2591 2591 data = f.read()
2592 2592 f.close()
2593 2593 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2594 2594 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2595 2595 b'"%~dp0python" "%~dp0hg" %*')
2596 2596 f = open(hgbat, 'wb')
2597 2597 f.write(data)
2598 2598 f.close()
2599 2599 else:
2600 2600 print('WARNING: cannot fix hg.bat reference to python.exe')
2601 2601
2602 2602 if self.options.anycoverage:
2603 2603 custom = os.path.join(self._testdir, 'sitecustomize.py')
2604 2604 target = os.path.join(self._pythondir, 'sitecustomize.py')
2605 2605 vlog('# Installing coverage trigger to %s' % target)
2606 2606 shutil.copyfile(custom, target)
2607 2607 rc = os.path.join(self._testdir, '.coveragerc')
2608 2608 vlog('# Installing coverage rc to %s' % rc)
2609 2609 os.environ['COVERAGE_PROCESS_START'] = rc
2610 2610 covdir = os.path.join(self._installdir, '..', 'coverage')
2611 2611 try:
2612 2612 os.mkdir(covdir)
2613 2613 except OSError as e:
2614 2614 if e.errno != errno.EEXIST:
2615 2615 raise
2616 2616
2617 2617 os.environ['COVERAGE_DIR'] = covdir
2618 2618
2619 2619 def _checkhglib(self, verb):
2620 2620 """Ensure that the 'mercurial' package imported by python is
2621 2621 the one we expect it to be. If not, print a warning to stderr."""
2622 2622 if ((self._bindir == self._pythondir) and
2623 2623 (self._bindir != self._tmpbindir)):
2624 2624 # The pythondir has been inferred from --with-hg flag.
2625 2625 # We cannot expect anything sensible here.
2626 2626 return
2627 2627 expecthg = os.path.join(self._pythondir, b'mercurial')
2628 2628 actualhg = self._gethgpath()
2629 2629 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2630 2630 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2631 2631 ' (expected %s)\n'
2632 2632 % (verb, actualhg, expecthg))
2633 2633 def _gethgpath(self):
2634 2634 """Return the path to the mercurial package that is actually found by
2635 2635 the current Python interpreter."""
2636 2636 if self._hgpath is not None:
2637 2637 return self._hgpath
2638 2638
2639 2639 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2640 2640 cmd = cmd % PYTHON
2641 2641 if PYTHON3:
2642 2642 cmd = _strpath(cmd)
2643 2643 pipe = os.popen(cmd)
2644 2644 try:
2645 2645 self._hgpath = _bytespath(pipe.read().strip())
2646 2646 finally:
2647 2647 pipe.close()
2648 2648
2649 2649 return self._hgpath
2650 2650
2651 2651 def _installchg(self):
2652 2652 """Install chg into the test environment"""
2653 2653 vlog('# Performing temporary installation of CHG')
2654 2654 assert os.path.dirname(self._bindir) == self._installdir
2655 2655 assert self._hgroot, 'must be called after _installhg()'
2656 2656 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2657 2657 % {b'make': 'make', # TODO: switch by option or environment?
2658 2658 b'prefix': self._installdir})
2659 2659 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2660 2660 vlog("# Running", cmd)
2661 2661 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2662 2662 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2663 2663 stderr=subprocess.STDOUT)
2664 2664 out, _err = proc.communicate()
2665 2665 if proc.returncode != 0:
2666 2666 if PYTHON3:
2667 2667 sys.stdout.buffer.write(out)
2668 2668 else:
2669 2669 sys.stdout.write(out)
2670 2670 sys.exit(1)
2671 2671
2672 2672 def _outputcoverage(self):
2673 2673 """Produce code coverage output."""
2674 2674 import coverage
2675 2675 coverage = coverage.coverage
2676 2676
2677 2677 vlog('# Producing coverage report')
2678 2678 # chdir is the easiest way to get short, relative paths in the
2679 2679 # output.
2680 2680 os.chdir(self._hgroot)
2681 2681 covdir = os.path.join(self._installdir, '..', 'coverage')
2682 2682 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2683 2683
2684 2684 # Map install directory paths back to source directory.
2685 2685 cov.config.paths['srcdir'] = ['.', self._pythondir]
2686 2686
2687 2687 cov.combine()
2688 2688
2689 2689 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2690 2690 cov.report(ignore_errors=True, omit=omit)
2691 2691
2692 2692 if self.options.htmlcov:
2693 2693 htmldir = os.path.join(self._testdir, 'htmlcov')
2694 2694 cov.html_report(directory=htmldir, omit=omit)
2695 2695 if self.options.annotate:
2696 2696 adir = os.path.join(self._testdir, 'annotated')
2697 2697 if not os.path.isdir(adir):
2698 2698 os.mkdir(adir)
2699 2699 cov.annotate(directory=adir, omit=omit)
2700 2700
2701 2701 def _findprogram(self, program):
2702 2702 """Search PATH for a executable program"""
2703 2703 dpb = _bytespath(os.defpath)
2704 2704 sepb = _bytespath(os.pathsep)
2705 2705 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2706 2706 name = os.path.join(p, program)
2707 2707 if os.name == 'nt' or os.access(name, os.X_OK):
2708 2708 return name
2709 2709 return None
2710 2710
2711 2711 def _checktools(self):
2712 2712 """Ensure tools required to run tests are present."""
2713 2713 for p in self.REQUIREDTOOLS:
2714 2714 if os.name == 'nt' and not p.endswith('.exe'):
2715 2715 p += '.exe'
2716 2716 found = self._findprogram(p)
2717 2717 if found:
2718 2718 vlog("# Found prerequisite", p, "at", found)
2719 2719 else:
2720 2720 print("WARNING: Did not find prerequisite tool: %s " %
2721 2721 p.decode("utf-8"))
2722 2722
2723 2723 if __name__ == '__main__':
2724 2724 runner = TestRunner()
2725 2725
2726 2726 try:
2727 2727 import msvcrt
2728 2728 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2729 2729 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2730 2730 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2731 2731 except ImportError:
2732 2732 pass
2733 2733
2734 2734 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now