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