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