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