##// END OF EJS Templates
run-tests: fix regular expression for path test...
Gregory Szorc -
r35618:31acf661 default
parent child Browse files
Show More
@@ -1,3023 +1,3023
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 if self._case:
1338 1338 script.append(b'TESTCASE=%s\n' % shellquote(self._case))
1339 1339 script.append(b'export TESTCASE\n')
1340 1340
1341 1341 n = 0
1342 1342 for n, l in enumerate(lines):
1343 1343 if not l.endswith(b'\n'):
1344 1344 l += b'\n'
1345 1345 if l.startswith(b'#require'):
1346 1346 lsplit = l.split()
1347 1347 if len(lsplit) < 2 or lsplit[0] != b'#require':
1348 1348 after.setdefault(pos, []).append(' !!! invalid #require\n')
1349 1349 haveresult, message = self._hghave(lsplit[1:])
1350 1350 if not haveresult:
1351 1351 script = [b'echo "%s"\nexit 80\n' % message]
1352 1352 break
1353 1353 after.setdefault(pos, []).append(l)
1354 1354 elif l.startswith(b'#if'):
1355 1355 lsplit = l.split()
1356 1356 if len(lsplit) < 2 or lsplit[0] != b'#if':
1357 1357 after.setdefault(pos, []).append(' !!! invalid #if\n')
1358 1358 if skipping is not None:
1359 1359 after.setdefault(pos, []).append(' !!! nested #if\n')
1360 1360 skipping = not self._iftest(lsplit[1:])
1361 1361 after.setdefault(pos, []).append(l)
1362 1362 elif l.startswith(b'#else'):
1363 1363 if skipping is None:
1364 1364 after.setdefault(pos, []).append(' !!! missing #if\n')
1365 1365 skipping = not skipping
1366 1366 after.setdefault(pos, []).append(l)
1367 1367 elif l.startswith(b'#endif'):
1368 1368 if skipping is None:
1369 1369 after.setdefault(pos, []).append(' !!! missing #if\n')
1370 1370 skipping = None
1371 1371 after.setdefault(pos, []).append(l)
1372 1372 elif skipping:
1373 1373 after.setdefault(pos, []).append(l)
1374 1374 elif l.startswith(b' >>> '): # python inlines
1375 1375 after.setdefault(pos, []).append(l)
1376 1376 prepos = pos
1377 1377 pos = n
1378 1378 if not inpython:
1379 1379 # We've just entered a Python block. Add the header.
1380 1380 inpython = True
1381 1381 addsalt(prepos, False) # Make sure we report the exit code.
1382 1382 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1383 1383 addsalt(n, True)
1384 1384 script.append(l[2:])
1385 1385 elif l.startswith(b' ... '): # python inlines
1386 1386 after.setdefault(prepos, []).append(l)
1387 1387 script.append(l[2:])
1388 1388 elif l.startswith(b' $ '): # commands
1389 1389 if inpython:
1390 1390 script.append(b'EOF\n')
1391 1391 inpython = False
1392 1392 after.setdefault(pos, []).append(l)
1393 1393 prepos = pos
1394 1394 pos = n
1395 1395 addsalt(n, False)
1396 1396 cmd = l[4:].split()
1397 1397 if len(cmd) == 2 and cmd[0] == b'cd':
1398 1398 l = b' $ cd %s || exit 1\n' % cmd[1]
1399 1399 script.append(l[4:])
1400 1400 elif l.startswith(b' > '): # continuations
1401 1401 after.setdefault(prepos, []).append(l)
1402 1402 script.append(l[4:])
1403 1403 elif l.startswith(b' '): # results
1404 1404 # Queue up a list of expected results.
1405 1405 expected.setdefault(pos, []).append(l[2:])
1406 1406 else:
1407 1407 if inpython:
1408 1408 script.append(b'EOF\n')
1409 1409 inpython = False
1410 1410 # Non-command/result. Queue up for merged output.
1411 1411 after.setdefault(pos, []).append(l)
1412 1412
1413 1413 if inpython:
1414 1414 script.append(b'EOF\n')
1415 1415 if skipping is not None:
1416 1416 after.setdefault(pos, []).append(' !!! missing #endif\n')
1417 1417 addsalt(n + 1, False)
1418 1418
1419 1419 return salt, script, after, expected
1420 1420
1421 1421 def _processoutput(self, exitcode, output, salt, after, expected):
1422 1422 # Merge the script output back into a unified test.
1423 1423 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1424 1424 if exitcode != 0:
1425 1425 warnonly = 3
1426 1426
1427 1427 pos = -1
1428 1428 postout = []
1429 1429 for l in output:
1430 1430 lout, lcmd = l, None
1431 1431 if salt in l:
1432 1432 lout, lcmd = l.split(salt, 1)
1433 1433
1434 1434 while lout:
1435 1435 if not lout.endswith(b'\n'):
1436 1436 lout += b' (no-eol)\n'
1437 1437
1438 1438 # Find the expected output at the current position.
1439 1439 els = [None]
1440 1440 if expected.get(pos, None):
1441 1441 els = expected[pos]
1442 1442
1443 1443 i = 0
1444 1444 optional = []
1445 1445 while i < len(els):
1446 1446 el = els[i]
1447 1447
1448 1448 r = self.linematch(el, lout)
1449 1449 if isinstance(r, str):
1450 1450 if r == '-glob':
1451 1451 lout = ''.join(el.rsplit(' (glob)', 1))
1452 1452 r = '' # Warn only this line.
1453 1453 elif r == "retry":
1454 1454 postout.append(b' ' + el)
1455 1455 els.pop(i)
1456 1456 break
1457 1457 else:
1458 1458 log('\ninfo, unknown linematch result: %r\n' % r)
1459 1459 r = False
1460 1460 if r:
1461 1461 els.pop(i)
1462 1462 break
1463 1463 if el:
1464 1464 if el.endswith(b" (?)\n"):
1465 1465 optional.append(i)
1466 1466 else:
1467 1467 m = optline.match(el)
1468 1468 if m:
1469 1469 conditions = [
1470 1470 c for c in m.group(2).split(b' ')]
1471 1471
1472 1472 if not self._iftest(conditions):
1473 1473 optional.append(i)
1474 1474
1475 1475 i += 1
1476 1476
1477 1477 if r:
1478 1478 if r == "retry":
1479 1479 continue
1480 1480 # clean up any optional leftovers
1481 1481 for i in optional:
1482 1482 postout.append(b' ' + els[i])
1483 1483 for i in reversed(optional):
1484 1484 del els[i]
1485 1485 postout.append(b' ' + el)
1486 1486 else:
1487 1487 if self.NEEDESCAPE(lout):
1488 1488 lout = TTest._stringescape(b'%s (esc)\n' %
1489 1489 lout.rstrip(b'\n'))
1490 1490 postout.append(b' ' + lout) # Let diff deal with it.
1491 1491 if r != '': # If line failed.
1492 1492 warnonly = 3 # for sure not
1493 1493 elif warnonly == 1: # Is "not yet" and line is warn only.
1494 1494 warnonly = 2 # Yes do warn.
1495 1495 break
1496 1496 else:
1497 1497 # clean up any optional leftovers
1498 1498 while expected.get(pos, None):
1499 1499 el = expected[pos].pop(0)
1500 1500 if el:
1501 1501 if not el.endswith(b" (?)\n"):
1502 1502 m = optline.match(el)
1503 1503 if m:
1504 1504 conditions = [c for c in m.group(2).split(b' ')]
1505 1505
1506 1506 if self._iftest(conditions):
1507 1507 # Don't append as optional line
1508 1508 continue
1509 1509 else:
1510 1510 continue
1511 1511 postout.append(b' ' + el)
1512 1512
1513 1513 if lcmd:
1514 1514 # Add on last return code.
1515 1515 ret = int(lcmd.split()[1])
1516 1516 if ret != 0:
1517 1517 postout.append(b' [%d]\n' % ret)
1518 1518 if pos in after:
1519 1519 # Merge in non-active test bits.
1520 1520 postout += after.pop(pos)
1521 1521 pos = int(lcmd.split()[0])
1522 1522
1523 1523 if pos in after:
1524 1524 postout += after.pop(pos)
1525 1525
1526 1526 if warnonly == 2:
1527 1527 exitcode = False # Set exitcode to warned.
1528 1528
1529 1529 return exitcode, postout
1530 1530
1531 1531 @staticmethod
1532 1532 def rematch(el, l):
1533 1533 try:
1534 1534 el = b'(?:' + el + b')'
1535 1535 # use \Z to ensure that the regex matches to the end of the string
1536 1536 if os.name == 'nt':
1537 1537 return re.match(el + br'\r?\n\Z', l)
1538 1538 return re.match(el + br'\n\Z', l)
1539 1539 except re.error:
1540 1540 # el is an invalid regex
1541 1541 return False
1542 1542
1543 1543 @staticmethod
1544 1544 def globmatch(el, l):
1545 1545 # The only supported special characters are * and ? plus / which also
1546 1546 # matches \ on windows. Escaping of these characters is supported.
1547 1547 if el + b'\n' == l:
1548 1548 if os.altsep:
1549 1549 # matching on "/" is not needed for this line
1550 1550 for pat in checkcodeglobpats:
1551 1551 if pat.match(el):
1552 1552 return True
1553 1553 return b'-glob'
1554 1554 return True
1555 1555 el = el.replace(b'$LOCALIP', b'*')
1556 1556 i, n = 0, len(el)
1557 1557 res = b''
1558 1558 while i < n:
1559 1559 c = el[i:i + 1]
1560 1560 i += 1
1561 1561 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1562 1562 res += el[i - 1:i + 1]
1563 1563 i += 1
1564 1564 elif c == b'*':
1565 1565 res += b'.*'
1566 1566 elif c == b'?':
1567 1567 res += b'.'
1568 1568 elif c == b'/' and os.altsep:
1569 1569 res += b'[/\\\\]'
1570 1570 else:
1571 1571 res += re.escape(c)
1572 1572 return TTest.rematch(res, l)
1573 1573
1574 1574 def linematch(self, el, l):
1575 1575 retry = False
1576 1576 if el == l: # perfect match (fast)
1577 1577 return True
1578 1578 if el:
1579 1579 if el.endswith(b" (?)\n"):
1580 1580 retry = "retry"
1581 1581 el = el[:-5] + b"\n"
1582 1582 else:
1583 1583 m = optline.match(el)
1584 1584 if m:
1585 1585 conditions = [c for c in m.group(2).split(b' ')]
1586 1586
1587 1587 el = m.group(1) + b"\n"
1588 1588 if not self._iftest(conditions):
1589 1589 retry = "retry" # Not required by listed features
1590 1590
1591 1591 if el.endswith(b" (esc)\n"):
1592 1592 if PYTHON3:
1593 1593 el = el[:-7].decode('unicode_escape') + '\n'
1594 1594 el = el.encode('utf-8')
1595 1595 else:
1596 1596 el = el[:-7].decode('string-escape') + '\n'
1597 1597 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1598 1598 return True
1599 1599 if el.endswith(b" (re)\n"):
1600 1600 return TTest.rematch(el[:-6], l) or retry
1601 1601 if el.endswith(b" (glob)\n"):
1602 1602 # ignore '(glob)' added to l by 'replacements'
1603 1603 if l.endswith(b" (glob)\n"):
1604 1604 l = l[:-8] + b"\n"
1605 1605 return TTest.globmatch(el[:-8], l) or retry
1606 1606 if os.altsep:
1607 1607 _l = l.replace(b'\\', b'/')
1608 1608 if el == _l or os.name == 'nt' and el[:-1] + b'\r\n' == _l:
1609 1609 return True
1610 1610 return retry
1611 1611
1612 1612 @staticmethod
1613 1613 def parsehghaveoutput(lines):
1614 1614 '''Parse hghave log lines.
1615 1615
1616 1616 Return tuple of lists (missing, failed):
1617 1617 * the missing/unknown features
1618 1618 * the features for which existence check failed'''
1619 1619 missing = []
1620 1620 failed = []
1621 1621 for line in lines:
1622 1622 if line.startswith(TTest.SKIPPED_PREFIX):
1623 1623 line = line.splitlines()[0]
1624 1624 missing.append(line[len(TTest.SKIPPED_PREFIX):].decode('utf-8'))
1625 1625 elif line.startswith(TTest.FAILED_PREFIX):
1626 1626 line = line.splitlines()[0]
1627 1627 failed.append(line[len(TTest.FAILED_PREFIX):].decode('utf-8'))
1628 1628
1629 1629 return missing, failed
1630 1630
1631 1631 @staticmethod
1632 1632 def _escapef(m):
1633 1633 return TTest.ESCAPEMAP[m.group(0)]
1634 1634
1635 1635 @staticmethod
1636 1636 def _stringescape(s):
1637 1637 return TTest.ESCAPESUB(TTest._escapef, s)
1638 1638
1639 1639 iolock = threading.RLock()
1640 1640
1641 1641 class TestResult(unittest._TextTestResult):
1642 1642 """Holds results when executing via unittest."""
1643 1643 # Don't worry too much about accessing the non-public _TextTestResult.
1644 1644 # It is relatively common in Python testing tools.
1645 1645 def __init__(self, options, *args, **kwargs):
1646 1646 super(TestResult, self).__init__(*args, **kwargs)
1647 1647
1648 1648 self._options = options
1649 1649
1650 1650 # unittest.TestResult didn't have skipped until 2.7. We need to
1651 1651 # polyfill it.
1652 1652 self.skipped = []
1653 1653
1654 1654 # We have a custom "ignored" result that isn't present in any Python
1655 1655 # unittest implementation. It is very similar to skipped. It may make
1656 1656 # sense to map it into skip some day.
1657 1657 self.ignored = []
1658 1658
1659 1659 self.times = []
1660 1660 self._firststarttime = None
1661 1661 # Data stored for the benefit of generating xunit reports.
1662 1662 self.successes = []
1663 1663 self.faildata = {}
1664 1664
1665 1665 if options.color == 'auto':
1666 1666 self.color = pygmentspresent and self.stream.isatty()
1667 1667 elif options.color == 'never':
1668 1668 self.color = False
1669 1669 else: # 'always', for testing purposes
1670 1670 self.color = pygmentspresent
1671 1671
1672 1672 def addFailure(self, test, reason):
1673 1673 self.failures.append((test, reason))
1674 1674
1675 1675 if self._options.first:
1676 1676 self.stop()
1677 1677 else:
1678 1678 with iolock:
1679 1679 if reason == "timed out":
1680 1680 self.stream.write('t')
1681 1681 else:
1682 1682 if not self._options.nodiff:
1683 1683 self.stream.write('\n')
1684 1684 # Exclude the '\n' from highlighting to lex correctly
1685 1685 formatted = 'ERROR: %s output changed\n' % test
1686 1686 self.stream.write(highlightmsg(formatted, self.color))
1687 1687 self.stream.write('!')
1688 1688
1689 1689 self.stream.flush()
1690 1690
1691 1691 def addSuccess(self, test):
1692 1692 with iolock:
1693 1693 super(TestResult, self).addSuccess(test)
1694 1694 self.successes.append(test)
1695 1695
1696 1696 def addError(self, test, err):
1697 1697 super(TestResult, self).addError(test, err)
1698 1698 if self._options.first:
1699 1699 self.stop()
1700 1700
1701 1701 # Polyfill.
1702 1702 def addSkip(self, test, reason):
1703 1703 self.skipped.append((test, reason))
1704 1704 with iolock:
1705 1705 if self.showAll:
1706 1706 self.stream.writeln('skipped %s' % reason)
1707 1707 else:
1708 1708 self.stream.write('s')
1709 1709 self.stream.flush()
1710 1710
1711 1711 def addIgnore(self, test, reason):
1712 1712 self.ignored.append((test, reason))
1713 1713 with iolock:
1714 1714 if self.showAll:
1715 1715 self.stream.writeln('ignored %s' % reason)
1716 1716 else:
1717 1717 if reason not in ('not retesting', "doesn't match keyword"):
1718 1718 self.stream.write('i')
1719 1719 else:
1720 1720 self.testsRun += 1
1721 1721 self.stream.flush()
1722 1722
1723 1723 def addOutputMismatch(self, test, ret, got, expected):
1724 1724 """Record a mismatch in test output for a particular test."""
1725 1725 if self.shouldStop:
1726 1726 # don't print, some other test case already failed and
1727 1727 # printed, we're just stale and probably failed due to our
1728 1728 # temp dir getting cleaned up.
1729 1729 return
1730 1730
1731 1731 accepted = False
1732 1732 lines = []
1733 1733
1734 1734 with iolock:
1735 1735 if self._options.nodiff:
1736 1736 pass
1737 1737 elif self._options.view:
1738 1738 v = self._options.view
1739 1739 if PYTHON3:
1740 1740 v = _bytespath(v)
1741 1741 os.system(b"%s %s %s" %
1742 1742 (v, test.refpath, test.errpath))
1743 1743 else:
1744 1744 servefail, lines = getdiff(expected, got,
1745 1745 test.refpath, test.errpath)
1746 1746 if servefail:
1747 1747 raise test.failureException(
1748 1748 'server failed to start (HGPORT=%s)' % test._startport)
1749 1749 else:
1750 1750 self.stream.write('\n')
1751 1751 for line in lines:
1752 1752 line = highlightdiff(line, self.color)
1753 1753 if PYTHON3:
1754 1754 self.stream.flush()
1755 1755 self.stream.buffer.write(line)
1756 1756 self.stream.buffer.flush()
1757 1757 else:
1758 1758 self.stream.write(line)
1759 1759 self.stream.flush()
1760 1760
1761 1761 # handle interactive prompt without releasing iolock
1762 1762 if self._options.interactive:
1763 1763 if test.readrefout() != expected:
1764 1764 self.stream.write(
1765 1765 'Reference output has changed (run again to prompt '
1766 1766 'changes)')
1767 1767 else:
1768 1768 self.stream.write('Accept this change? [n] ')
1769 1769 answer = sys.stdin.readline().strip()
1770 1770 if answer.lower() in ('y', 'yes'):
1771 1771 if test.path.endswith(b'.t'):
1772 1772 rename(test.errpath, test.path)
1773 1773 else:
1774 1774 rename(test.errpath, '%s.out' % test.path)
1775 1775 accepted = True
1776 1776 if not accepted:
1777 1777 self.faildata[test.name] = b''.join(lines)
1778 1778
1779 1779 return accepted
1780 1780
1781 1781 def startTest(self, test):
1782 1782 super(TestResult, self).startTest(test)
1783 1783
1784 1784 # os.times module computes the user time and system time spent by
1785 1785 # child's processes along with real elapsed time taken by a process.
1786 1786 # This module has one limitation. It can only work for Linux user
1787 1787 # and not for Windows.
1788 1788 test.started = os.times()
1789 1789 if self._firststarttime is None: # thread racy but irrelevant
1790 1790 self._firststarttime = test.started[4]
1791 1791
1792 1792 def stopTest(self, test, interrupted=False):
1793 1793 super(TestResult, self).stopTest(test)
1794 1794
1795 1795 test.stopped = os.times()
1796 1796
1797 1797 starttime = test.started
1798 1798 endtime = test.stopped
1799 1799 origin = self._firststarttime
1800 1800 self.times.append((test.name,
1801 1801 endtime[2] - starttime[2], # user space CPU time
1802 1802 endtime[3] - starttime[3], # sys space CPU time
1803 1803 endtime[4] - starttime[4], # real time
1804 1804 starttime[4] - origin, # start date in run context
1805 1805 endtime[4] - origin, # end date in run context
1806 1806 ))
1807 1807
1808 1808 if interrupted:
1809 1809 with iolock:
1810 1810 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1811 1811 test.name, self.times[-1][3]))
1812 1812
1813 1813 class TestSuite(unittest.TestSuite):
1814 1814 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1815 1815
1816 1816 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1817 1817 retest=False, keywords=None, loop=False, runs_per_test=1,
1818 1818 loadtest=None, showchannels=False,
1819 1819 *args, **kwargs):
1820 1820 """Create a new instance that can run tests with a configuration.
1821 1821
1822 1822 testdir specifies the directory where tests are executed from. This
1823 1823 is typically the ``tests`` directory from Mercurial's source
1824 1824 repository.
1825 1825
1826 1826 jobs specifies the number of jobs to run concurrently. Each test
1827 1827 executes on its own thread. Tests actually spawn new processes, so
1828 1828 state mutation should not be an issue.
1829 1829
1830 1830 If there is only one job, it will use the main thread.
1831 1831
1832 1832 whitelist and blacklist denote tests that have been whitelisted and
1833 1833 blacklisted, respectively. These arguments don't belong in TestSuite.
1834 1834 Instead, whitelist and blacklist should be handled by the thing that
1835 1835 populates the TestSuite with tests. They are present to preserve
1836 1836 backwards compatible behavior which reports skipped tests as part
1837 1837 of the results.
1838 1838
1839 1839 retest denotes whether to retest failed tests. This arguably belongs
1840 1840 outside of TestSuite.
1841 1841
1842 1842 keywords denotes key words that will be used to filter which tests
1843 1843 to execute. This arguably belongs outside of TestSuite.
1844 1844
1845 1845 loop denotes whether to loop over tests forever.
1846 1846 """
1847 1847 super(TestSuite, self).__init__(*args, **kwargs)
1848 1848
1849 1849 self._jobs = jobs
1850 1850 self._whitelist = whitelist
1851 1851 self._blacklist = blacklist
1852 1852 self._retest = retest
1853 1853 self._keywords = keywords
1854 1854 self._loop = loop
1855 1855 self._runs_per_test = runs_per_test
1856 1856 self._loadtest = loadtest
1857 1857 self._showchannels = showchannels
1858 1858
1859 1859 def run(self, result):
1860 1860 # We have a number of filters that need to be applied. We do this
1861 1861 # here instead of inside Test because it makes the running logic for
1862 1862 # Test simpler.
1863 1863 tests = []
1864 1864 num_tests = [0]
1865 1865 for test in self._tests:
1866 1866 def get():
1867 1867 num_tests[0] += 1
1868 1868 if getattr(test, 'should_reload', False):
1869 1869 return self._loadtest(test, num_tests[0])
1870 1870 return test
1871 1871 if not os.path.exists(test.path):
1872 1872 result.addSkip(test, "Doesn't exist")
1873 1873 continue
1874 1874
1875 1875 if not (self._whitelist and test.bname in self._whitelist):
1876 1876 if self._blacklist and test.bname in self._blacklist:
1877 1877 result.addSkip(test, 'blacklisted')
1878 1878 continue
1879 1879
1880 1880 if self._retest and not os.path.exists(test.errpath):
1881 1881 result.addIgnore(test, 'not retesting')
1882 1882 continue
1883 1883
1884 1884 if self._keywords:
1885 1885 with open(test.path, 'rb') as f:
1886 1886 t = f.read().lower() + test.bname.lower()
1887 1887 ignored = False
1888 1888 for k in self._keywords.lower().split():
1889 1889 if k not in t:
1890 1890 result.addIgnore(test, "doesn't match keyword")
1891 1891 ignored = True
1892 1892 break
1893 1893
1894 1894 if ignored:
1895 1895 continue
1896 1896 for _ in xrange(self._runs_per_test):
1897 1897 tests.append(get())
1898 1898
1899 1899 runtests = list(tests)
1900 1900 done = queue.Queue()
1901 1901 running = 0
1902 1902
1903 1903 channels = [""] * self._jobs
1904 1904
1905 1905 def job(test, result):
1906 1906 for n, v in enumerate(channels):
1907 1907 if not v:
1908 1908 channel = n
1909 1909 break
1910 1910 else:
1911 1911 raise ValueError('Could not find output channel')
1912 1912 channels[channel] = "=" + test.name[5:].split(".")[0]
1913 1913 try:
1914 1914 test(result)
1915 1915 done.put(None)
1916 1916 except KeyboardInterrupt:
1917 1917 pass
1918 1918 except: # re-raises
1919 1919 done.put(('!', test, 'run-test raised an error, see traceback'))
1920 1920 raise
1921 1921 finally:
1922 1922 try:
1923 1923 channels[channel] = ''
1924 1924 except IndexError:
1925 1925 pass
1926 1926
1927 1927 def stat():
1928 1928 count = 0
1929 1929 while channels:
1930 1930 d = '\n%03s ' % count
1931 1931 for n, v in enumerate(channels):
1932 1932 if v:
1933 1933 d += v[0]
1934 1934 channels[n] = v[1:] or '.'
1935 1935 else:
1936 1936 d += ' '
1937 1937 d += ' '
1938 1938 with iolock:
1939 1939 sys.stdout.write(d + ' ')
1940 1940 sys.stdout.flush()
1941 1941 for x in xrange(10):
1942 1942 if channels:
1943 1943 time.sleep(.1)
1944 1944 count += 1
1945 1945
1946 1946 stoppedearly = False
1947 1947
1948 1948 if self._showchannels:
1949 1949 statthread = threading.Thread(target=stat, name="stat")
1950 1950 statthread.start()
1951 1951
1952 1952 try:
1953 1953 while tests or running:
1954 1954 if not done.empty() or running == self._jobs or not tests:
1955 1955 try:
1956 1956 done.get(True, 1)
1957 1957 running -= 1
1958 1958 if result and result.shouldStop:
1959 1959 stoppedearly = True
1960 1960 break
1961 1961 except queue.Empty:
1962 1962 continue
1963 1963 if tests and not running == self._jobs:
1964 1964 test = tests.pop(0)
1965 1965 if self._loop:
1966 1966 if getattr(test, 'should_reload', False):
1967 1967 num_tests[0] += 1
1968 1968 tests.append(
1969 1969 self._loadtest(test, num_tests[0]))
1970 1970 else:
1971 1971 tests.append(test)
1972 1972 if self._jobs == 1:
1973 1973 job(test, result)
1974 1974 else:
1975 1975 t = threading.Thread(target=job, name=test.name,
1976 1976 args=(test, result))
1977 1977 t.start()
1978 1978 running += 1
1979 1979
1980 1980 # If we stop early we still need to wait on started tests to
1981 1981 # finish. Otherwise, there is a race between the test completing
1982 1982 # and the test's cleanup code running. This could result in the
1983 1983 # test reporting incorrect.
1984 1984 if stoppedearly:
1985 1985 while running:
1986 1986 try:
1987 1987 done.get(True, 1)
1988 1988 running -= 1
1989 1989 except queue.Empty:
1990 1990 continue
1991 1991 except KeyboardInterrupt:
1992 1992 for test in runtests:
1993 1993 test.abort()
1994 1994
1995 1995 channels = []
1996 1996
1997 1997 return result
1998 1998
1999 1999 # Save the most recent 5 wall-clock runtimes of each test to a
2000 2000 # human-readable text file named .testtimes. Tests are sorted
2001 2001 # alphabetically, while times for each test are listed from oldest to
2002 2002 # newest.
2003 2003
2004 2004 def loadtimes(outputdir):
2005 2005 times = []
2006 2006 try:
2007 2007 with open(os.path.join(outputdir, b'.testtimes-')) as fp:
2008 2008 for line in fp:
2009 2009 ts = line.split()
2010 2010 times.append((ts[0], [float(t) for t in ts[1:]]))
2011 2011 except IOError as err:
2012 2012 if err.errno != errno.ENOENT:
2013 2013 raise
2014 2014 return times
2015 2015
2016 2016 def savetimes(outputdir, result):
2017 2017 saved = dict(loadtimes(outputdir))
2018 2018 maxruns = 5
2019 2019 skipped = set([str(t[0]) for t in result.skipped])
2020 2020 for tdata in result.times:
2021 2021 test, real = tdata[0], tdata[3]
2022 2022 if test not in skipped:
2023 2023 ts = saved.setdefault(test, [])
2024 2024 ts.append(real)
2025 2025 ts[:] = ts[-maxruns:]
2026 2026
2027 2027 fd, tmpname = tempfile.mkstemp(prefix=b'.testtimes',
2028 2028 dir=outputdir, text=True)
2029 2029 with os.fdopen(fd, 'w') as fp:
2030 2030 for name, ts in sorted(saved.items()):
2031 2031 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
2032 2032 timepath = os.path.join(outputdir, b'.testtimes')
2033 2033 try:
2034 2034 os.unlink(timepath)
2035 2035 except OSError:
2036 2036 pass
2037 2037 try:
2038 2038 os.rename(tmpname, timepath)
2039 2039 except OSError:
2040 2040 pass
2041 2041
2042 2042 class TextTestRunner(unittest.TextTestRunner):
2043 2043 """Custom unittest test runner that uses appropriate settings."""
2044 2044
2045 2045 def __init__(self, runner, *args, **kwargs):
2046 2046 super(TextTestRunner, self).__init__(*args, **kwargs)
2047 2047
2048 2048 self._runner = runner
2049 2049
2050 2050 def listtests(self, test):
2051 2051 result = TestResult(self._runner.options, self.stream,
2052 2052 self.descriptions, 0)
2053 2053 test = sorted(test, key=lambda t: t.name)
2054 2054 for t in test:
2055 2055 print(t.name)
2056 2056 result.addSuccess(t)
2057 2057
2058 2058 if self._runner.options.xunit:
2059 2059 with open(self._runner.options.xunit, "wb") as xuf:
2060 2060 self._writexunit(result, xuf)
2061 2061
2062 2062 if self._runner.options.json:
2063 2063 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2064 2064 with open(jsonpath, 'w') as fp:
2065 2065 self._writejson(result, fp)
2066 2066
2067 2067 return result
2068 2068
2069 2069 def run(self, test):
2070 2070 result = TestResult(self._runner.options, self.stream,
2071 2071 self.descriptions, self.verbosity)
2072 2072
2073 2073 test(result)
2074 2074
2075 2075 failed = len(result.failures)
2076 2076 skipped = len(result.skipped)
2077 2077 ignored = len(result.ignored)
2078 2078
2079 2079 with iolock:
2080 2080 self.stream.writeln('')
2081 2081
2082 2082 if not self._runner.options.noskips:
2083 2083 for test, msg in result.skipped:
2084 2084 formatted = 'Skipped %s: %s\n' % (test.name, msg)
2085 2085 self.stream.write(highlightmsg(formatted, result.color))
2086 2086 for test, msg in result.failures:
2087 2087 formatted = 'Failed %s: %s\n' % (test.name, msg)
2088 2088 self.stream.write(highlightmsg(formatted, result.color))
2089 2089 for test, msg in result.errors:
2090 2090 self.stream.writeln('Errored %s: %s' % (test.name, msg))
2091 2091
2092 2092 if self._runner.options.xunit:
2093 2093 with open(self._runner.options.xunit, "wb") as xuf:
2094 2094 self._writexunit(result, xuf)
2095 2095
2096 2096 if self._runner.options.json:
2097 2097 jsonpath = os.path.join(self._runner._outputdir, b'report.json')
2098 2098 with open(jsonpath, 'w') as fp:
2099 2099 self._writejson(result, fp)
2100 2100
2101 2101 self._runner._checkhglib('Tested')
2102 2102
2103 2103 savetimes(self._runner._outputdir, result)
2104 2104
2105 2105 if failed and self._runner.options.known_good_rev:
2106 2106 self._bisecttests(t for t, m in result.failures)
2107 2107 self.stream.writeln(
2108 2108 '# Ran %d tests, %d skipped, %d failed.'
2109 2109 % (result.testsRun, skipped + ignored, failed))
2110 2110 if failed:
2111 2111 self.stream.writeln('python hash seed: %s' %
2112 2112 os.environ['PYTHONHASHSEED'])
2113 2113 if self._runner.options.time:
2114 2114 self.printtimes(result.times)
2115 2115
2116 2116 if self._runner.options.exceptions:
2117 2117 exceptions = aggregateexceptions(
2118 2118 os.path.join(self._runner._outputdir, b'exceptions'))
2119 2119 total = sum(exceptions.values())
2120 2120
2121 2121 self.stream.writeln('Exceptions Report:')
2122 2122 self.stream.writeln('%d total from %d frames' %
2123 2123 (total, len(exceptions)))
2124 2124 for (frame, line, exc), count in exceptions.most_common():
2125 2125 self.stream.writeln('%d\t%s: %s' % (count, frame, exc))
2126 2126
2127 2127 self.stream.flush()
2128 2128
2129 2129 return result
2130 2130
2131 2131 def _bisecttests(self, tests):
2132 2132 bisectcmd = ['hg', 'bisect']
2133 2133 bisectrepo = self._runner.options.bisect_repo
2134 2134 if bisectrepo:
2135 2135 bisectcmd.extend(['-R', os.path.abspath(bisectrepo)])
2136 2136 def pread(args):
2137 2137 env = os.environ.copy()
2138 2138 env['HGPLAIN'] = '1'
2139 2139 p = subprocess.Popen(args, stderr=subprocess.STDOUT,
2140 2140 stdout=subprocess.PIPE, env=env)
2141 2141 data = p.stdout.read()
2142 2142 p.wait()
2143 2143 return data
2144 2144 for test in tests:
2145 2145 pread(bisectcmd + ['--reset']),
2146 2146 pread(bisectcmd + ['--bad', '.'])
2147 2147 pread(bisectcmd + ['--good', self._runner.options.known_good_rev])
2148 2148 # TODO: we probably need to forward more options
2149 2149 # that alter hg's behavior inside the tests.
2150 2150 opts = ''
2151 2151 withhg = self._runner.options.with_hg
2152 2152 if withhg:
2153 2153 opts += ' --with-hg=%s ' % shellquote(_strpath(withhg))
2154 2154 rtc = '%s %s %s %s' % (sys.executable, sys.argv[0], opts,
2155 2155 test)
2156 2156 data = pread(bisectcmd + ['--command', rtc])
2157 2157 m = re.search(
2158 2158 (br'\nThe first (?P<goodbad>bad|good) revision '
2159 2159 br'is:\nchangeset: +\d+:(?P<node>[a-f0-9]+)\n.*\n'
2160 2160 br'summary: +(?P<summary>[^\n]+)\n'),
2161 2161 data, (re.MULTILINE | re.DOTALL))
2162 2162 if m is None:
2163 2163 self.stream.writeln(
2164 2164 'Failed to identify failure point for %s' % test)
2165 2165 continue
2166 2166 dat = m.groupdict()
2167 2167 verb = 'broken' if dat['goodbad'] == 'bad' else 'fixed'
2168 2168 self.stream.writeln(
2169 2169 '%s %s by %s (%s)' % (
2170 2170 test, verb, dat['node'], dat['summary']))
2171 2171
2172 2172 def printtimes(self, times):
2173 2173 # iolock held by run
2174 2174 self.stream.writeln('# Producing time report')
2175 2175 times.sort(key=lambda t: (t[3]))
2176 2176 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
2177 2177 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
2178 2178 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
2179 2179 for tdata in times:
2180 2180 test = tdata[0]
2181 2181 cuser, csys, real, start, end = tdata[1:6]
2182 2182 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
2183 2183
2184 2184 @staticmethod
2185 2185 def _writexunit(result, outf):
2186 2186 # See http://llg.cubic.org/docs/junit/ for a reference.
2187 2187 timesd = dict((t[0], t[3]) for t in result.times)
2188 2188 doc = minidom.Document()
2189 2189 s = doc.createElement('testsuite')
2190 2190 s.setAttribute('name', 'run-tests')
2191 2191 s.setAttribute('tests', str(result.testsRun))
2192 2192 s.setAttribute('errors', "0") # TODO
2193 2193 s.setAttribute('failures', str(len(result.failures)))
2194 2194 s.setAttribute('skipped', str(len(result.skipped) +
2195 2195 len(result.ignored)))
2196 2196 doc.appendChild(s)
2197 2197 for tc in result.successes:
2198 2198 t = doc.createElement('testcase')
2199 2199 t.setAttribute('name', tc.name)
2200 2200 tctime = timesd.get(tc.name)
2201 2201 if tctime is not None:
2202 2202 t.setAttribute('time', '%.3f' % tctime)
2203 2203 s.appendChild(t)
2204 2204 for tc, err in sorted(result.faildata.items()):
2205 2205 t = doc.createElement('testcase')
2206 2206 t.setAttribute('name', tc)
2207 2207 tctime = timesd.get(tc)
2208 2208 if tctime is not None:
2209 2209 t.setAttribute('time', '%.3f' % tctime)
2210 2210 # createCDATASection expects a unicode or it will
2211 2211 # convert using default conversion rules, which will
2212 2212 # fail if string isn't ASCII.
2213 2213 err = cdatasafe(err).decode('utf-8', 'replace')
2214 2214 cd = doc.createCDATASection(err)
2215 2215 # Use 'failure' here instead of 'error' to match errors = 0,
2216 2216 # failures = len(result.failures) in the testsuite element.
2217 2217 failelem = doc.createElement('failure')
2218 2218 failelem.setAttribute('message', 'output changed')
2219 2219 failelem.setAttribute('type', 'output-mismatch')
2220 2220 failelem.appendChild(cd)
2221 2221 t.appendChild(failelem)
2222 2222 s.appendChild(t)
2223 2223 for tc, message in result.skipped:
2224 2224 # According to the schema, 'skipped' has no attributes. So store
2225 2225 # the skip message as a text node instead.
2226 2226 t = doc.createElement('testcase')
2227 2227 t.setAttribute('name', tc.name)
2228 2228 binmessage = message.encode('utf-8')
2229 2229 message = cdatasafe(binmessage).decode('utf-8', 'replace')
2230 2230 cd = doc.createCDATASection(message)
2231 2231 skipelem = doc.createElement('skipped')
2232 2232 skipelem.appendChild(cd)
2233 2233 t.appendChild(skipelem)
2234 2234 s.appendChild(t)
2235 2235 outf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
2236 2236
2237 2237 @staticmethod
2238 2238 def _writejson(result, outf):
2239 2239 timesd = {}
2240 2240 for tdata in result.times:
2241 2241 test = tdata[0]
2242 2242 timesd[test] = tdata[1:]
2243 2243
2244 2244 outcome = {}
2245 2245 groups = [('success', ((tc, None)
2246 2246 for tc in result.successes)),
2247 2247 ('failure', result.failures),
2248 2248 ('skip', result.skipped)]
2249 2249 for res, testcases in groups:
2250 2250 for tc, __ in testcases:
2251 2251 if tc.name in timesd:
2252 2252 diff = result.faildata.get(tc.name, b'')
2253 2253 try:
2254 2254 diff = diff.decode('unicode_escape')
2255 2255 except UnicodeDecodeError as e:
2256 2256 diff = '%r decoding diff, sorry' % e
2257 2257 tres = {'result': res,
2258 2258 'time': ('%0.3f' % timesd[tc.name][2]),
2259 2259 'cuser': ('%0.3f' % timesd[tc.name][0]),
2260 2260 'csys': ('%0.3f' % timesd[tc.name][1]),
2261 2261 'start': ('%0.3f' % timesd[tc.name][3]),
2262 2262 'end': ('%0.3f' % timesd[tc.name][4]),
2263 2263 'diff': diff,
2264 2264 }
2265 2265 else:
2266 2266 # blacklisted test
2267 2267 tres = {'result': res}
2268 2268
2269 2269 outcome[tc.name] = tres
2270 2270 jsonout = json.dumps(outcome, sort_keys=True, indent=4,
2271 2271 separators=(',', ': '))
2272 2272 outf.writelines(("testreport =", jsonout))
2273 2273
2274 2274 def sorttests(testdescs, shuffle=False):
2275 2275 """Do an in-place sort of tests."""
2276 2276 if shuffle:
2277 2277 random.shuffle(testdescs)
2278 2278 return
2279 2279
2280 2280 # keywords for slow tests
2281 2281 slow = {b'svn': 10,
2282 2282 b'cvs': 10,
2283 2283 b'hghave': 10,
2284 2284 b'largefiles-update': 10,
2285 2285 b'run-tests': 10,
2286 2286 b'corruption': 10,
2287 2287 b'race': 10,
2288 2288 b'i18n': 10,
2289 2289 b'check': 100,
2290 2290 b'gendoc': 100,
2291 2291 b'contrib-perf': 200,
2292 2292 }
2293 2293 perf = {}
2294 2294
2295 2295 def sortkey(f):
2296 2296 # run largest tests first, as they tend to take the longest
2297 2297 f = f['path']
2298 2298 try:
2299 2299 return perf[f]
2300 2300 except KeyError:
2301 2301 try:
2302 2302 val = -os.stat(f).st_size
2303 2303 except OSError as e:
2304 2304 if e.errno != errno.ENOENT:
2305 2305 raise
2306 2306 perf[f] = -1e9 # file does not exist, tell early
2307 2307 return -1e9
2308 2308 for kw, mul in slow.items():
2309 2309 if kw in f:
2310 2310 val *= mul
2311 2311 if f.endswith(b'.py'):
2312 2312 val /= 10.0
2313 2313 perf[f] = val / 1000.0
2314 2314 return perf[f]
2315 2315
2316 2316 testdescs.sort(key=sortkey)
2317 2317
2318 2318 class TestRunner(object):
2319 2319 """Holds context for executing tests.
2320 2320
2321 2321 Tests rely on a lot of state. This object holds it for them.
2322 2322 """
2323 2323
2324 2324 # Programs required to run tests.
2325 2325 REQUIREDTOOLS = [
2326 2326 b'diff',
2327 2327 b'grep',
2328 2328 b'unzip',
2329 2329 b'gunzip',
2330 2330 b'bunzip2',
2331 2331 b'sed',
2332 2332 ]
2333 2333
2334 2334 # Maps file extensions to test class.
2335 2335 TESTTYPES = [
2336 2336 (b'.py', PythonTest),
2337 2337 (b'.t', TTest),
2338 2338 ]
2339 2339
2340 2340 def __init__(self):
2341 2341 self.options = None
2342 2342 self._hgroot = None
2343 2343 self._testdir = None
2344 2344 self._outputdir = None
2345 2345 self._hgtmp = None
2346 2346 self._installdir = None
2347 2347 self._bindir = None
2348 2348 self._tmpbinddir = None
2349 2349 self._pythondir = None
2350 2350 self._coveragefile = None
2351 2351 self._createdfiles = []
2352 2352 self._hgcommand = None
2353 2353 self._hgpath = None
2354 2354 self._portoffset = 0
2355 2355 self._ports = {}
2356 2356
2357 2357 def run(self, args, parser=None):
2358 2358 """Run the test suite."""
2359 2359 oldmask = os.umask(0o22)
2360 2360 try:
2361 2361 parser = parser or getparser()
2362 2362 options = parseargs(args, parser)
2363 2363 tests = [_bytespath(a) for a in options.tests]
2364 2364 if options.test_list is not None:
2365 2365 for listfile in options.test_list:
2366 2366 with open(listfile, 'rb') as f:
2367 2367 tests.extend(t for t in f.read().splitlines() if t)
2368 2368 self.options = options
2369 2369
2370 2370 self._checktools()
2371 2371 testdescs = self.findtests(tests)
2372 2372 if options.profile_runner:
2373 2373 import statprof
2374 2374 statprof.start()
2375 2375 result = self._run(testdescs)
2376 2376 if options.profile_runner:
2377 2377 statprof.stop()
2378 2378 statprof.display()
2379 2379 return result
2380 2380
2381 2381 finally:
2382 2382 os.umask(oldmask)
2383 2383
2384 2384 def _run(self, testdescs):
2385 2385 sorttests(testdescs, shuffle=self.options.random)
2386 2386
2387 2387 self._testdir = osenvironb[b'TESTDIR'] = getattr(
2388 2388 os, 'getcwdb', os.getcwd)()
2389 2389 # assume all tests in same folder for now
2390 2390 if testdescs:
2391 2391 pathname = os.path.dirname(testdescs[0]['path'])
2392 2392 if pathname:
2393 2393 osenvironb[b'TESTDIR'] = os.path.join(osenvironb[b'TESTDIR'],
2394 2394 pathname)
2395 2395 if self.options.outputdir:
2396 2396 self._outputdir = canonpath(_bytespath(self.options.outputdir))
2397 2397 else:
2398 2398 self._outputdir = self._testdir
2399 2399 if testdescs and pathname:
2400 2400 self._outputdir = os.path.join(self._outputdir, pathname)
2401 2401
2402 2402 if 'PYTHONHASHSEED' not in os.environ:
2403 2403 # use a random python hash seed all the time
2404 2404 # we do the randomness ourself to know what seed is used
2405 2405 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
2406 2406
2407 2407 if self.options.tmpdir:
2408 2408 self.options.keep_tmpdir = True
2409 2409 tmpdir = _bytespath(self.options.tmpdir)
2410 2410 if os.path.exists(tmpdir):
2411 2411 # Meaning of tmpdir has changed since 1.3: we used to create
2412 2412 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
2413 2413 # tmpdir already exists.
2414 2414 print("error: temp dir %r already exists" % tmpdir)
2415 2415 return 1
2416 2416
2417 2417 os.makedirs(tmpdir)
2418 2418 else:
2419 2419 d = None
2420 2420 if os.name == 'nt':
2421 2421 # without this, we get the default temp dir location, but
2422 2422 # in all lowercase, which causes troubles with paths (issue3490)
2423 2423 d = osenvironb.get(b'TMP', None)
2424 2424 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
2425 2425
2426 2426 self._hgtmp = osenvironb[b'HGTMP'] = (
2427 2427 os.path.realpath(tmpdir))
2428 2428
2429 2429 if self.options.with_hg:
2430 2430 self._installdir = None
2431 2431 whg = self.options.with_hg
2432 2432 self._bindir = os.path.dirname(os.path.realpath(whg))
2433 2433 assert isinstance(self._bindir, bytes)
2434 2434 self._hgcommand = os.path.basename(whg)
2435 2435 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
2436 2436 os.makedirs(self._tmpbindir)
2437 2437
2438 2438 normbin = os.path.normpath(os.path.abspath(whg))
2439 2439 normbin = normbin.replace(os.sep.encode('ascii'), b'/')
2440 2440
2441 2441 # Other Python scripts in the test harness need to
2442 2442 # `import mercurial`. If `hg` is a Python script, we assume
2443 2443 # the Mercurial modules are relative to its path and tell the tests
2444 2444 # to load Python modules from its directory.
2445 2445 with open(whg, 'rb') as fh:
2446 2446 initial = fh.read(1024)
2447 2447
2448 2448 if re.match(b'#!.*python', initial):
2449 2449 self._pythondir = self._bindir
2450 2450 # If it looks like our in-repo Rust binary, use the source root.
2451 2451 # This is a bit hacky. But rhg is still not supported outside the
2452 2452 # source directory. So until it is, do the simple thing.
2453 elif re.search(b'|/rust/target/[^/]+/hg', normbin):
2453 elif re.search(b'/rust/target/[^/]+/hg', normbin):
2454 2454 self._pythondir = os.path.dirname(self._testdir)
2455 2455 # Fall back to the legacy behavior.
2456 2456 else:
2457 2457 self._pythondir = self._bindir
2458 2458
2459 2459 else:
2460 2460 self._installdir = os.path.join(self._hgtmp, b"install")
2461 2461 self._bindir = os.path.join(self._installdir, b"bin")
2462 2462 self._hgcommand = b'hg'
2463 2463 self._tmpbindir = self._bindir
2464 2464 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
2465 2465
2466 2466 # set CHGHG, then replace "hg" command by "chg"
2467 2467 chgbindir = self._bindir
2468 2468 if self.options.chg or self.options.with_chg:
2469 2469 osenvironb[b'CHGHG'] = os.path.join(self._bindir, self._hgcommand)
2470 2470 else:
2471 2471 osenvironb.pop(b'CHGHG', None) # drop flag for hghave
2472 2472 if self.options.chg:
2473 2473 self._hgcommand = b'chg'
2474 2474 elif self.options.with_chg:
2475 2475 chgbindir = os.path.dirname(os.path.realpath(self.options.with_chg))
2476 2476 self._hgcommand = os.path.basename(self.options.with_chg)
2477 2477
2478 2478 osenvironb[b"BINDIR"] = self._bindir
2479 2479 osenvironb[b"PYTHON"] = PYTHON
2480 2480
2481 2481 if self.options.with_python3:
2482 2482 osenvironb[b'PYTHON3'] = self.options.with_python3
2483 2483
2484 2484 fileb = _bytespath(__file__)
2485 2485 runtestdir = os.path.abspath(os.path.dirname(fileb))
2486 2486 osenvironb[b'RUNTESTDIR'] = runtestdir
2487 2487 if PYTHON3:
2488 2488 sepb = _bytespath(os.pathsep)
2489 2489 else:
2490 2490 sepb = os.pathsep
2491 2491 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
2492 2492 if os.path.islink(__file__):
2493 2493 # test helper will likely be at the end of the symlink
2494 2494 realfile = os.path.realpath(fileb)
2495 2495 realdir = os.path.abspath(os.path.dirname(realfile))
2496 2496 path.insert(2, realdir)
2497 2497 if chgbindir != self._bindir:
2498 2498 path.insert(1, chgbindir)
2499 2499 if self._testdir != runtestdir:
2500 2500 path = [self._testdir] + path
2501 2501 if self._tmpbindir != self._bindir:
2502 2502 path = [self._tmpbindir] + path
2503 2503 osenvironb[b"PATH"] = sepb.join(path)
2504 2504
2505 2505 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
2506 2506 # can run .../tests/run-tests.py test-foo where test-foo
2507 2507 # adds an extension to HGRC. Also include run-test.py directory to
2508 2508 # import modules like heredoctest.
2509 2509 pypath = [self._pythondir, self._testdir, runtestdir]
2510 2510 # We have to augment PYTHONPATH, rather than simply replacing
2511 2511 # it, in case external libraries are only available via current
2512 2512 # PYTHONPATH. (In particular, the Subversion bindings on OS X
2513 2513 # are in /opt/subversion.)
2514 2514 oldpypath = osenvironb.get(IMPL_PATH)
2515 2515 if oldpypath:
2516 2516 pypath.append(oldpypath)
2517 2517 osenvironb[IMPL_PATH] = sepb.join(pypath)
2518 2518
2519 2519 if self.options.pure:
2520 2520 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
2521 2521 os.environ["HGMODULEPOLICY"] = "py"
2522 2522
2523 2523 if self.options.allow_slow_tests:
2524 2524 os.environ["HGTEST_SLOW"] = "slow"
2525 2525 elif 'HGTEST_SLOW' in os.environ:
2526 2526 del os.environ['HGTEST_SLOW']
2527 2527
2528 2528 self._coveragefile = os.path.join(self._testdir, b'.coverage')
2529 2529
2530 2530 if self.options.exceptions:
2531 2531 exceptionsdir = os.path.join(self._outputdir, b'exceptions')
2532 2532 try:
2533 2533 os.makedirs(exceptionsdir)
2534 2534 except OSError as e:
2535 2535 if e.errno != errno.EEXIST:
2536 2536 raise
2537 2537
2538 2538 # Remove all existing exception reports.
2539 2539 for f in os.listdir(exceptionsdir):
2540 2540 os.unlink(os.path.join(exceptionsdir, f))
2541 2541
2542 2542 osenvironb[b'HGEXCEPTIONSDIR'] = exceptionsdir
2543 2543 logexceptions = os.path.join(self._testdir, b'logexceptions.py')
2544 2544 self.options.extra_config_opt.append(
2545 2545 'extensions.logexceptions=%s' % logexceptions.decode('utf-8'))
2546 2546
2547 2547 vlog("# Using TESTDIR", self._testdir)
2548 2548 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
2549 2549 vlog("# Using HGTMP", self._hgtmp)
2550 2550 vlog("# Using PATH", os.environ["PATH"])
2551 2551 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
2552 2552 vlog("# Writing to directory", self._outputdir)
2553 2553
2554 2554 try:
2555 2555 return self._runtests(testdescs) or 0
2556 2556 finally:
2557 2557 time.sleep(.1)
2558 2558 self._cleanup()
2559 2559
2560 2560 def findtests(self, args):
2561 2561 """Finds possible test files from arguments.
2562 2562
2563 2563 If you wish to inject custom tests into the test harness, this would
2564 2564 be a good function to monkeypatch or override in a derived class.
2565 2565 """
2566 2566 if not args:
2567 2567 if self.options.changed:
2568 2568 proc = Popen4('hg st --rev "%s" -man0 .' %
2569 2569 self.options.changed, None, 0)
2570 2570 stdout, stderr = proc.communicate()
2571 2571 args = stdout.strip(b'\0').split(b'\0')
2572 2572 else:
2573 2573 args = os.listdir(b'.')
2574 2574
2575 2575 expanded_args = []
2576 2576 for arg in args:
2577 2577 if os.path.isdir(arg):
2578 2578 if not arg.endswith(b'/'):
2579 2579 arg += b'/'
2580 2580 expanded_args.extend([arg + a for a in os.listdir(arg)])
2581 2581 else:
2582 2582 expanded_args.append(arg)
2583 2583 args = expanded_args
2584 2584
2585 2585 tests = []
2586 2586 for t in args:
2587 2587 if not (os.path.basename(t).startswith(b'test-')
2588 2588 and (t.endswith(b'.py') or t.endswith(b'.t'))):
2589 2589 continue
2590 2590 if t.endswith(b'.t'):
2591 2591 # .t file may contain multiple test cases
2592 2592 cases = sorted(parsettestcases(t))
2593 2593 if cases:
2594 2594 tests += [{'path': t, 'case': c} for c in sorted(cases)]
2595 2595 else:
2596 2596 tests.append({'path': t})
2597 2597 else:
2598 2598 tests.append({'path': t})
2599 2599 return tests
2600 2600
2601 2601 def _runtests(self, testdescs):
2602 2602 def _reloadtest(test, i):
2603 2603 # convert a test back to its description dict
2604 2604 desc = {'path': test.path}
2605 2605 case = getattr(test, '_case', None)
2606 2606 if case:
2607 2607 desc['case'] = case
2608 2608 return self._gettest(desc, i)
2609 2609
2610 2610 try:
2611 2611 if self.options.restart:
2612 2612 orig = list(testdescs)
2613 2613 while testdescs:
2614 2614 desc = testdescs[0]
2615 2615 # desc['path'] is a relative path
2616 2616 if 'case' in desc:
2617 2617 errpath = b'%s.%s.err' % (desc['path'], desc['case'])
2618 2618 else:
2619 2619 errpath = b'%s.err' % desc['path']
2620 2620 errpath = os.path.join(self._outputdir, errpath)
2621 2621 if os.path.exists(errpath):
2622 2622 break
2623 2623 testdescs.pop(0)
2624 2624 if not testdescs:
2625 2625 print("running all tests")
2626 2626 testdescs = orig
2627 2627
2628 2628 tests = [self._gettest(d, i) for i, d in enumerate(testdescs)]
2629 2629
2630 2630 failed = False
2631 2631 kws = self.options.keywords
2632 2632 if kws is not None and PYTHON3:
2633 2633 kws = kws.encode('utf-8')
2634 2634
2635 2635 suite = TestSuite(self._testdir,
2636 2636 jobs=self.options.jobs,
2637 2637 whitelist=self.options.whitelisted,
2638 2638 blacklist=self.options.blacklist,
2639 2639 retest=self.options.retest,
2640 2640 keywords=kws,
2641 2641 loop=self.options.loop,
2642 2642 runs_per_test=self.options.runs_per_test,
2643 2643 showchannels=self.options.showchannels,
2644 2644 tests=tests, loadtest=_reloadtest)
2645 2645 verbosity = 1
2646 2646 if self.options.verbose:
2647 2647 verbosity = 2
2648 2648 runner = TextTestRunner(self, verbosity=verbosity)
2649 2649
2650 2650 if self.options.list_tests:
2651 2651 result = runner.listtests(suite)
2652 2652 else:
2653 2653 if self._installdir:
2654 2654 self._installhg()
2655 2655 self._checkhglib("Testing")
2656 2656 else:
2657 2657 self._usecorrectpython()
2658 2658 if self.options.chg:
2659 2659 assert self._installdir
2660 2660 self._installchg()
2661 2661
2662 2662 result = runner.run(suite)
2663 2663
2664 2664 if result.failures:
2665 2665 failed = True
2666 2666
2667 2667 if self.options.anycoverage:
2668 2668 self._outputcoverage()
2669 2669 except KeyboardInterrupt:
2670 2670 failed = True
2671 2671 print("\ninterrupted!")
2672 2672
2673 2673 if failed:
2674 2674 return 1
2675 2675
2676 2676 def _getport(self, count):
2677 2677 port = self._ports.get(count) # do we have a cached entry?
2678 2678 if port is None:
2679 2679 portneeded = 3
2680 2680 # above 100 tries we just give up and let test reports failure
2681 2681 for tries in xrange(100):
2682 2682 allfree = True
2683 2683 port = self.options.port + self._portoffset
2684 2684 for idx in xrange(portneeded):
2685 2685 if not checkportisavailable(port + idx):
2686 2686 allfree = False
2687 2687 break
2688 2688 self._portoffset += portneeded
2689 2689 if allfree:
2690 2690 break
2691 2691 self._ports[count] = port
2692 2692 return port
2693 2693
2694 2694 def _gettest(self, testdesc, count):
2695 2695 """Obtain a Test by looking at its filename.
2696 2696
2697 2697 Returns a Test instance. The Test may not be runnable if it doesn't
2698 2698 map to a known type.
2699 2699 """
2700 2700 path = testdesc['path']
2701 2701 lctest = path.lower()
2702 2702 testcls = Test
2703 2703
2704 2704 for ext, cls in self.TESTTYPES:
2705 2705 if lctest.endswith(ext):
2706 2706 testcls = cls
2707 2707 break
2708 2708
2709 2709 refpath = os.path.join(self._testdir, path)
2710 2710 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2711 2711
2712 2712 # extra keyword parameters. 'case' is used by .t tests
2713 2713 kwds = dict((k, testdesc[k]) for k in ['case'] if k in testdesc)
2714 2714
2715 2715 t = testcls(refpath, self._outputdir, tmpdir,
2716 2716 keeptmpdir=self.options.keep_tmpdir,
2717 2717 debug=self.options.debug,
2718 2718 timeout=self.options.timeout,
2719 2719 startport=self._getport(count),
2720 2720 extraconfigopts=self.options.extra_config_opt,
2721 2721 py3kwarnings=self.options.py3k_warnings,
2722 2722 shell=self.options.shell,
2723 2723 hgcommand=self._hgcommand,
2724 2724 usechg=bool(self.options.with_chg or self.options.chg),
2725 2725 useipv6=useipv6, **kwds)
2726 2726 t.should_reload = True
2727 2727 return t
2728 2728
2729 2729 def _cleanup(self):
2730 2730 """Clean up state from this test invocation."""
2731 2731 if self.options.keep_tmpdir:
2732 2732 return
2733 2733
2734 2734 vlog("# Cleaning up HGTMP", self._hgtmp)
2735 2735 shutil.rmtree(self._hgtmp, True)
2736 2736 for f in self._createdfiles:
2737 2737 try:
2738 2738 os.remove(f)
2739 2739 except OSError:
2740 2740 pass
2741 2741
2742 2742 def _usecorrectpython(self):
2743 2743 """Configure the environment to use the appropriate Python in tests."""
2744 2744 # Tests must use the same interpreter as us or bad things will happen.
2745 2745 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2746 2746 if getattr(os, 'symlink', None):
2747 2747 vlog("# Making python executable in test path a symlink to '%s'" %
2748 2748 sys.executable)
2749 2749 mypython = os.path.join(self._tmpbindir, pyexename)
2750 2750 try:
2751 2751 if os.readlink(mypython) == sys.executable:
2752 2752 return
2753 2753 os.unlink(mypython)
2754 2754 except OSError as err:
2755 2755 if err.errno != errno.ENOENT:
2756 2756 raise
2757 2757 if self._findprogram(pyexename) != sys.executable:
2758 2758 try:
2759 2759 os.symlink(sys.executable, mypython)
2760 2760 self._createdfiles.append(mypython)
2761 2761 except OSError as err:
2762 2762 # child processes may race, which is harmless
2763 2763 if err.errno != errno.EEXIST:
2764 2764 raise
2765 2765 else:
2766 2766 exedir, exename = os.path.split(sys.executable)
2767 2767 vlog("# Modifying search path to find %s as %s in '%s'" %
2768 2768 (exename, pyexename, exedir))
2769 2769 path = os.environ['PATH'].split(os.pathsep)
2770 2770 while exedir in path:
2771 2771 path.remove(exedir)
2772 2772 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2773 2773 if not self._findprogram(pyexename):
2774 2774 print("WARNING: Cannot find %s in search path" % pyexename)
2775 2775
2776 2776 def _installhg(self):
2777 2777 """Install hg into the test environment.
2778 2778
2779 2779 This will also configure hg with the appropriate testing settings.
2780 2780 """
2781 2781 vlog("# Performing temporary installation of HG")
2782 2782 installerrs = os.path.join(self._hgtmp, b"install.err")
2783 2783 compiler = ''
2784 2784 if self.options.compiler:
2785 2785 compiler = '--compiler ' + self.options.compiler
2786 2786 if self.options.pure:
2787 2787 pure = b"--pure"
2788 2788 else:
2789 2789 pure = b""
2790 2790
2791 2791 # Run installer in hg root
2792 2792 script = os.path.realpath(sys.argv[0])
2793 2793 exe = sys.executable
2794 2794 if PYTHON3:
2795 2795 compiler = _bytespath(compiler)
2796 2796 script = _bytespath(script)
2797 2797 exe = _bytespath(exe)
2798 2798 hgroot = os.path.dirname(os.path.dirname(script))
2799 2799 self._hgroot = hgroot
2800 2800 os.chdir(hgroot)
2801 2801 nohome = b'--home=""'
2802 2802 if os.name == 'nt':
2803 2803 # The --home="" trick works only on OS where os.sep == '/'
2804 2804 # because of a distutils convert_path() fast-path. Avoid it at
2805 2805 # least on Windows for now, deal with .pydistutils.cfg bugs
2806 2806 # when they happen.
2807 2807 nohome = b''
2808 2808 cmd = (b'%(exe)s setup.py %(pure)s clean --all'
2809 2809 b' build %(compiler)s --build-base="%(base)s"'
2810 2810 b' install --force --prefix="%(prefix)s"'
2811 2811 b' --install-lib="%(libdir)s"'
2812 2812 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2813 2813 % {b'exe': exe, b'pure': pure,
2814 2814 b'compiler': compiler,
2815 2815 b'base': os.path.join(self._hgtmp, b"build"),
2816 2816 b'prefix': self._installdir, b'libdir': self._pythondir,
2817 2817 b'bindir': self._bindir,
2818 2818 b'nohome': nohome, b'logfile': installerrs})
2819 2819
2820 2820 # setuptools requires install directories to exist.
2821 2821 def makedirs(p):
2822 2822 try:
2823 2823 os.makedirs(p)
2824 2824 except OSError as e:
2825 2825 if e.errno != errno.EEXIST:
2826 2826 raise
2827 2827 makedirs(self._pythondir)
2828 2828 makedirs(self._bindir)
2829 2829
2830 2830 vlog("# Running", cmd)
2831 2831 if os.system(cmd) == 0:
2832 2832 if not self.options.verbose:
2833 2833 try:
2834 2834 os.remove(installerrs)
2835 2835 except OSError as e:
2836 2836 if e.errno != errno.ENOENT:
2837 2837 raise
2838 2838 else:
2839 2839 with open(installerrs, 'rb') as f:
2840 2840 for line in f:
2841 2841 if PYTHON3:
2842 2842 sys.stdout.buffer.write(line)
2843 2843 else:
2844 2844 sys.stdout.write(line)
2845 2845 sys.exit(1)
2846 2846 os.chdir(self._testdir)
2847 2847
2848 2848 self._usecorrectpython()
2849 2849
2850 2850 if self.options.py3k_warnings and not self.options.anycoverage:
2851 2851 vlog("# Updating hg command to enable Py3k Warnings switch")
2852 2852 with open(os.path.join(self._bindir, 'hg'), 'rb') as f:
2853 2853 lines = [line.rstrip() for line in f]
2854 2854 lines[0] += ' -3'
2855 2855 with open(os.path.join(self._bindir, 'hg'), 'wb') as f:
2856 2856 for line in lines:
2857 2857 f.write(line + '\n')
2858 2858
2859 2859 hgbat = os.path.join(self._bindir, b'hg.bat')
2860 2860 if os.path.isfile(hgbat):
2861 2861 # hg.bat expects to be put in bin/scripts while run-tests.py
2862 2862 # installation layout put it in bin/ directly. Fix it
2863 2863 with open(hgbat, 'rb') as f:
2864 2864 data = f.read()
2865 2865 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2866 2866 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2867 2867 b'"%~dp0python" "%~dp0hg" %*')
2868 2868 with open(hgbat, 'wb') as f:
2869 2869 f.write(data)
2870 2870 else:
2871 2871 print('WARNING: cannot fix hg.bat reference to python.exe')
2872 2872
2873 2873 if self.options.anycoverage:
2874 2874 custom = os.path.join(self._testdir, 'sitecustomize.py')
2875 2875 target = os.path.join(self._pythondir, 'sitecustomize.py')
2876 2876 vlog('# Installing coverage trigger to %s' % target)
2877 2877 shutil.copyfile(custom, target)
2878 2878 rc = os.path.join(self._testdir, '.coveragerc')
2879 2879 vlog('# Installing coverage rc to %s' % rc)
2880 2880 os.environ['COVERAGE_PROCESS_START'] = rc
2881 2881 covdir = os.path.join(self._installdir, '..', 'coverage')
2882 2882 try:
2883 2883 os.mkdir(covdir)
2884 2884 except OSError as e:
2885 2885 if e.errno != errno.EEXIST:
2886 2886 raise
2887 2887
2888 2888 os.environ['COVERAGE_DIR'] = covdir
2889 2889
2890 2890 def _checkhglib(self, verb):
2891 2891 """Ensure that the 'mercurial' package imported by python is
2892 2892 the one we expect it to be. If not, print a warning to stderr."""
2893 2893 if ((self._bindir == self._pythondir) and
2894 2894 (self._bindir != self._tmpbindir)):
2895 2895 # The pythondir has been inferred from --with-hg flag.
2896 2896 # We cannot expect anything sensible here.
2897 2897 return
2898 2898 expecthg = os.path.join(self._pythondir, b'mercurial')
2899 2899 actualhg = self._gethgpath()
2900 2900 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2901 2901 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2902 2902 ' (expected %s)\n'
2903 2903 % (verb, actualhg, expecthg))
2904 2904 def _gethgpath(self):
2905 2905 """Return the path to the mercurial package that is actually found by
2906 2906 the current Python interpreter."""
2907 2907 if self._hgpath is not None:
2908 2908 return self._hgpath
2909 2909
2910 2910 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2911 2911 cmd = cmd % PYTHON
2912 2912 if PYTHON3:
2913 2913 cmd = _strpath(cmd)
2914 2914 pipe = os.popen(cmd)
2915 2915 try:
2916 2916 self._hgpath = _bytespath(pipe.read().strip())
2917 2917 finally:
2918 2918 pipe.close()
2919 2919
2920 2920 return self._hgpath
2921 2921
2922 2922 def _installchg(self):
2923 2923 """Install chg into the test environment"""
2924 2924 vlog('# Performing temporary installation of CHG')
2925 2925 assert os.path.dirname(self._bindir) == self._installdir
2926 2926 assert self._hgroot, 'must be called after _installhg()'
2927 2927 cmd = (b'"%(make)s" clean install PREFIX="%(prefix)s"'
2928 2928 % {b'make': 'make', # TODO: switch by option or environment?
2929 2929 b'prefix': self._installdir})
2930 2930 cwd = os.path.join(self._hgroot, b'contrib', b'chg')
2931 2931 vlog("# Running", cmd)
2932 2932 proc = subprocess.Popen(cmd, shell=True, cwd=cwd,
2933 2933 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
2934 2934 stderr=subprocess.STDOUT)
2935 2935 out, _err = proc.communicate()
2936 2936 if proc.returncode != 0:
2937 2937 if PYTHON3:
2938 2938 sys.stdout.buffer.write(out)
2939 2939 else:
2940 2940 sys.stdout.write(out)
2941 2941 sys.exit(1)
2942 2942
2943 2943 def _outputcoverage(self):
2944 2944 """Produce code coverage output."""
2945 2945 import coverage
2946 2946 coverage = coverage.coverage
2947 2947
2948 2948 vlog('# Producing coverage report')
2949 2949 # chdir is the easiest way to get short, relative paths in the
2950 2950 # output.
2951 2951 os.chdir(self._hgroot)
2952 2952 covdir = os.path.join(self._installdir, '..', 'coverage')
2953 2953 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2954 2954
2955 2955 # Map install directory paths back to source directory.
2956 2956 cov.config.paths['srcdir'] = ['.', self._pythondir]
2957 2957
2958 2958 cov.combine()
2959 2959
2960 2960 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2961 2961 cov.report(ignore_errors=True, omit=omit)
2962 2962
2963 2963 if self.options.htmlcov:
2964 2964 htmldir = os.path.join(self._outputdir, 'htmlcov')
2965 2965 cov.html_report(directory=htmldir, omit=omit)
2966 2966 if self.options.annotate:
2967 2967 adir = os.path.join(self._outputdir, 'annotated')
2968 2968 if not os.path.isdir(adir):
2969 2969 os.mkdir(adir)
2970 2970 cov.annotate(directory=adir, omit=omit)
2971 2971
2972 2972 def _findprogram(self, program):
2973 2973 """Search PATH for a executable program"""
2974 2974 dpb = _bytespath(os.defpath)
2975 2975 sepb = _bytespath(os.pathsep)
2976 2976 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2977 2977 name = os.path.join(p, program)
2978 2978 if os.name == 'nt' or os.access(name, os.X_OK):
2979 2979 return name
2980 2980 return None
2981 2981
2982 2982 def _checktools(self):
2983 2983 """Ensure tools required to run tests are present."""
2984 2984 for p in self.REQUIREDTOOLS:
2985 2985 if os.name == 'nt' and not p.endswith('.exe'):
2986 2986 p += '.exe'
2987 2987 found = self._findprogram(p)
2988 2988 if found:
2989 2989 vlog("# Found prerequisite", p, "at", found)
2990 2990 else:
2991 2991 print("WARNING: Did not find prerequisite tool: %s " %
2992 2992 p.decode("utf-8"))
2993 2993
2994 2994 def aggregateexceptions(path):
2995 2995 exceptions = collections.Counter()
2996 2996
2997 2997 for f in os.listdir(path):
2998 2998 with open(os.path.join(path, f), 'rb') as fh:
2999 2999 data = fh.read().split(b'\0')
3000 3000 if len(data) != 4:
3001 3001 continue
3002 3002
3003 3003 exc, mainframe, hgframe, hgline = data
3004 3004 exc = exc.decode('utf-8')
3005 3005 mainframe = mainframe.decode('utf-8')
3006 3006 hgframe = hgframe.decode('utf-8')
3007 3007 hgline = hgline.decode('utf-8')
3008 3008 exceptions[(hgframe, hgline, exc)] += 1
3009 3009
3010 3010 return exceptions
3011 3011
3012 3012 if __name__ == '__main__':
3013 3013 runner = TestRunner()
3014 3014
3015 3015 try:
3016 3016 import msvcrt
3017 3017 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
3018 3018 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
3019 3019 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
3020 3020 except ImportError:
3021 3021 pass
3022 3022
3023 3023 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now