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