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