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