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