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