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