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