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