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