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