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