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