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