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