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