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