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