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