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