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