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