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