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