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