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