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