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