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