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