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