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