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