##// END OF EJS Templates
run-tests: drop redundant assignment to BINDIR...
Yuya Nishihara -
r28098:d7b0e81b default
parent child Browse files
Show More
@@ -1,2386 +1,2385
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 print_function
47 47
48 48 from distutils import version
49 49 import difflib
50 50 import errno
51 51 import optparse
52 52 import os
53 53 import shutil
54 54 import subprocess
55 55 import signal
56 56 import socket
57 57 import sys
58 58 import tempfile
59 59 import time
60 60 import random
61 61 import re
62 62 import threading
63 63 import killdaemons as killmod
64 64 try:
65 65 import Queue as queue
66 66 except ImportError:
67 67 import queue
68 68 from xml.dom import minidom
69 69 import unittest
70 70
71 71 osenvironb = getattr(os, 'environb', os.environ)
72 72
73 73 try:
74 74 import json
75 75 except ImportError:
76 76 try:
77 77 import simplejson as json
78 78 except ImportError:
79 79 json = None
80 80
81 81 processlock = threading.Lock()
82 82
83 83 if sys.version_info > (3, 5, 0):
84 84 PYTHON3 = True
85 85 xrange = range # we use xrange in one place, and we'd rather not use range
86 86 def _bytespath(p):
87 87 return p.encode('utf-8')
88 88
89 89 def _strpath(p):
90 90 return p.decode('utf-8')
91 91
92 92 elif sys.version_info >= (3, 0, 0):
93 93 print('%s is only supported on Python 3.5+ and 2.6-2.7, not %s' %
94 94 (sys.argv[0], '.'.join(str(v) for v in sys.version_info[:3])))
95 95 sys.exit(70) # EX_SOFTWARE from `man 3 sysexit`
96 96 else:
97 97 PYTHON3 = False
98 98
99 99 # In python 2.x, path operations are generally done using
100 100 # bytestrings by default, so we don't have to do any extra
101 101 # fiddling there. We define the wrapper functions anyway just to
102 102 # help keep code consistent between platforms.
103 103 def _bytespath(p):
104 104 return p
105 105
106 106 _strpath = _bytespath
107 107
108 108 # For Windows support
109 109 wifexited = getattr(os, "WIFEXITED", lambda x: False)
110 110
111 111 def checkportisavailable(port):
112 112 """return true if a port seems free to bind on localhost"""
113 113 try:
114 114 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
115 115 s.bind(('localhost', port))
116 116 s.close()
117 117 return True
118 118 except socket.error as exc:
119 119 if not exc.errno == errno.EADDRINUSE:
120 120 raise
121 121 return False
122 122
123 123 closefds = os.name == 'posix'
124 124 def Popen4(cmd, wd, timeout, env=None):
125 125 processlock.acquire()
126 126 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
127 127 close_fds=closefds,
128 128 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
129 129 stderr=subprocess.STDOUT)
130 130 processlock.release()
131 131
132 132 p.fromchild = p.stdout
133 133 p.tochild = p.stdin
134 134 p.childerr = p.stderr
135 135
136 136 p.timeout = False
137 137 if timeout:
138 138 def t():
139 139 start = time.time()
140 140 while time.time() - start < timeout and p.returncode is None:
141 141 time.sleep(.1)
142 142 p.timeout = True
143 143 if p.returncode is None:
144 144 terminate(p)
145 145 threading.Thread(target=t).start()
146 146
147 147 return p
148 148
149 149 PYTHON = _bytespath(sys.executable.replace('\\', '/'))
150 150 IMPL_PATH = b'PYTHONPATH'
151 151 if 'java' in sys.platform:
152 152 IMPL_PATH = b'JYTHONPATH'
153 153
154 154 defaults = {
155 155 'jobs': ('HGTEST_JOBS', 1),
156 156 'timeout': ('HGTEST_TIMEOUT', 180),
157 157 'slowtimeout': ('HGTEST_SLOWTIMEOUT', 500),
158 158 'port': ('HGTEST_PORT', 20059),
159 159 'shell': ('HGTEST_SHELL', 'sh'),
160 160 }
161 161
162 162 def parselistfiles(files, listtype, warn=True):
163 163 entries = dict()
164 164 for filename in files:
165 165 try:
166 166 path = os.path.expanduser(os.path.expandvars(filename))
167 167 f = open(path, "rb")
168 168 except IOError as err:
169 169 if err.errno != errno.ENOENT:
170 170 raise
171 171 if warn:
172 172 print("warning: no such %s file: %s" % (listtype, filename))
173 173 continue
174 174
175 175 for line in f.readlines():
176 176 line = line.split(b'#', 1)[0].strip()
177 177 if line:
178 178 entries[line] = filename
179 179
180 180 f.close()
181 181 return entries
182 182
183 183 def getparser():
184 184 """Obtain the OptionParser used by the CLI."""
185 185 parser = optparse.OptionParser("%prog [options] [tests]")
186 186
187 187 # keep these sorted
188 188 parser.add_option("--blacklist", action="append",
189 189 help="skip tests listed in the specified blacklist file")
190 190 parser.add_option("--whitelist", action="append",
191 191 help="always run tests listed in the specified whitelist file")
192 192 parser.add_option("--changed", type="string",
193 193 help="run tests that are changed in parent rev or working directory")
194 194 parser.add_option("-C", "--annotate", action="store_true",
195 195 help="output files annotated with coverage")
196 196 parser.add_option("-c", "--cover", action="store_true",
197 197 help="print a test coverage report")
198 198 parser.add_option("-d", "--debug", action="store_true",
199 199 help="debug mode: write output of test scripts to console"
200 200 " rather than capturing and diffing it (disables timeout)")
201 201 parser.add_option("-f", "--first", action="store_true",
202 202 help="exit on the first test failure")
203 203 parser.add_option("-H", "--htmlcov", action="store_true",
204 204 help="create an HTML report of the coverage of the files")
205 205 parser.add_option("-i", "--interactive", action="store_true",
206 206 help="prompt to accept changed output")
207 207 parser.add_option("-j", "--jobs", type="int",
208 208 help="number of jobs to run in parallel"
209 209 " (default: $%s or %d)" % defaults['jobs'])
210 210 parser.add_option("--keep-tmpdir", action="store_true",
211 211 help="keep temporary directory after running tests")
212 212 parser.add_option("-k", "--keywords",
213 213 help="run tests matching keywords")
214 214 parser.add_option("-l", "--local", action="store_true",
215 215 help="shortcut for --with-hg=<testdir>/../hg")
216 216 parser.add_option("--loop", action="store_true",
217 217 help="loop tests repeatedly")
218 218 parser.add_option("--runs-per-test", type="int", dest="runs_per_test",
219 219 help="run each test N times (default=1)", default=1)
220 220 parser.add_option("-n", "--nodiff", action="store_true",
221 221 help="skip showing test changes")
222 222 parser.add_option("-p", "--port", type="int",
223 223 help="port on which servers should listen"
224 224 " (default: $%s or %d)" % defaults['port'])
225 225 parser.add_option("--compiler", type="string",
226 226 help="compiler to build with")
227 227 parser.add_option("--pure", action="store_true",
228 228 help="use pure Python code instead of C extensions")
229 229 parser.add_option("-R", "--restart", action="store_true",
230 230 help="restart at last error")
231 231 parser.add_option("-r", "--retest", action="store_true",
232 232 help="retest failed tests")
233 233 parser.add_option("-S", "--noskips", action="store_true",
234 234 help="don't report skip tests verbosely")
235 235 parser.add_option("--shell", type="string",
236 236 help="shell to use (default: $%s or %s)" % defaults['shell'])
237 237 parser.add_option("-t", "--timeout", type="int",
238 238 help="kill errant tests after TIMEOUT seconds"
239 239 " (default: $%s or %d)" % defaults['timeout'])
240 240 parser.add_option("--slowtimeout", type="int",
241 241 help="kill errant slow tests after SLOWTIMEOUT seconds"
242 242 " (default: $%s or %d)" % defaults['slowtimeout'])
243 243 parser.add_option("--time", action="store_true",
244 244 help="time how long each test takes")
245 245 parser.add_option("--json", action="store_true",
246 246 help="store test result data in 'report.json' file")
247 247 parser.add_option("--tmpdir", type="string",
248 248 help="run tests in the given temporary directory"
249 249 " (implies --keep-tmpdir)")
250 250 parser.add_option("-v", "--verbose", action="store_true",
251 251 help="output verbose messages")
252 252 parser.add_option("--xunit", type="string",
253 253 help="record xunit results at specified path")
254 254 parser.add_option("--view", type="string",
255 255 help="external diff viewer")
256 256 parser.add_option("--with-hg", type="string",
257 257 metavar="HG",
258 258 help="test using specified hg script rather than a "
259 259 "temporary installation")
260 260 parser.add_option("-3", "--py3k-warnings", action="store_true",
261 261 help="enable Py3k warnings on Python 2.6+")
262 262 parser.add_option('--extra-config-opt', action="append",
263 263 help='set the given config opt in the test hgrc')
264 264 parser.add_option('--random', action="store_true",
265 265 help='run tests in random order')
266 266 parser.add_option('--profile-runner', action='store_true',
267 267 help='run statprof on run-tests')
268 268 parser.add_option('--allow-slow-tests', action='store_true',
269 269 help='allow extremely slow tests')
270 270 parser.add_option('--showchannels', action='store_true',
271 271 help='show scheduling channels')
272 272
273 273 for option, (envvar, default) in defaults.items():
274 274 defaults[option] = type(default)(os.environ.get(envvar, default))
275 275 parser.set_defaults(**defaults)
276 276
277 277 return parser
278 278
279 279 def parseargs(args, parser):
280 280 """Parse arguments with our OptionParser and validate results."""
281 281 (options, args) = parser.parse_args(args)
282 282
283 283 # jython is always pure
284 284 if 'java' in sys.platform or '__pypy__' in sys.modules:
285 285 options.pure = True
286 286
287 287 if options.with_hg:
288 288 options.with_hg = os.path.realpath(
289 289 os.path.expanduser(_bytespath(options.with_hg)))
290 290 if not (os.path.isfile(options.with_hg) and
291 291 os.access(options.with_hg, os.X_OK)):
292 292 parser.error('--with-hg must specify an executable hg script')
293 293 if not os.path.basename(options.with_hg) == b'hg':
294 294 sys.stderr.write('warning: --with-hg should specify an hg script\n')
295 295 if options.local:
296 296 testdir = os.path.dirname(_bytespath(os.path.realpath(sys.argv[0])))
297 297 hgbin = os.path.join(os.path.dirname(testdir), b'hg')
298 298 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
299 299 parser.error('--local specified, but %r not found or not executable'
300 300 % hgbin)
301 301 options.with_hg = hgbin
302 302
303 303 options.anycoverage = options.cover or options.annotate or options.htmlcov
304 304 if options.anycoverage:
305 305 try:
306 306 import coverage
307 307 covver = version.StrictVersion(coverage.__version__).version
308 308 if covver < (3, 3):
309 309 parser.error('coverage options require coverage 3.3 or later')
310 310 except ImportError:
311 311 parser.error('coverage options now require the coverage package')
312 312
313 313 if options.anycoverage and options.local:
314 314 # this needs some path mangling somewhere, I guess
315 315 parser.error("sorry, coverage options do not work when --local "
316 316 "is specified")
317 317
318 318 if options.anycoverage and options.with_hg:
319 319 parser.error("sorry, coverage options do not work when --with-hg "
320 320 "is specified")
321 321
322 322 global verbose
323 323 if options.verbose:
324 324 verbose = ''
325 325
326 326 if options.tmpdir:
327 327 options.tmpdir = os.path.expanduser(options.tmpdir)
328 328
329 329 if options.jobs < 1:
330 330 parser.error('--jobs must be positive')
331 331 if options.interactive and options.debug:
332 332 parser.error("-i/--interactive and -d/--debug are incompatible")
333 333 if options.debug:
334 334 if options.timeout != defaults['timeout']:
335 335 sys.stderr.write(
336 336 'warning: --timeout option ignored with --debug\n')
337 337 if options.slowtimeout != defaults['slowtimeout']:
338 338 sys.stderr.write(
339 339 'warning: --slowtimeout option ignored with --debug\n')
340 340 options.timeout = 0
341 341 options.slowtimeout = 0
342 342 if options.py3k_warnings:
343 343 if PYTHON3:
344 344 parser.error(
345 345 '--py3k-warnings can only be used on Python 2.6 and 2.7')
346 346 if options.blacklist:
347 347 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
348 348 if options.whitelist:
349 349 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
350 350 else:
351 351 options.whitelisted = {}
352 352
353 353 if options.showchannels:
354 354 options.nodiff = True
355 355
356 356 return (options, args)
357 357
358 358 def rename(src, dst):
359 359 """Like os.rename(), trade atomicity and opened files friendliness
360 360 for existing destination support.
361 361 """
362 362 shutil.copy(src, dst)
363 363 os.remove(src)
364 364
365 365 _unified_diff = difflib.unified_diff
366 366 if PYTHON3:
367 367 import functools
368 368 _unified_diff = functools.partial(difflib.diff_bytes, difflib.unified_diff)
369 369
370 370 def getdiff(expected, output, ref, err):
371 371 servefail = False
372 372 lines = []
373 373 for line in _unified_diff(expected, output, ref, err):
374 374 if line.startswith(b'+++') or line.startswith(b'---'):
375 375 line = line.replace(b'\\', b'/')
376 376 if line.endswith(b' \n'):
377 377 line = line[:-2] + b'\n'
378 378 lines.append(line)
379 379 if not servefail and line.startswith(
380 380 b'+ abort: child process failed to start'):
381 381 servefail = True
382 382
383 383 return servefail, lines
384 384
385 385 verbose = False
386 386 def vlog(*msg):
387 387 """Log only when in verbose mode."""
388 388 if verbose is False:
389 389 return
390 390
391 391 return log(*msg)
392 392
393 393 # Bytes that break XML even in a CDATA block: control characters 0-31
394 394 # sans \t, \n and \r
395 395 CDATA_EVIL = re.compile(br"[\000-\010\013\014\016-\037]")
396 396
397 397 def cdatasafe(data):
398 398 """Make a string safe to include in a CDATA block.
399 399
400 400 Certain control characters are illegal in a CDATA block, and
401 401 there's no way to include a ]]> in a CDATA either. This function
402 402 replaces illegal bytes with ? and adds a space between the ]] so
403 403 that it won't break the CDATA block.
404 404 """
405 405 return CDATA_EVIL.sub(b'?', data).replace(b']]>', b'] ]>')
406 406
407 407 def log(*msg):
408 408 """Log something to stdout.
409 409
410 410 Arguments are strings to print.
411 411 """
412 412 with iolock:
413 413 if verbose:
414 414 print(verbose, end=' ')
415 415 for m in msg:
416 416 print(m, end=' ')
417 417 print()
418 418 sys.stdout.flush()
419 419
420 420 def terminate(proc):
421 421 """Terminate subprocess (with fallback for Python versions < 2.6)"""
422 422 vlog('# Terminating process %d' % proc.pid)
423 423 try:
424 424 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
425 425 except OSError:
426 426 pass
427 427
428 428 def killdaemons(pidfile):
429 429 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
430 430 logfn=vlog)
431 431
432 432 class Test(unittest.TestCase):
433 433 """Encapsulates a single, runnable test.
434 434
435 435 While this class conforms to the unittest.TestCase API, it differs in that
436 436 instances need to be instantiated manually. (Typically, unittest.TestCase
437 437 classes are instantiated automatically by scanning modules.)
438 438 """
439 439
440 440 # Status code reserved for skipped tests (used by hghave).
441 441 SKIPPED_STATUS = 80
442 442
443 443 def __init__(self, path, tmpdir, keeptmpdir=False,
444 444 debug=False,
445 445 timeout=defaults['timeout'],
446 446 startport=defaults['port'], extraconfigopts=None,
447 447 py3kwarnings=False, shell=None,
448 448 slowtimeout=defaults['slowtimeout']):
449 449 """Create a test from parameters.
450 450
451 451 path is the full path to the file defining the test.
452 452
453 453 tmpdir is the main temporary directory to use for this test.
454 454
455 455 keeptmpdir determines whether to keep the test's temporary directory
456 456 after execution. It defaults to removal (False).
457 457
458 458 debug mode will make the test execute verbosely, with unfiltered
459 459 output.
460 460
461 461 timeout controls the maximum run time of the test. It is ignored when
462 462 debug is True. See slowtimeout for tests with #require slow.
463 463
464 464 slowtimeout overrides timeout if the test has #require slow.
465 465
466 466 startport controls the starting port number to use for this test. Each
467 467 test will reserve 3 port numbers for execution. It is the caller's
468 468 responsibility to allocate a non-overlapping port range to Test
469 469 instances.
470 470
471 471 extraconfigopts is an iterable of extra hgrc config options. Values
472 472 must have the form "key=value" (something understood by hgrc). Values
473 473 of the form "foo.key=value" will result in "[foo] key=value".
474 474
475 475 py3kwarnings enables Py3k warnings.
476 476
477 477 shell is the shell to execute tests in.
478 478 """
479 479 self.path = path
480 480 self.bname = os.path.basename(path)
481 481 self.name = _strpath(self.bname)
482 482 self._testdir = os.path.dirname(path)
483 483 self.errpath = os.path.join(self._testdir, b'%s.err' % self.bname)
484 484
485 485 self._threadtmp = tmpdir
486 486 self._keeptmpdir = keeptmpdir
487 487 self._debug = debug
488 488 self._timeout = timeout
489 489 self._slowtimeout = slowtimeout
490 490 self._startport = startport
491 491 self._extraconfigopts = extraconfigopts or []
492 492 self._py3kwarnings = py3kwarnings
493 493 self._shell = _bytespath(shell)
494 494
495 495 self._aborted = False
496 496 self._daemonpids = []
497 497 self._finished = None
498 498 self._ret = None
499 499 self._out = None
500 500 self._skipped = None
501 501 self._testtmp = None
502 502
503 503 # If we're not in --debug mode and reference output file exists,
504 504 # check test output against it.
505 505 if debug:
506 506 self._refout = None # to match "out is None"
507 507 elif os.path.exists(self.refpath):
508 508 f = open(self.refpath, 'rb')
509 509 self._refout = f.read().splitlines(True)
510 510 f.close()
511 511 else:
512 512 self._refout = []
513 513
514 514 # needed to get base class __repr__ running
515 515 @property
516 516 def _testMethodName(self):
517 517 return self.name
518 518
519 519 def __str__(self):
520 520 return self.name
521 521
522 522 def shortDescription(self):
523 523 return self.name
524 524
525 525 def setUp(self):
526 526 """Tasks to perform before run()."""
527 527 self._finished = False
528 528 self._ret = None
529 529 self._out = None
530 530 self._skipped = None
531 531
532 532 try:
533 533 os.mkdir(self._threadtmp)
534 534 except OSError as e:
535 535 if e.errno != errno.EEXIST:
536 536 raise
537 537
538 538 self._testtmp = os.path.join(self._threadtmp,
539 539 os.path.basename(self.path))
540 540 os.mkdir(self._testtmp)
541 541
542 542 # Remove any previous output files.
543 543 if os.path.exists(self.errpath):
544 544 try:
545 545 os.remove(self.errpath)
546 546 except OSError as e:
547 547 # We might have raced another test to clean up a .err
548 548 # file, so ignore ENOENT when removing a previous .err
549 549 # file.
550 550 if e.errno != errno.ENOENT:
551 551 raise
552 552
553 553 def run(self, result):
554 554 """Run this test and report results against a TestResult instance."""
555 555 # This function is extremely similar to unittest.TestCase.run(). Once
556 556 # we require Python 2.7 (or at least its version of unittest), this
557 557 # function can largely go away.
558 558 self._result = result
559 559 result.startTest(self)
560 560 try:
561 561 try:
562 562 self.setUp()
563 563 except (KeyboardInterrupt, SystemExit):
564 564 self._aborted = True
565 565 raise
566 566 except Exception:
567 567 result.addError(self, sys.exc_info())
568 568 return
569 569
570 570 success = False
571 571 try:
572 572 self.runTest()
573 573 except KeyboardInterrupt:
574 574 self._aborted = True
575 575 raise
576 576 except SkipTest as e:
577 577 result.addSkip(self, str(e))
578 578 # The base class will have already counted this as a
579 579 # test we "ran", but we want to exclude skipped tests
580 580 # from those we count towards those run.
581 581 result.testsRun -= 1
582 582 except IgnoreTest as e:
583 583 result.addIgnore(self, str(e))
584 584 # As with skips, ignores also should be excluded from
585 585 # the number of tests executed.
586 586 result.testsRun -= 1
587 587 except WarnTest as e:
588 588 result.addWarn(self, str(e))
589 589 except ReportedTest as e:
590 590 pass
591 591 except self.failureException as e:
592 592 # This differs from unittest in that we don't capture
593 593 # the stack trace. This is for historical reasons and
594 594 # this decision could be revisited in the future,
595 595 # especially for PythonTest instances.
596 596 if result.addFailure(self, str(e)):
597 597 success = True
598 598 except Exception:
599 599 result.addError(self, sys.exc_info())
600 600 else:
601 601 success = True
602 602
603 603 try:
604 604 self.tearDown()
605 605 except (KeyboardInterrupt, SystemExit):
606 606 self._aborted = True
607 607 raise
608 608 except Exception:
609 609 result.addError(self, sys.exc_info())
610 610 success = False
611 611
612 612 if success:
613 613 result.addSuccess(self)
614 614 finally:
615 615 result.stopTest(self, interrupted=self._aborted)
616 616
617 617 def runTest(self):
618 618 """Run this test instance.
619 619
620 620 This will return a tuple describing the result of the test.
621 621 """
622 622 env = self._getenv()
623 623 self._daemonpids.append(env['DAEMON_PIDS'])
624 624 self._createhgrc(env['HGRCPATH'])
625 625
626 626 vlog('# Test', self.name)
627 627
628 628 ret, out = self._run(env)
629 629 self._finished = True
630 630 self._ret = ret
631 631 self._out = out
632 632
633 633 def describe(ret):
634 634 if ret < 0:
635 635 return 'killed by signal: %d' % -ret
636 636 return 'returned error code %d' % ret
637 637
638 638 self._skipped = False
639 639
640 640 if ret == self.SKIPPED_STATUS:
641 641 if out is None: # Debug mode, nothing to parse.
642 642 missing = ['unknown']
643 643 failed = None
644 644 else:
645 645 missing, failed = TTest.parsehghaveoutput(out)
646 646
647 647 if not missing:
648 648 missing = ['skipped']
649 649
650 650 if failed:
651 651 self.fail('hg have failed checking for %s' % failed[-1])
652 652 else:
653 653 self._skipped = True
654 654 raise SkipTest(missing[-1])
655 655 elif ret == 'timeout':
656 656 self.fail('timed out')
657 657 elif ret is False:
658 658 raise WarnTest('no result code from test')
659 659 elif out != self._refout:
660 660 # Diff generation may rely on written .err file.
661 661 if (ret != 0 or out != self._refout) and not self._skipped \
662 662 and not self._debug:
663 663 f = open(self.errpath, 'wb')
664 664 for line in out:
665 665 f.write(line)
666 666 f.close()
667 667
668 668 # The result object handles diff calculation for us.
669 669 if self._result.addOutputMismatch(self, ret, out, self._refout):
670 670 # change was accepted, skip failing
671 671 return
672 672
673 673 if ret:
674 674 msg = 'output changed and ' + describe(ret)
675 675 else:
676 676 msg = 'output changed'
677 677
678 678 self.fail(msg)
679 679 elif ret:
680 680 self.fail(describe(ret))
681 681
682 682 def tearDown(self):
683 683 """Tasks to perform after run()."""
684 684 for entry in self._daemonpids:
685 685 killdaemons(entry)
686 686 self._daemonpids = []
687 687
688 688 if self._keeptmpdir:
689 689 log('\nKeeping testtmp dir: %s\nKeeping threadtmp dir: %s' %
690 690 (self._testtmp, self._threadtmp))
691 691 else:
692 692 shutil.rmtree(self._testtmp, True)
693 693 shutil.rmtree(self._threadtmp, True)
694 694
695 695 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
696 696 and not self._debug and self._out:
697 697 f = open(self.errpath, 'wb')
698 698 for line in self._out:
699 699 f.write(line)
700 700 f.close()
701 701
702 702 vlog("# Ret was:", self._ret, '(%s)' % self.name)
703 703
704 704 def _run(self, env):
705 705 # This should be implemented in child classes to run tests.
706 706 raise SkipTest('unknown test type')
707 707
708 708 def abort(self):
709 709 """Terminate execution of this test."""
710 710 self._aborted = True
711 711
712 712 def _getreplacements(self):
713 713 """Obtain a mapping of text replacements to apply to test output.
714 714
715 715 Test output needs to be normalized so it can be compared to expected
716 716 output. This function defines how some of that normalization will
717 717 occur.
718 718 """
719 719 r = [
720 720 (br':%d\b' % self._startport, b':$HGPORT'),
721 721 (br':%d\b' % (self._startport + 1), b':$HGPORT1'),
722 722 (br':%d\b' % (self._startport + 2), b':$HGPORT2'),
723 723 (br':%d\b' % (self._startport + 2), b':$HGPORT3'),
724 724 (br':%d\b' % (self._startport + 2), b':$HGPORT4'),
725 725 (br'(?m)^(saved backup bundle to .*\.hg)( \(glob\))?$',
726 726 br'\1 (glob)'),
727 727 ]
728 728 r.append((self._escapepath(self._testtmp), b'$TESTTMP'))
729 729
730 730 return r
731 731
732 732 def _escapepath(self, p):
733 733 if os.name == 'nt':
734 734 return (
735 735 (b''.join(c.isalpha() and b'[%s%s]' % (c.lower(), c.upper()) or
736 736 c in b'/\\' and br'[/\\]' or c.isdigit() and c or b'\\' + c
737 737 for c in p))
738 738 )
739 739 else:
740 740 return re.escape(p)
741 741
742 742 def _getenv(self):
743 743 """Obtain environment variables to use during test execution."""
744 744 env = os.environ.copy()
745 745 env['TESTTMP'] = self._testtmp
746 746 env['HOME'] = self._testtmp
747 747 env["HGPORT"] = str(self._startport)
748 748 env["HGPORT1"] = str(self._startport + 1)
749 749 env["HGPORT2"] = str(self._startport + 2)
750 750 env["HGPORT3"] = str(self._startport + 3)
751 751 env["HGPORT4"] = str(self._startport + 4)
752 752 env["HGRCPATH"] = os.path.join(self._threadtmp, b'.hgrc')
753 753 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, b'daemon.pids')
754 754 env["HGEDITOR"] = ('"' + sys.executable + '"'
755 755 + ' -c "import sys; sys.exit(0)"')
756 756 env["HGMERGE"] = "internal:merge"
757 757 env["HGUSER"] = "test"
758 758 env["HGENCODING"] = "ascii"
759 759 env["HGENCODINGMODE"] = "strict"
760 760
761 761 # Reset some environment variables to well-known values so that
762 762 # the tests produce repeatable output.
763 763 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
764 764 env['TZ'] = 'GMT'
765 765 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
766 766 env['COLUMNS'] = '80'
767 767 env['TERM'] = 'xterm'
768 768
769 769 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
770 770 'NO_PROXY').split():
771 771 if k in env:
772 772 del env[k]
773 773
774 774 # unset env related to hooks
775 775 for k in env.keys():
776 776 if k.startswith('HG_'):
777 777 del env[k]
778 778
779 779 return env
780 780
781 781 def _createhgrc(self, path):
782 782 """Create an hgrc file for this test."""
783 783 hgrc = open(path, 'wb')
784 784 hgrc.write(b'[ui]\n')
785 785 hgrc.write(b'slash = True\n')
786 786 hgrc.write(b'interactive = False\n')
787 787 hgrc.write(b'mergemarkers = detailed\n')
788 788 hgrc.write(b'promptecho = True\n')
789 789 hgrc.write(b'[defaults]\n')
790 790 hgrc.write(b'backout = -d "0 0"\n')
791 791 hgrc.write(b'commit = -d "0 0"\n')
792 792 hgrc.write(b'shelve = --date "0 0"\n')
793 793 hgrc.write(b'tag = -d "0 0"\n')
794 794 hgrc.write(b'[devel]\n')
795 795 hgrc.write(b'all-warnings = true\n')
796 796 hgrc.write(b'[largefiles]\n')
797 797 hgrc.write(b'usercache = %s\n' %
798 798 (os.path.join(self._testtmp, b'.cache/largefiles')))
799 799
800 800 for opt in self._extraconfigopts:
801 801 section, key = opt.split('.', 1)
802 802 assert '=' in key, ('extra config opt %s must '
803 803 'have an = for assignment' % opt)
804 804 hgrc.write(b'[%s]\n%s\n' % (section, key))
805 805 hgrc.close()
806 806
807 807 def fail(self, msg):
808 808 # unittest differentiates between errored and failed.
809 809 # Failed is denoted by AssertionError (by default at least).
810 810 raise AssertionError(msg)
811 811
812 812 def _runcommand(self, cmd, env, normalizenewlines=False):
813 813 """Run command in a sub-process, capturing the output (stdout and
814 814 stderr).
815 815
816 816 Return a tuple (exitcode, output). output is None in debug mode.
817 817 """
818 818 if self._debug:
819 819 proc = subprocess.Popen(cmd, shell=True, cwd=self._testtmp,
820 820 env=env)
821 821 ret = proc.wait()
822 822 return (ret, None)
823 823
824 824 proc = Popen4(cmd, self._testtmp, self._timeout, env)
825 825 def cleanup():
826 826 terminate(proc)
827 827 ret = proc.wait()
828 828 if ret == 0:
829 829 ret = signal.SIGTERM << 8
830 830 killdaemons(env['DAEMON_PIDS'])
831 831 return ret
832 832
833 833 output = ''
834 834 proc.tochild.close()
835 835
836 836 try:
837 837 output = proc.fromchild.read()
838 838 except KeyboardInterrupt:
839 839 vlog('# Handling keyboard interrupt')
840 840 cleanup()
841 841 raise
842 842
843 843 ret = proc.wait()
844 844 if wifexited(ret):
845 845 ret = os.WEXITSTATUS(ret)
846 846
847 847 if proc.timeout:
848 848 ret = 'timeout'
849 849
850 850 if ret:
851 851 killdaemons(env['DAEMON_PIDS'])
852 852
853 853 for s, r in self._getreplacements():
854 854 output = re.sub(s, r, output)
855 855
856 856 if normalizenewlines:
857 857 output = output.replace('\r\n', '\n')
858 858
859 859 return ret, output.splitlines(True)
860 860
861 861 class PythonTest(Test):
862 862 """A Python-based test."""
863 863
864 864 @property
865 865 def refpath(self):
866 866 return os.path.join(self._testdir, b'%s.out' % self.bname)
867 867
868 868 def _run(self, env):
869 869 py3kswitch = self._py3kwarnings and b' -3' or b''
870 870 cmd = b'%s%s "%s"' % (PYTHON, py3kswitch, self.path)
871 871 vlog("# Running", cmd)
872 872 normalizenewlines = os.name == 'nt'
873 873 result = self._runcommand(cmd, env,
874 874 normalizenewlines=normalizenewlines)
875 875 if self._aborted:
876 876 raise KeyboardInterrupt()
877 877
878 878 return result
879 879
880 880 # This script may want to drop globs from lines matching these patterns on
881 881 # Windows, but check-code.py wants a glob on these lines unconditionally. Don't
882 882 # warn if that is the case for anything matching these lines.
883 883 checkcodeglobpats = [
884 884 re.compile(br'^pushing to \$TESTTMP/.*[^)]$'),
885 885 re.compile(br'^moving \S+/.*[^)]$'),
886 886 re.compile(br'^pulling from \$TESTTMP/.*[^)]$')
887 887 ]
888 888
889 889 bchr = chr
890 890 if PYTHON3:
891 891 bchr = lambda x: bytes([x])
892 892
893 893 class TTest(Test):
894 894 """A "t test" is a test backed by a .t file."""
895 895
896 896 SKIPPED_PREFIX = 'skipped: '
897 897 FAILED_PREFIX = 'hghave check failed: '
898 898 NEEDESCAPE = re.compile(br'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
899 899
900 900 ESCAPESUB = re.compile(br'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
901 901 ESCAPEMAP = dict((bchr(i), br'\x%02x' % i) for i in range(256))
902 902 ESCAPEMAP.update({b'\\': b'\\\\', b'\r': br'\r'})
903 903
904 904 @property
905 905 def refpath(self):
906 906 return os.path.join(self._testdir, self.bname)
907 907
908 908 def _run(self, env):
909 909 f = open(self.path, 'rb')
910 910 lines = f.readlines()
911 911 f.close()
912 912
913 913 salt, script, after, expected = self._parsetest(lines)
914 914
915 915 # Write out the generated script.
916 916 fname = b'%s.sh' % self._testtmp
917 917 f = open(fname, 'wb')
918 918 for l in script:
919 919 f.write(l)
920 920 f.close()
921 921
922 922 cmd = b'%s "%s"' % (self._shell, fname)
923 923 vlog("# Running", cmd)
924 924
925 925 exitcode, output = self._runcommand(cmd, env)
926 926
927 927 if self._aborted:
928 928 raise KeyboardInterrupt()
929 929
930 930 # Do not merge output if skipped. Return hghave message instead.
931 931 # Similarly, with --debug, output is None.
932 932 if exitcode == self.SKIPPED_STATUS or output is None:
933 933 return exitcode, output
934 934
935 935 return self._processoutput(exitcode, output, salt, after, expected)
936 936
937 937 def _hghave(self, reqs):
938 938 # TODO do something smarter when all other uses of hghave are gone.
939 939 runtestdir = os.path.abspath(os.path.dirname(_bytespath(__file__)))
940 940 tdir = runtestdir.replace(b'\\', b'/')
941 941 proc = Popen4(b'%s -c "%s/hghave %s"' %
942 942 (self._shell, tdir, b' '.join(reqs)),
943 943 self._testtmp, 0, self._getenv())
944 944 stdout, stderr = proc.communicate()
945 945 ret = proc.wait()
946 946 if wifexited(ret):
947 947 ret = os.WEXITSTATUS(ret)
948 948 if ret == 2:
949 949 print(stdout)
950 950 sys.exit(1)
951 951
952 952 if ret != 0:
953 953 return False, stdout
954 954
955 955 if 'slow' in reqs:
956 956 self._timeout = self._slowtimeout
957 957 return True, None
958 958
959 959 def _parsetest(self, lines):
960 960 # We generate a shell script which outputs unique markers to line
961 961 # up script results with our source. These markers include input
962 962 # line number and the last return code.
963 963 salt = b"SALT%d" % time.time()
964 964 def addsalt(line, inpython):
965 965 if inpython:
966 966 script.append(b'%s %d 0\n' % (salt, line))
967 967 else:
968 968 script.append(b'echo %s %d $?\n' % (salt, line))
969 969
970 970 script = []
971 971
972 972 # After we run the shell script, we re-unify the script output
973 973 # with non-active parts of the source, with synchronization by our
974 974 # SALT line number markers. The after table contains the non-active
975 975 # components, ordered by line number.
976 976 after = {}
977 977
978 978 # Expected shell script output.
979 979 expected = {}
980 980
981 981 pos = prepos = -1
982 982
983 983 # True or False when in a true or false conditional section
984 984 skipping = None
985 985
986 986 # We keep track of whether or not we're in a Python block so we
987 987 # can generate the surrounding doctest magic.
988 988 inpython = False
989 989
990 990 if self._debug:
991 991 script.append(b'set -x\n')
992 992 if os.getenv('MSYSTEM'):
993 993 script.append(b'alias pwd="pwd -W"\n')
994 994
995 995 for n, l in enumerate(lines):
996 996 if not l.endswith(b'\n'):
997 997 l += b'\n'
998 998 if l.startswith(b'#require'):
999 999 lsplit = l.split()
1000 1000 if len(lsplit) < 2 or lsplit[0] != b'#require':
1001 1001 after.setdefault(pos, []).append(' !!! invalid #require\n')
1002 1002 haveresult, message = self._hghave(lsplit[1:])
1003 1003 if not haveresult:
1004 1004 script = [b'echo "%s"\nexit 80\n' % message]
1005 1005 break
1006 1006 after.setdefault(pos, []).append(l)
1007 1007 elif l.startswith(b'#if'):
1008 1008 lsplit = l.split()
1009 1009 if len(lsplit) < 2 or lsplit[0] != b'#if':
1010 1010 after.setdefault(pos, []).append(' !!! invalid #if\n')
1011 1011 if skipping is not None:
1012 1012 after.setdefault(pos, []).append(' !!! nested #if\n')
1013 1013 skipping = not self._hghave(lsplit[1:])[0]
1014 1014 after.setdefault(pos, []).append(l)
1015 1015 elif l.startswith(b'#else'):
1016 1016 if skipping is None:
1017 1017 after.setdefault(pos, []).append(' !!! missing #if\n')
1018 1018 skipping = not skipping
1019 1019 after.setdefault(pos, []).append(l)
1020 1020 elif l.startswith(b'#endif'):
1021 1021 if skipping is None:
1022 1022 after.setdefault(pos, []).append(' !!! missing #if\n')
1023 1023 skipping = None
1024 1024 after.setdefault(pos, []).append(l)
1025 1025 elif skipping:
1026 1026 after.setdefault(pos, []).append(l)
1027 1027 elif l.startswith(b' >>> '): # python inlines
1028 1028 after.setdefault(pos, []).append(l)
1029 1029 prepos = pos
1030 1030 pos = n
1031 1031 if not inpython:
1032 1032 # We've just entered a Python block. Add the header.
1033 1033 inpython = True
1034 1034 addsalt(prepos, False) # Make sure we report the exit code.
1035 1035 script.append(b'%s -m heredoctest <<EOF\n' % PYTHON)
1036 1036 addsalt(n, True)
1037 1037 script.append(l[2:])
1038 1038 elif l.startswith(b' ... '): # python inlines
1039 1039 after.setdefault(prepos, []).append(l)
1040 1040 script.append(l[2:])
1041 1041 elif l.startswith(b' $ '): # commands
1042 1042 if inpython:
1043 1043 script.append(b'EOF\n')
1044 1044 inpython = False
1045 1045 after.setdefault(pos, []).append(l)
1046 1046 prepos = pos
1047 1047 pos = n
1048 1048 addsalt(n, False)
1049 1049 cmd = l[4:].split()
1050 1050 if len(cmd) == 2 and cmd[0] == b'cd':
1051 1051 l = b' $ cd %s || exit 1\n' % cmd[1]
1052 1052 script.append(l[4:])
1053 1053 elif l.startswith(b' > '): # continuations
1054 1054 after.setdefault(prepos, []).append(l)
1055 1055 script.append(l[4:])
1056 1056 elif l.startswith(b' '): # results
1057 1057 # Queue up a list of expected results.
1058 1058 expected.setdefault(pos, []).append(l[2:])
1059 1059 else:
1060 1060 if inpython:
1061 1061 script.append(b'EOF\n')
1062 1062 inpython = False
1063 1063 # Non-command/result. Queue up for merged output.
1064 1064 after.setdefault(pos, []).append(l)
1065 1065
1066 1066 if inpython:
1067 1067 script.append(b'EOF\n')
1068 1068 if skipping is not None:
1069 1069 after.setdefault(pos, []).append(' !!! missing #endif\n')
1070 1070 addsalt(n + 1, False)
1071 1071
1072 1072 return salt, script, after, expected
1073 1073
1074 1074 def _processoutput(self, exitcode, output, salt, after, expected):
1075 1075 # Merge the script output back into a unified test.
1076 1076 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
1077 1077 if exitcode != 0:
1078 1078 warnonly = 3
1079 1079
1080 1080 pos = -1
1081 1081 postout = []
1082 1082 for l in output:
1083 1083 lout, lcmd = l, None
1084 1084 if salt in l:
1085 1085 lout, lcmd = l.split(salt, 1)
1086 1086
1087 1087 while lout:
1088 1088 if not lout.endswith(b'\n'):
1089 1089 lout += b' (no-eol)\n'
1090 1090
1091 1091 # Find the expected output at the current position.
1092 1092 el = None
1093 1093 if expected.get(pos, None):
1094 1094 el = expected[pos].pop(0)
1095 1095
1096 1096 r = TTest.linematch(el, lout)
1097 1097 if isinstance(r, str):
1098 1098 if r == '+glob':
1099 1099 lout = el[:-1] + ' (glob)\n'
1100 1100 r = '' # Warn only this line.
1101 1101 elif r == '-glob':
1102 1102 lout = ''.join(el.rsplit(' (glob)', 1))
1103 1103 r = '' # Warn only this line.
1104 1104 elif r == "retry":
1105 1105 postout.append(b' ' + el)
1106 1106 continue
1107 1107 else:
1108 1108 log('\ninfo, unknown linematch result: %r\n' % r)
1109 1109 r = False
1110 1110 if r:
1111 1111 postout.append(b' ' + el)
1112 1112 else:
1113 1113 if self.NEEDESCAPE(lout):
1114 1114 lout = TTest._stringescape(b'%s (esc)\n' %
1115 1115 lout.rstrip(b'\n'))
1116 1116 postout.append(b' ' + lout) # Let diff deal with it.
1117 1117 if r != '': # If line failed.
1118 1118 warnonly = 3 # for sure not
1119 1119 elif warnonly == 1: # Is "not yet" and line is warn only.
1120 1120 warnonly = 2 # Yes do warn.
1121 1121 break
1122 1122
1123 1123 # clean up any optional leftovers
1124 1124 while expected.get(pos, None):
1125 1125 el = expected[pos].pop(0)
1126 1126 if not el.endswith(b" (?)\n"):
1127 1127 expected[pos].insert(0, el)
1128 1128 break
1129 1129 postout.append(b' ' + el)
1130 1130
1131 1131 if lcmd:
1132 1132 # Add on last return code.
1133 1133 ret = int(lcmd.split()[1])
1134 1134 if ret != 0:
1135 1135 postout.append(b' [%d]\n' % ret)
1136 1136 if pos in after:
1137 1137 # Merge in non-active test bits.
1138 1138 postout += after.pop(pos)
1139 1139 pos = int(lcmd.split()[0])
1140 1140
1141 1141 if pos in after:
1142 1142 postout += after.pop(pos)
1143 1143
1144 1144 if warnonly == 2:
1145 1145 exitcode = False # Set exitcode to warned.
1146 1146
1147 1147 return exitcode, postout
1148 1148
1149 1149 @staticmethod
1150 1150 def rematch(el, l):
1151 1151 try:
1152 1152 # use \Z to ensure that the regex matches to the end of the string
1153 1153 if os.name == 'nt':
1154 1154 return re.match(el + br'\r?\n\Z', l)
1155 1155 return re.match(el + br'\n\Z', l)
1156 1156 except re.error:
1157 1157 # el is an invalid regex
1158 1158 return False
1159 1159
1160 1160 @staticmethod
1161 1161 def globmatch(el, l):
1162 1162 # The only supported special characters are * and ? plus / which also
1163 1163 # matches \ on windows. Escaping of these characters is supported.
1164 1164 if el + b'\n' == l:
1165 1165 if os.altsep:
1166 1166 # matching on "/" is not needed for this line
1167 1167 for pat in checkcodeglobpats:
1168 1168 if pat.match(el):
1169 1169 return True
1170 1170 return b'-glob'
1171 1171 return True
1172 1172 i, n = 0, len(el)
1173 1173 res = b''
1174 1174 while i < n:
1175 1175 c = el[i:i + 1]
1176 1176 i += 1
1177 1177 if c == b'\\' and i < n and el[i:i + 1] in b'*?\\/':
1178 1178 res += el[i - 1:i + 1]
1179 1179 i += 1
1180 1180 elif c == b'*':
1181 1181 res += b'.*'
1182 1182 elif c == b'?':
1183 1183 res += b'.'
1184 1184 elif c == b'/' and os.altsep:
1185 1185 res += b'[/\\\\]'
1186 1186 else:
1187 1187 res += re.escape(c)
1188 1188 return TTest.rematch(res, l)
1189 1189
1190 1190 @staticmethod
1191 1191 def linematch(el, l):
1192 1192 retry = False
1193 1193 if el == l: # perfect match (fast)
1194 1194 return True
1195 1195 if el:
1196 1196 if el.endswith(b" (?)\n"):
1197 1197 retry = "retry"
1198 1198 el = el[:-5] + "\n"
1199 1199 if el.endswith(b" (esc)\n"):
1200 1200 if PYTHON3:
1201 1201 el = el[:-7].decode('unicode_escape') + '\n'
1202 1202 el = el.encode('utf-8')
1203 1203 else:
1204 1204 el = el[:-7].decode('string-escape') + '\n'
1205 1205 if el == l or os.name == 'nt' and el[:-1] + b'\r\n' == l:
1206 1206 return True
1207 1207 if el.endswith(b" (re)\n"):
1208 1208 return TTest.rematch(el[:-6], l) or retry
1209 1209 if el.endswith(b" (glob)\n"):
1210 1210 # ignore '(glob)' added to l by 'replacements'
1211 1211 if l.endswith(b" (glob)\n"):
1212 1212 l = l[:-8] + b"\n"
1213 1213 return TTest.globmatch(el[:-8], l)
1214 1214 if os.altsep and l.replace(b'\\', b'/') == el:
1215 1215 return b'+glob'
1216 1216 return retry
1217 1217
1218 1218 @staticmethod
1219 1219 def parsehghaveoutput(lines):
1220 1220 '''Parse hghave log lines.
1221 1221
1222 1222 Return tuple of lists (missing, failed):
1223 1223 * the missing/unknown features
1224 1224 * the features for which existence check failed'''
1225 1225 missing = []
1226 1226 failed = []
1227 1227 for line in lines:
1228 1228 if line.startswith(TTest.SKIPPED_PREFIX):
1229 1229 line = line.splitlines()[0]
1230 1230 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1231 1231 elif line.startswith(TTest.FAILED_PREFIX):
1232 1232 line = line.splitlines()[0]
1233 1233 failed.append(line[len(TTest.FAILED_PREFIX):])
1234 1234
1235 1235 return missing, failed
1236 1236
1237 1237 @staticmethod
1238 1238 def _escapef(m):
1239 1239 return TTest.ESCAPEMAP[m.group(0)]
1240 1240
1241 1241 @staticmethod
1242 1242 def _stringescape(s):
1243 1243 return TTest.ESCAPESUB(TTest._escapef, s)
1244 1244
1245 1245 iolock = threading.RLock()
1246 1246
1247 1247 class SkipTest(Exception):
1248 1248 """Raised to indicate that a test is to be skipped."""
1249 1249
1250 1250 class IgnoreTest(Exception):
1251 1251 """Raised to indicate that a test is to be ignored."""
1252 1252
1253 1253 class WarnTest(Exception):
1254 1254 """Raised to indicate that a test warned."""
1255 1255
1256 1256 class ReportedTest(Exception):
1257 1257 """Raised to indicate that a test already reported."""
1258 1258
1259 1259 class TestResult(unittest._TextTestResult):
1260 1260 """Holds results when executing via unittest."""
1261 1261 # Don't worry too much about accessing the non-public _TextTestResult.
1262 1262 # It is relatively common in Python testing tools.
1263 1263 def __init__(self, options, *args, **kwargs):
1264 1264 super(TestResult, self).__init__(*args, **kwargs)
1265 1265
1266 1266 self._options = options
1267 1267
1268 1268 # unittest.TestResult didn't have skipped until 2.7. We need to
1269 1269 # polyfill it.
1270 1270 self.skipped = []
1271 1271
1272 1272 # We have a custom "ignored" result that isn't present in any Python
1273 1273 # unittest implementation. It is very similar to skipped. It may make
1274 1274 # sense to map it into skip some day.
1275 1275 self.ignored = []
1276 1276
1277 1277 # We have a custom "warned" result that isn't present in any Python
1278 1278 # unittest implementation. It is very similar to failed. It may make
1279 1279 # sense to map it into fail some day.
1280 1280 self.warned = []
1281 1281
1282 1282 self.times = []
1283 1283 self._firststarttime = None
1284 1284 # Data stored for the benefit of generating xunit reports.
1285 1285 self.successes = []
1286 1286 self.faildata = {}
1287 1287
1288 1288 def addFailure(self, test, reason):
1289 1289 self.failures.append((test, reason))
1290 1290
1291 1291 if self._options.first:
1292 1292 self.stop()
1293 1293 else:
1294 1294 with iolock:
1295 1295 if reason == "timed out":
1296 1296 self.stream.write('t')
1297 1297 else:
1298 1298 if not self._options.nodiff:
1299 1299 self.stream.write('\nERROR: %s output changed\n' % test)
1300 1300 self.stream.write('!')
1301 1301
1302 1302 self.stream.flush()
1303 1303
1304 1304 def addSuccess(self, test):
1305 1305 with iolock:
1306 1306 super(TestResult, self).addSuccess(test)
1307 1307 self.successes.append(test)
1308 1308
1309 1309 def addError(self, test, err):
1310 1310 super(TestResult, self).addError(test, err)
1311 1311 if self._options.first:
1312 1312 self.stop()
1313 1313
1314 1314 # Polyfill.
1315 1315 def addSkip(self, test, reason):
1316 1316 self.skipped.append((test, reason))
1317 1317 with iolock:
1318 1318 if self.showAll:
1319 1319 self.stream.writeln('skipped %s' % reason)
1320 1320 else:
1321 1321 self.stream.write('s')
1322 1322 self.stream.flush()
1323 1323
1324 1324 def addIgnore(self, test, reason):
1325 1325 self.ignored.append((test, reason))
1326 1326 with iolock:
1327 1327 if self.showAll:
1328 1328 self.stream.writeln('ignored %s' % reason)
1329 1329 else:
1330 1330 if reason not in ('not retesting', "doesn't match keyword"):
1331 1331 self.stream.write('i')
1332 1332 else:
1333 1333 self.testsRun += 1
1334 1334 self.stream.flush()
1335 1335
1336 1336 def addWarn(self, test, reason):
1337 1337 self.warned.append((test, reason))
1338 1338
1339 1339 if self._options.first:
1340 1340 self.stop()
1341 1341
1342 1342 with iolock:
1343 1343 if self.showAll:
1344 1344 self.stream.writeln('warned %s' % reason)
1345 1345 else:
1346 1346 self.stream.write('~')
1347 1347 self.stream.flush()
1348 1348
1349 1349 def addOutputMismatch(self, test, ret, got, expected):
1350 1350 """Record a mismatch in test output for a particular test."""
1351 1351 if self.shouldStop:
1352 1352 # don't print, some other test case already failed and
1353 1353 # printed, we're just stale and probably failed due to our
1354 1354 # temp dir getting cleaned up.
1355 1355 return
1356 1356
1357 1357 accepted = False
1358 1358 failed = False
1359 1359 lines = []
1360 1360
1361 1361 with iolock:
1362 1362 if self._options.nodiff:
1363 1363 pass
1364 1364 elif self._options.view:
1365 1365 v = self._options.view
1366 1366 if PYTHON3:
1367 1367 v = _bytespath(v)
1368 1368 os.system(b"%s %s %s" %
1369 1369 (v, test.refpath, test.errpath))
1370 1370 else:
1371 1371 servefail, lines = getdiff(expected, got,
1372 1372 test.refpath, test.errpath)
1373 1373 if servefail:
1374 1374 self.addFailure(
1375 1375 test,
1376 1376 'server failed to start (HGPORT=%s)' % test._startport)
1377 1377 raise ReportedTest('server failed to start')
1378 1378 else:
1379 1379 self.stream.write('\n')
1380 1380 for line in lines:
1381 1381 if PYTHON3:
1382 1382 self.stream.flush()
1383 1383 self.stream.buffer.write(line)
1384 1384 self.stream.buffer.flush()
1385 1385 else:
1386 1386 self.stream.write(line)
1387 1387 self.stream.flush()
1388 1388
1389 1389 # handle interactive prompt without releasing iolock
1390 1390 if self._options.interactive:
1391 1391 self.stream.write('Accept this change? [n] ')
1392 1392 answer = sys.stdin.readline().strip()
1393 1393 if answer.lower() in ('y', 'yes'):
1394 1394 if test.name.endswith('.t'):
1395 1395 rename(test.errpath, test.path)
1396 1396 else:
1397 1397 rename(test.errpath, '%s.out' % test.path)
1398 1398 accepted = True
1399 1399 if not accepted and not failed:
1400 1400 self.faildata[test.name] = b''.join(lines)
1401 1401
1402 1402 return accepted
1403 1403
1404 1404 def startTest(self, test):
1405 1405 super(TestResult, self).startTest(test)
1406 1406
1407 1407 # os.times module computes the user time and system time spent by
1408 1408 # child's processes along with real elapsed time taken by a process.
1409 1409 # This module has one limitation. It can only work for Linux user
1410 1410 # and not for Windows.
1411 1411 test.started = os.times()
1412 1412 if self._firststarttime is None: # thread racy but irrelevant
1413 1413 self._firststarttime = test.started[4]
1414 1414
1415 1415 def stopTest(self, test, interrupted=False):
1416 1416 super(TestResult, self).stopTest(test)
1417 1417
1418 1418 test.stopped = os.times()
1419 1419
1420 1420 starttime = test.started
1421 1421 endtime = test.stopped
1422 1422 origin = self._firststarttime
1423 1423 self.times.append((test.name,
1424 1424 endtime[2] - starttime[2], # user space CPU time
1425 1425 endtime[3] - starttime[3], # sys space CPU time
1426 1426 endtime[4] - starttime[4], # real time
1427 1427 starttime[4] - origin, # start date in run context
1428 1428 endtime[4] - origin, # end date in run context
1429 1429 ))
1430 1430
1431 1431 if interrupted:
1432 1432 with iolock:
1433 1433 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1434 1434 test.name, self.times[-1][3]))
1435 1435
1436 1436 class TestSuite(unittest.TestSuite):
1437 1437 """Custom unittest TestSuite that knows how to execute Mercurial tests."""
1438 1438
1439 1439 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1440 1440 retest=False, keywords=None, loop=False, runs_per_test=1,
1441 1441 loadtest=None, showchannels=False,
1442 1442 *args, **kwargs):
1443 1443 """Create a new instance that can run tests with a configuration.
1444 1444
1445 1445 testdir specifies the directory where tests are executed from. This
1446 1446 is typically the ``tests`` directory from Mercurial's source
1447 1447 repository.
1448 1448
1449 1449 jobs specifies the number of jobs to run concurrently. Each test
1450 1450 executes on its own thread. Tests actually spawn new processes, so
1451 1451 state mutation should not be an issue.
1452 1452
1453 1453 If there is only one job, it will use the main thread.
1454 1454
1455 1455 whitelist and blacklist denote tests that have been whitelisted and
1456 1456 blacklisted, respectively. These arguments don't belong in TestSuite.
1457 1457 Instead, whitelist and blacklist should be handled by the thing that
1458 1458 populates the TestSuite with tests. They are present to preserve
1459 1459 backwards compatible behavior which reports skipped tests as part
1460 1460 of the results.
1461 1461
1462 1462 retest denotes whether to retest failed tests. This arguably belongs
1463 1463 outside of TestSuite.
1464 1464
1465 1465 keywords denotes key words that will be used to filter which tests
1466 1466 to execute. This arguably belongs outside of TestSuite.
1467 1467
1468 1468 loop denotes whether to loop over tests forever.
1469 1469 """
1470 1470 super(TestSuite, self).__init__(*args, **kwargs)
1471 1471
1472 1472 self._jobs = jobs
1473 1473 self._whitelist = whitelist
1474 1474 self._blacklist = blacklist
1475 1475 self._retest = retest
1476 1476 self._keywords = keywords
1477 1477 self._loop = loop
1478 1478 self._runs_per_test = runs_per_test
1479 1479 self._loadtest = loadtest
1480 1480 self._showchannels = showchannels
1481 1481
1482 1482 def run(self, result):
1483 1483 # We have a number of filters that need to be applied. We do this
1484 1484 # here instead of inside Test because it makes the running logic for
1485 1485 # Test simpler.
1486 1486 tests = []
1487 1487 num_tests = [0]
1488 1488 for test in self._tests:
1489 1489 def get():
1490 1490 num_tests[0] += 1
1491 1491 if getattr(test, 'should_reload', False):
1492 1492 return self._loadtest(test.bname, num_tests[0])
1493 1493 return test
1494 1494 if not os.path.exists(test.path):
1495 1495 result.addSkip(test, "Doesn't exist")
1496 1496 continue
1497 1497
1498 1498 if not (self._whitelist and test.name in self._whitelist):
1499 1499 if self._blacklist and test.bname in self._blacklist:
1500 1500 result.addSkip(test, 'blacklisted')
1501 1501 continue
1502 1502
1503 1503 if self._retest and not os.path.exists(test.errpath):
1504 1504 result.addIgnore(test, 'not retesting')
1505 1505 continue
1506 1506
1507 1507 if self._keywords:
1508 1508 f = open(test.path, 'rb')
1509 1509 t = f.read().lower() + test.bname.lower()
1510 1510 f.close()
1511 1511 ignored = False
1512 1512 for k in self._keywords.lower().split():
1513 1513 if k not in t:
1514 1514 result.addIgnore(test, "doesn't match keyword")
1515 1515 ignored = True
1516 1516 break
1517 1517
1518 1518 if ignored:
1519 1519 continue
1520 1520 for _ in xrange(self._runs_per_test):
1521 1521 tests.append(get())
1522 1522
1523 1523 runtests = list(tests)
1524 1524 done = queue.Queue()
1525 1525 running = 0
1526 1526
1527 1527 channels = [""] * self._jobs
1528 1528
1529 1529 def job(test, result):
1530 1530 for n, v in enumerate(channels):
1531 1531 if not v:
1532 1532 channel = n
1533 1533 break
1534 1534 channels[channel] = "=" + test.name[5:].split(".")[0]
1535 1535 try:
1536 1536 test(result)
1537 1537 done.put(None)
1538 1538 except KeyboardInterrupt:
1539 1539 pass
1540 1540 except: # re-raises
1541 1541 done.put(('!', test, 'run-test raised an error, see traceback'))
1542 1542 raise
1543 1543 try:
1544 1544 channels[channel] = ''
1545 1545 except IndexError:
1546 1546 pass
1547 1547
1548 1548 def stat():
1549 1549 count = 0
1550 1550 while channels:
1551 1551 d = '\n%03s ' % count
1552 1552 for n, v in enumerate(channels):
1553 1553 if v:
1554 1554 d += v[0]
1555 1555 channels[n] = v[1:] or '.'
1556 1556 else:
1557 1557 d += ' '
1558 1558 d += ' '
1559 1559 with iolock:
1560 1560 sys.stdout.write(d + ' ')
1561 1561 sys.stdout.flush()
1562 1562 for x in xrange(10):
1563 1563 if channels:
1564 1564 time.sleep(.1)
1565 1565 count += 1
1566 1566
1567 1567 stoppedearly = False
1568 1568
1569 1569 if self._showchannels:
1570 1570 statthread = threading.Thread(target=stat, name="stat")
1571 1571 statthread.start()
1572 1572
1573 1573 try:
1574 1574 while tests or running:
1575 1575 if not done.empty() or running == self._jobs or not tests:
1576 1576 try:
1577 1577 done.get(True, 1)
1578 1578 running -= 1
1579 1579 if result and result.shouldStop:
1580 1580 stoppedearly = True
1581 1581 break
1582 1582 except queue.Empty:
1583 1583 continue
1584 1584 if tests and not running == self._jobs:
1585 1585 test = tests.pop(0)
1586 1586 if self._loop:
1587 1587 if getattr(test, 'should_reload', False):
1588 1588 num_tests[0] += 1
1589 1589 tests.append(
1590 1590 self._loadtest(test.name, num_tests[0]))
1591 1591 else:
1592 1592 tests.append(test)
1593 1593 if self._jobs == 1:
1594 1594 job(test, result)
1595 1595 else:
1596 1596 t = threading.Thread(target=job, name=test.name,
1597 1597 args=(test, result))
1598 1598 t.start()
1599 1599 running += 1
1600 1600
1601 1601 # If we stop early we still need to wait on started tests to
1602 1602 # finish. Otherwise, there is a race between the test completing
1603 1603 # and the test's cleanup code running. This could result in the
1604 1604 # test reporting incorrect.
1605 1605 if stoppedearly:
1606 1606 while running:
1607 1607 try:
1608 1608 done.get(True, 1)
1609 1609 running -= 1
1610 1610 except queue.Empty:
1611 1611 continue
1612 1612 except KeyboardInterrupt:
1613 1613 for test in runtests:
1614 1614 test.abort()
1615 1615
1616 1616 channels = []
1617 1617
1618 1618 return result
1619 1619
1620 1620 # Save the most recent 5 wall-clock runtimes of each test to a
1621 1621 # human-readable text file named .testtimes. Tests are sorted
1622 1622 # alphabetically, while times for each test are listed from oldest to
1623 1623 # newest.
1624 1624
1625 1625 def loadtimes(testdir):
1626 1626 times = []
1627 1627 try:
1628 1628 with open(os.path.join(testdir, '.testtimes-')) as fp:
1629 1629 for line in fp:
1630 1630 ts = line.split()
1631 1631 times.append((ts[0], [float(t) for t in ts[1:]]))
1632 1632 except IOError as err:
1633 1633 if err.errno != errno.ENOENT:
1634 1634 raise
1635 1635 return times
1636 1636
1637 1637 def savetimes(testdir, result):
1638 1638 saved = dict(loadtimes(testdir))
1639 1639 maxruns = 5
1640 1640 skipped = set([str(t[0]) for t in result.skipped])
1641 1641 for tdata in result.times:
1642 1642 test, real = tdata[0], tdata[3]
1643 1643 if test not in skipped:
1644 1644 ts = saved.setdefault(test, [])
1645 1645 ts.append(real)
1646 1646 ts[:] = ts[-maxruns:]
1647 1647
1648 1648 fd, tmpname = tempfile.mkstemp(prefix='.testtimes',
1649 1649 dir=testdir, text=True)
1650 1650 with os.fdopen(fd, 'w') as fp:
1651 1651 for name, ts in sorted(saved.iteritems()):
1652 1652 fp.write('%s %s\n' % (name, ' '.join(['%.3f' % (t,) for t in ts])))
1653 1653 timepath = os.path.join(testdir, '.testtimes')
1654 1654 try:
1655 1655 os.unlink(timepath)
1656 1656 except OSError:
1657 1657 pass
1658 1658 try:
1659 1659 os.rename(tmpname, timepath)
1660 1660 except OSError:
1661 1661 pass
1662 1662
1663 1663 class TextTestRunner(unittest.TextTestRunner):
1664 1664 """Custom unittest test runner that uses appropriate settings."""
1665 1665
1666 1666 def __init__(self, runner, *args, **kwargs):
1667 1667 super(TextTestRunner, self).__init__(*args, **kwargs)
1668 1668
1669 1669 self._runner = runner
1670 1670
1671 1671 def run(self, test):
1672 1672 result = TestResult(self._runner.options, self.stream,
1673 1673 self.descriptions, self.verbosity)
1674 1674
1675 1675 test(result)
1676 1676
1677 1677 failed = len(result.failures)
1678 1678 warned = len(result.warned)
1679 1679 skipped = len(result.skipped)
1680 1680 ignored = len(result.ignored)
1681 1681
1682 1682 with iolock:
1683 1683 self.stream.writeln('')
1684 1684
1685 1685 if not self._runner.options.noskips:
1686 1686 for test, msg in result.skipped:
1687 1687 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1688 1688 for test, msg in result.warned:
1689 1689 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1690 1690 for test, msg in result.failures:
1691 1691 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1692 1692 for test, msg in result.errors:
1693 1693 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1694 1694
1695 1695 if self._runner.options.xunit:
1696 1696 with open(self._runner.options.xunit, 'wb') as xuf:
1697 1697 timesd = dict((t[0], t[3]) for t in result.times)
1698 1698 doc = minidom.Document()
1699 1699 s = doc.createElement('testsuite')
1700 1700 s.setAttribute('name', 'run-tests')
1701 1701 s.setAttribute('tests', str(result.testsRun))
1702 1702 s.setAttribute('errors', "0") # TODO
1703 1703 s.setAttribute('failures', str(failed))
1704 1704 s.setAttribute('skipped', str(skipped + ignored))
1705 1705 doc.appendChild(s)
1706 1706 for tc in result.successes:
1707 1707 t = doc.createElement('testcase')
1708 1708 t.setAttribute('name', tc.name)
1709 1709 t.setAttribute('time', '%.3f' % timesd[tc.name])
1710 1710 s.appendChild(t)
1711 1711 for tc, err in sorted(result.faildata.items()):
1712 1712 t = doc.createElement('testcase')
1713 1713 t.setAttribute('name', tc)
1714 1714 t.setAttribute('time', '%.3f' % timesd[tc])
1715 1715 # createCDATASection expects a unicode or it will
1716 1716 # convert using default conversion rules, which will
1717 1717 # fail if string isn't ASCII.
1718 1718 err = cdatasafe(err).decode('utf-8', 'replace')
1719 1719 cd = doc.createCDATASection(err)
1720 1720 t.appendChild(cd)
1721 1721 s.appendChild(t)
1722 1722 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1723 1723
1724 1724 if self._runner.options.json:
1725 1725 if json is None:
1726 1726 raise ImportError("json module not installed")
1727 1727 jsonpath = os.path.join(self._runner._testdir, 'report.json')
1728 1728 with open(jsonpath, 'w') as fp:
1729 1729 timesd = {}
1730 1730 for tdata in result.times:
1731 1731 test = tdata[0]
1732 1732 timesd[test] = tdata[1:]
1733 1733
1734 1734 outcome = {}
1735 1735 groups = [('success', ((tc, None)
1736 1736 for tc in result.successes)),
1737 1737 ('failure', result.failures),
1738 1738 ('skip', result.skipped)]
1739 1739 for res, testcases in groups:
1740 1740 for tc, __ in testcases:
1741 1741 if tc.name in timesd:
1742 1742 tres = {'result': res,
1743 1743 'time': ('%0.3f' % timesd[tc.name][2]),
1744 1744 'cuser': ('%0.3f' % timesd[tc.name][0]),
1745 1745 'csys': ('%0.3f' % timesd[tc.name][1]),
1746 1746 'start': ('%0.3f' % timesd[tc.name][3]),
1747 1747 'end': ('%0.3f' % timesd[tc.name][4]),
1748 1748 'diff': result.faildata.get(tc.name,
1749 1749 ''),
1750 1750 }
1751 1751 else:
1752 1752 # blacklisted test
1753 1753 tres = {'result': res}
1754 1754
1755 1755 outcome[tc.name] = tres
1756 1756 jsonout = json.dumps(outcome, sort_keys=True, indent=4)
1757 1757 fp.writelines(("testreport =", jsonout))
1758 1758
1759 1759 self._runner._checkhglib('Tested')
1760 1760
1761 1761 savetimes(self._runner._testdir, result)
1762 1762 self.stream.writeln(
1763 1763 '# Ran %d tests, %d skipped, %d warned, %d failed.'
1764 1764 % (result.testsRun,
1765 1765 skipped + ignored, warned, failed))
1766 1766 if failed:
1767 1767 self.stream.writeln('python hash seed: %s' %
1768 1768 os.environ['PYTHONHASHSEED'])
1769 1769 if self._runner.options.time:
1770 1770 self.printtimes(result.times)
1771 1771
1772 1772 return result
1773 1773
1774 1774 def printtimes(self, times):
1775 1775 # iolock held by run
1776 1776 self.stream.writeln('# Producing time report')
1777 1777 times.sort(key=lambda t: (t[3]))
1778 1778 cols = '%7.3f %7.3f %7.3f %7.3f %7.3f %s'
1779 1779 self.stream.writeln('%-7s %-7s %-7s %-7s %-7s %s' %
1780 1780 ('start', 'end', 'cuser', 'csys', 'real', 'Test'))
1781 1781 for tdata in times:
1782 1782 test = tdata[0]
1783 1783 cuser, csys, real, start, end = tdata[1:6]
1784 1784 self.stream.writeln(cols % (start, end, cuser, csys, real, test))
1785 1785
1786 1786 class TestRunner(object):
1787 1787 """Holds context for executing tests.
1788 1788
1789 1789 Tests rely on a lot of state. This object holds it for them.
1790 1790 """
1791 1791
1792 1792 # Programs required to run tests.
1793 1793 REQUIREDTOOLS = [
1794 1794 os.path.basename(_bytespath(sys.executable)),
1795 1795 b'diff',
1796 1796 b'grep',
1797 1797 b'unzip',
1798 1798 b'gunzip',
1799 1799 b'bunzip2',
1800 1800 b'sed',
1801 1801 ]
1802 1802
1803 1803 # Maps file extensions to test class.
1804 1804 TESTTYPES = [
1805 1805 (b'.py', PythonTest),
1806 1806 (b'.t', TTest),
1807 1807 ]
1808 1808
1809 1809 def __init__(self):
1810 1810 self.options = None
1811 1811 self._hgroot = None
1812 1812 self._testdir = None
1813 1813 self._hgtmp = None
1814 1814 self._installdir = None
1815 1815 self._bindir = None
1816 1816 self._tmpbinddir = None
1817 1817 self._pythondir = None
1818 1818 self._coveragefile = None
1819 1819 self._createdfiles = []
1820 1820 self._hgpath = None
1821 1821 self._portoffset = 0
1822 1822 self._ports = {}
1823 1823
1824 1824 def run(self, args, parser=None):
1825 1825 """Run the test suite."""
1826 1826 oldmask = os.umask(0o22)
1827 1827 try:
1828 1828 parser = parser or getparser()
1829 1829 options, args = parseargs(args, parser)
1830 1830 # positional arguments are paths to test files to run, so
1831 1831 # we make sure they're all bytestrings
1832 1832 args = [_bytespath(a) for a in args]
1833 1833 self.options = options
1834 1834
1835 1835 self._checktools()
1836 1836 tests = self.findtests(args)
1837 1837 if options.profile_runner:
1838 1838 import statprof
1839 1839 statprof.start()
1840 1840 result = self._run(tests)
1841 1841 if options.profile_runner:
1842 1842 statprof.stop()
1843 1843 statprof.display()
1844 1844 return result
1845 1845
1846 1846 finally:
1847 1847 os.umask(oldmask)
1848 1848
1849 1849 def _run(self, tests):
1850 1850 if self.options.random:
1851 1851 random.shuffle(tests)
1852 1852 else:
1853 1853 # keywords for slow tests
1854 1854 slow = {b'svn': 10,
1855 1855 b'cvs': 10,
1856 1856 b'hghave': 10,
1857 1857 b'largefiles-update': 10,
1858 1858 b'run-tests': 10,
1859 1859 b'corruption': 10,
1860 1860 b'race': 10,
1861 1861 b'i18n': 10,
1862 1862 b'check': 100,
1863 1863 b'gendoc': 100,
1864 1864 b'contrib-perf': 200,
1865 1865 }
1866 1866 perf = {}
1867 1867 def sortkey(f):
1868 1868 # run largest tests first, as they tend to take the longest
1869 1869 try:
1870 1870 return perf[f]
1871 1871 except KeyError:
1872 1872 try:
1873 1873 val = -os.stat(f).st_size
1874 1874 except OSError as e:
1875 1875 if e.errno != errno.ENOENT:
1876 1876 raise
1877 1877 perf[f] = -1e9 # file does not exist, tell early
1878 1878 return -1e9
1879 1879 for kw, mul in slow.items():
1880 1880 if kw in f:
1881 1881 val *= mul
1882 1882 if f.endswith(b'.py'):
1883 1883 val /= 10.0
1884 1884 perf[f] = val / 1000.0
1885 1885 return perf[f]
1886 1886 tests.sort(key=sortkey)
1887 1887
1888 1888 self._testdir = osenvironb[b'TESTDIR'] = getattr(
1889 1889 os, 'getcwdb', os.getcwd)()
1890 1890
1891 1891 if 'PYTHONHASHSEED' not in os.environ:
1892 1892 # use a random python hash seed all the time
1893 1893 # we do the randomness ourself to know what seed is used
1894 1894 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1895 1895
1896 1896 if self.options.tmpdir:
1897 1897 self.options.keep_tmpdir = True
1898 1898 tmpdir = _bytespath(self.options.tmpdir)
1899 1899 if os.path.exists(tmpdir):
1900 1900 # Meaning of tmpdir has changed since 1.3: we used to create
1901 1901 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1902 1902 # tmpdir already exists.
1903 1903 print("error: temp dir %r already exists" % tmpdir)
1904 1904 return 1
1905 1905
1906 1906 # Automatically removing tmpdir sounds convenient, but could
1907 1907 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1908 1908 # or "--tmpdir=$HOME".
1909 1909 #vlog("# Removing temp dir", tmpdir)
1910 1910 #shutil.rmtree(tmpdir)
1911 1911 os.makedirs(tmpdir)
1912 1912 else:
1913 1913 d = None
1914 1914 if os.name == 'nt':
1915 1915 # without this, we get the default temp dir location, but
1916 1916 # in all lowercase, which causes troubles with paths (issue3490)
1917 1917 d = osenvironb.get(b'TMP', None)
1918 1918 tmpdir = tempfile.mkdtemp(b'', b'hgtests.', d)
1919 1919
1920 1920 self._hgtmp = osenvironb[b'HGTMP'] = (
1921 1921 os.path.realpath(tmpdir))
1922 1922
1923 1923 if self.options.with_hg:
1924 1924 self._installdir = None
1925 1925 whg = self.options.with_hg
1926 1926 self._bindir = os.path.dirname(os.path.realpath(whg))
1927 1927 assert isinstance(self._bindir, bytes)
1928 1928 self._tmpbindir = os.path.join(self._hgtmp, b'install', b'bin')
1929 1929 os.makedirs(self._tmpbindir)
1930 1930
1931 1931 # This looks redundant with how Python initializes sys.path from
1932 1932 # the location of the script being executed. Needed because the
1933 1933 # "hg" specified by --with-hg is not the only Python script
1934 1934 # executed in the test suite that needs to import 'mercurial'
1935 1935 # ... which means it's not really redundant at all.
1936 1936 self._pythondir = self._bindir
1937 1937 else:
1938 1938 self._installdir = os.path.join(self._hgtmp, b"install")
1939 self._bindir = osenvironb[b"BINDIR"] = \
1940 os.path.join(self._installdir, b"bin")
1939 self._bindir = os.path.join(self._installdir, b"bin")
1941 1940 self._tmpbindir = self._bindir
1942 1941 self._pythondir = os.path.join(self._installdir, b"lib", b"python")
1943 1942
1944 1943 osenvironb[b"BINDIR"] = self._bindir
1945 1944 osenvironb[b"PYTHON"] = PYTHON
1946 1945
1947 1946 fileb = _bytespath(__file__)
1948 1947 runtestdir = os.path.abspath(os.path.dirname(fileb))
1949 1948 osenvironb[b'RUNTESTDIR'] = runtestdir
1950 1949 if PYTHON3:
1951 1950 sepb = _bytespath(os.pathsep)
1952 1951 else:
1953 1952 sepb = os.pathsep
1954 1953 path = [self._bindir, runtestdir] + osenvironb[b"PATH"].split(sepb)
1955 1954 if os.path.islink(__file__):
1956 1955 # test helper will likely be at the end of the symlink
1957 1956 realfile = os.path.realpath(fileb)
1958 1957 realdir = os.path.abspath(os.path.dirname(realfile))
1959 1958 path.insert(2, realdir)
1960 1959 if self._testdir != runtestdir:
1961 1960 path = [self._testdir] + path
1962 1961 if self._tmpbindir != self._bindir:
1963 1962 path = [self._tmpbindir] + path
1964 1963 osenvironb[b"PATH"] = sepb.join(path)
1965 1964
1966 1965 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1967 1966 # can run .../tests/run-tests.py test-foo where test-foo
1968 1967 # adds an extension to HGRC. Also include run-test.py directory to
1969 1968 # import modules like heredoctest.
1970 1969 pypath = [self._pythondir, self._testdir, runtestdir]
1971 1970 # We have to augment PYTHONPATH, rather than simply replacing
1972 1971 # it, in case external libraries are only available via current
1973 1972 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1974 1973 # are in /opt/subversion.)
1975 1974 oldpypath = osenvironb.get(IMPL_PATH)
1976 1975 if oldpypath:
1977 1976 pypath.append(oldpypath)
1978 1977 osenvironb[IMPL_PATH] = sepb.join(pypath)
1979 1978
1980 1979 if self.options.pure:
1981 1980 os.environ["HGTEST_RUN_TESTS_PURE"] = "--pure"
1982 1981
1983 1982 if self.options.allow_slow_tests:
1984 1983 os.environ["HGTEST_SLOW"] = "slow"
1985 1984 elif 'HGTEST_SLOW' in os.environ:
1986 1985 del os.environ['HGTEST_SLOW']
1987 1986
1988 1987 self._coveragefile = os.path.join(self._testdir, b'.coverage')
1989 1988
1990 1989 vlog("# Using TESTDIR", self._testdir)
1991 1990 vlog("# Using RUNTESTDIR", osenvironb[b'RUNTESTDIR'])
1992 1991 vlog("# Using HGTMP", self._hgtmp)
1993 1992 vlog("# Using PATH", os.environ["PATH"])
1994 1993 vlog("# Using", IMPL_PATH, osenvironb[IMPL_PATH])
1995 1994
1996 1995 try:
1997 1996 return self._runtests(tests) or 0
1998 1997 finally:
1999 1998 time.sleep(.1)
2000 1999 self._cleanup()
2001 2000
2002 2001 def findtests(self, args):
2003 2002 """Finds possible test files from arguments.
2004 2003
2005 2004 If you wish to inject custom tests into the test harness, this would
2006 2005 be a good function to monkeypatch or override in a derived class.
2007 2006 """
2008 2007 if not args:
2009 2008 if self.options.changed:
2010 2009 proc = Popen4('hg st --rev "%s" -man0 .' %
2011 2010 self.options.changed, None, 0)
2012 2011 stdout, stderr = proc.communicate()
2013 2012 args = stdout.strip(b'\0').split(b'\0')
2014 2013 else:
2015 2014 args = os.listdir(b'.')
2016 2015
2017 2016 return [t for t in args
2018 2017 if os.path.basename(t).startswith(b'test-')
2019 2018 and (t.endswith(b'.py') or t.endswith(b'.t'))]
2020 2019
2021 2020 def _runtests(self, tests):
2022 2021 try:
2023 2022 if self._installdir:
2024 2023 self._installhg()
2025 2024 self._checkhglib("Testing")
2026 2025 else:
2027 2026 self._usecorrectpython()
2028 2027
2029 2028 if self.options.restart:
2030 2029 orig = list(tests)
2031 2030 while tests:
2032 2031 if os.path.exists(tests[0] + ".err"):
2033 2032 break
2034 2033 tests.pop(0)
2035 2034 if not tests:
2036 2035 print("running all tests")
2037 2036 tests = orig
2038 2037
2039 2038 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
2040 2039
2041 2040 failed = False
2042 2041 warned = False
2043 2042 kws = self.options.keywords
2044 2043 if kws is not None and PYTHON3:
2045 2044 kws = kws.encode('utf-8')
2046 2045
2047 2046 suite = TestSuite(self._testdir,
2048 2047 jobs=self.options.jobs,
2049 2048 whitelist=self.options.whitelisted,
2050 2049 blacklist=self.options.blacklist,
2051 2050 retest=self.options.retest,
2052 2051 keywords=kws,
2053 2052 loop=self.options.loop,
2054 2053 runs_per_test=self.options.runs_per_test,
2055 2054 showchannels=self.options.showchannels,
2056 2055 tests=tests, loadtest=self._gettest)
2057 2056 verbosity = 1
2058 2057 if self.options.verbose:
2059 2058 verbosity = 2
2060 2059 runner = TextTestRunner(self, verbosity=verbosity)
2061 2060 result = runner.run(suite)
2062 2061
2063 2062 if result.failures:
2064 2063 failed = True
2065 2064 if result.warned:
2066 2065 warned = True
2067 2066
2068 2067 if self.options.anycoverage:
2069 2068 self._outputcoverage()
2070 2069 except KeyboardInterrupt:
2071 2070 failed = True
2072 2071 print("\ninterrupted!")
2073 2072
2074 2073 if failed:
2075 2074 return 1
2076 2075 if warned:
2077 2076 return 80
2078 2077
2079 2078 def _getport(self, count):
2080 2079 port = self._ports.get(count) # do we have a cached entry?
2081 2080 if port is None:
2082 2081 portneeded = 3
2083 2082 # above 100 tries we just give up and let test reports failure
2084 2083 for tries in xrange(100):
2085 2084 allfree = True
2086 2085 port = self.options.port + self._portoffset
2087 2086 for idx in xrange(portneeded):
2088 2087 if not checkportisavailable(port + idx):
2089 2088 allfree = False
2090 2089 break
2091 2090 self._portoffset += portneeded
2092 2091 if allfree:
2093 2092 break
2094 2093 self._ports[count] = port
2095 2094 return port
2096 2095
2097 2096 def _gettest(self, test, count):
2098 2097 """Obtain a Test by looking at its filename.
2099 2098
2100 2099 Returns a Test instance. The Test may not be runnable if it doesn't
2101 2100 map to a known type.
2102 2101 """
2103 2102 lctest = test.lower()
2104 2103 testcls = Test
2105 2104
2106 2105 for ext, cls in self.TESTTYPES:
2107 2106 if lctest.endswith(ext):
2108 2107 testcls = cls
2109 2108 break
2110 2109
2111 2110 refpath = os.path.join(self._testdir, test)
2112 2111 tmpdir = os.path.join(self._hgtmp, b'child%d' % count)
2113 2112
2114 2113 t = testcls(refpath, tmpdir,
2115 2114 keeptmpdir=self.options.keep_tmpdir,
2116 2115 debug=self.options.debug,
2117 2116 timeout=self.options.timeout,
2118 2117 startport=self._getport(count),
2119 2118 extraconfigopts=self.options.extra_config_opt,
2120 2119 py3kwarnings=self.options.py3k_warnings,
2121 2120 shell=self.options.shell)
2122 2121 t.should_reload = True
2123 2122 return t
2124 2123
2125 2124 def _cleanup(self):
2126 2125 """Clean up state from this test invocation."""
2127 2126
2128 2127 if self.options.keep_tmpdir:
2129 2128 return
2130 2129
2131 2130 vlog("# Cleaning up HGTMP", self._hgtmp)
2132 2131 shutil.rmtree(self._hgtmp, True)
2133 2132 for f in self._createdfiles:
2134 2133 try:
2135 2134 os.remove(f)
2136 2135 except OSError:
2137 2136 pass
2138 2137
2139 2138 def _usecorrectpython(self):
2140 2139 """Configure the environment to use the appropriate Python in tests."""
2141 2140 # Tests must use the same interpreter as us or bad things will happen.
2142 2141 pyexename = sys.platform == 'win32' and b'python.exe' or b'python'
2143 2142 if getattr(os, 'symlink', None):
2144 2143 vlog("# Making python executable in test path a symlink to '%s'" %
2145 2144 sys.executable)
2146 2145 mypython = os.path.join(self._tmpbindir, pyexename)
2147 2146 try:
2148 2147 if os.readlink(mypython) == sys.executable:
2149 2148 return
2150 2149 os.unlink(mypython)
2151 2150 except OSError as err:
2152 2151 if err.errno != errno.ENOENT:
2153 2152 raise
2154 2153 if self._findprogram(pyexename) != sys.executable:
2155 2154 try:
2156 2155 os.symlink(sys.executable, mypython)
2157 2156 self._createdfiles.append(mypython)
2158 2157 except OSError as err:
2159 2158 # child processes may race, which is harmless
2160 2159 if err.errno != errno.EEXIST:
2161 2160 raise
2162 2161 else:
2163 2162 exedir, exename = os.path.split(sys.executable)
2164 2163 vlog("# Modifying search path to find %s as %s in '%s'" %
2165 2164 (exename, pyexename, exedir))
2166 2165 path = os.environ['PATH'].split(os.pathsep)
2167 2166 while exedir in path:
2168 2167 path.remove(exedir)
2169 2168 os.environ['PATH'] = os.pathsep.join([exedir] + path)
2170 2169 if not self._findprogram(pyexename):
2171 2170 print("WARNING: Cannot find %s in search path" % pyexename)
2172 2171
2173 2172 def _installhg(self):
2174 2173 """Install hg into the test environment.
2175 2174
2176 2175 This will also configure hg with the appropriate testing settings.
2177 2176 """
2178 2177 vlog("# Performing temporary installation of HG")
2179 2178 installerrs = os.path.join(b"tests", b"install.err")
2180 2179 compiler = ''
2181 2180 if self.options.compiler:
2182 2181 compiler = '--compiler ' + self.options.compiler
2183 2182 if self.options.pure:
2184 2183 pure = b"--pure"
2185 2184 else:
2186 2185 pure = b""
2187 2186 py3 = ''
2188 2187
2189 2188 # Run installer in hg root
2190 2189 script = os.path.realpath(sys.argv[0])
2191 2190 exe = sys.executable
2192 2191 if PYTHON3:
2193 2192 py3 = b'--c2to3'
2194 2193 compiler = _bytespath(compiler)
2195 2194 script = _bytespath(script)
2196 2195 exe = _bytespath(exe)
2197 2196 hgroot = os.path.dirname(os.path.dirname(script))
2198 2197 self._hgroot = hgroot
2199 2198 os.chdir(hgroot)
2200 2199 nohome = b'--home=""'
2201 2200 if os.name == 'nt':
2202 2201 # The --home="" trick works only on OS where os.sep == '/'
2203 2202 # because of a distutils convert_path() fast-path. Avoid it at
2204 2203 # least on Windows for now, deal with .pydistutils.cfg bugs
2205 2204 # when they happen.
2206 2205 nohome = b''
2207 2206 cmd = (b'%(exe)s setup.py %(py3)s %(pure)s clean --all'
2208 2207 b' build %(compiler)s --build-base="%(base)s"'
2209 2208 b' install --force --prefix="%(prefix)s"'
2210 2209 b' --install-lib="%(libdir)s"'
2211 2210 b' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
2212 2211 % {b'exe': exe, b'py3': py3, b'pure': pure,
2213 2212 b'compiler': compiler,
2214 2213 b'base': os.path.join(self._hgtmp, b"build"),
2215 2214 b'prefix': self._installdir, b'libdir': self._pythondir,
2216 2215 b'bindir': self._bindir,
2217 2216 b'nohome': nohome, b'logfile': installerrs})
2218 2217
2219 2218 # setuptools requires install directories to exist.
2220 2219 def makedirs(p):
2221 2220 try:
2222 2221 os.makedirs(p)
2223 2222 except OSError as e:
2224 2223 if e.errno != errno.EEXIST:
2225 2224 raise
2226 2225 makedirs(self._pythondir)
2227 2226 makedirs(self._bindir)
2228 2227
2229 2228 vlog("# Running", cmd)
2230 2229 if os.system(cmd) == 0:
2231 2230 if not self.options.verbose:
2232 2231 try:
2233 2232 os.remove(installerrs)
2234 2233 except OSError as e:
2235 2234 if e.errno != errno.ENOENT:
2236 2235 raise
2237 2236 else:
2238 2237 f = open(installerrs, 'rb')
2239 2238 for line in f:
2240 2239 if PYTHON3:
2241 2240 sys.stdout.buffer.write(line)
2242 2241 else:
2243 2242 sys.stdout.write(line)
2244 2243 f.close()
2245 2244 sys.exit(1)
2246 2245 os.chdir(self._testdir)
2247 2246
2248 2247 self._usecorrectpython()
2249 2248
2250 2249 if self.options.py3k_warnings and not self.options.anycoverage:
2251 2250 vlog("# Updating hg command to enable Py3k Warnings switch")
2252 2251 f = open(os.path.join(self._bindir, 'hg'), 'rb')
2253 2252 lines = [line.rstrip() for line in f]
2254 2253 lines[0] += ' -3'
2255 2254 f.close()
2256 2255 f = open(os.path.join(self._bindir, 'hg'), 'wb')
2257 2256 for line in lines:
2258 2257 f.write(line + '\n')
2259 2258 f.close()
2260 2259
2261 2260 hgbat = os.path.join(self._bindir, b'hg.bat')
2262 2261 if os.path.isfile(hgbat):
2263 2262 # hg.bat expects to be put in bin/scripts while run-tests.py
2264 2263 # installation layout put it in bin/ directly. Fix it
2265 2264 f = open(hgbat, 'rb')
2266 2265 data = f.read()
2267 2266 f.close()
2268 2267 if b'"%~dp0..\python" "%~dp0hg" %*' in data:
2269 2268 data = data.replace(b'"%~dp0..\python" "%~dp0hg" %*',
2270 2269 b'"%~dp0python" "%~dp0hg" %*')
2271 2270 f = open(hgbat, 'wb')
2272 2271 f.write(data)
2273 2272 f.close()
2274 2273 else:
2275 2274 print('WARNING: cannot fix hg.bat reference to python.exe')
2276 2275
2277 2276 if self.options.anycoverage:
2278 2277 custom = os.path.join(self._testdir, 'sitecustomize.py')
2279 2278 target = os.path.join(self._pythondir, 'sitecustomize.py')
2280 2279 vlog('# Installing coverage trigger to %s' % target)
2281 2280 shutil.copyfile(custom, target)
2282 2281 rc = os.path.join(self._testdir, '.coveragerc')
2283 2282 vlog('# Installing coverage rc to %s' % rc)
2284 2283 os.environ['COVERAGE_PROCESS_START'] = rc
2285 2284 covdir = os.path.join(self._installdir, '..', 'coverage')
2286 2285 try:
2287 2286 os.mkdir(covdir)
2288 2287 except OSError as e:
2289 2288 if e.errno != errno.EEXIST:
2290 2289 raise
2291 2290
2292 2291 os.environ['COVERAGE_DIR'] = covdir
2293 2292
2294 2293 def _checkhglib(self, verb):
2295 2294 """Ensure that the 'mercurial' package imported by python is
2296 2295 the one we expect it to be. If not, print a warning to stderr."""
2297 2296 if ((self._bindir == self._pythondir) and
2298 2297 (self._bindir != self._tmpbindir)):
2299 2298 # The pythondir has been inferred from --with-hg flag.
2300 2299 # We cannot expect anything sensible here.
2301 2300 return
2302 2301 expecthg = os.path.join(self._pythondir, b'mercurial')
2303 2302 actualhg = self._gethgpath()
2304 2303 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
2305 2304 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
2306 2305 ' (expected %s)\n'
2307 2306 % (verb, actualhg, expecthg))
2308 2307 def _gethgpath(self):
2309 2308 """Return the path to the mercurial package that is actually found by
2310 2309 the current Python interpreter."""
2311 2310 if self._hgpath is not None:
2312 2311 return self._hgpath
2313 2312
2314 2313 cmd = b'%s -c "import mercurial; print (mercurial.__path__[0])"'
2315 2314 cmd = cmd % PYTHON
2316 2315 if PYTHON3:
2317 2316 cmd = _strpath(cmd)
2318 2317 pipe = os.popen(cmd)
2319 2318 try:
2320 2319 self._hgpath = _bytespath(pipe.read().strip())
2321 2320 finally:
2322 2321 pipe.close()
2323 2322
2324 2323 return self._hgpath
2325 2324
2326 2325 def _outputcoverage(self):
2327 2326 """Produce code coverage output."""
2328 2327 from coverage import coverage
2329 2328
2330 2329 vlog('# Producing coverage report')
2331 2330 # chdir is the easiest way to get short, relative paths in the
2332 2331 # output.
2333 2332 os.chdir(self._hgroot)
2334 2333 covdir = os.path.join(self._installdir, '..', 'coverage')
2335 2334 cov = coverage(data_file=os.path.join(covdir, 'cov'))
2336 2335
2337 2336 # Map install directory paths back to source directory.
2338 2337 cov.config.paths['srcdir'] = ['.', self._pythondir]
2339 2338
2340 2339 cov.combine()
2341 2340
2342 2341 omit = [os.path.join(x, '*') for x in [self._bindir, self._testdir]]
2343 2342 cov.report(ignore_errors=True, omit=omit)
2344 2343
2345 2344 if self.options.htmlcov:
2346 2345 htmldir = os.path.join(self._testdir, 'htmlcov')
2347 2346 cov.html_report(directory=htmldir, omit=omit)
2348 2347 if self.options.annotate:
2349 2348 adir = os.path.join(self._testdir, 'annotated')
2350 2349 if not os.path.isdir(adir):
2351 2350 os.mkdir(adir)
2352 2351 cov.annotate(directory=adir, omit=omit)
2353 2352
2354 2353 def _findprogram(self, program):
2355 2354 """Search PATH for a executable program"""
2356 2355 dpb = _bytespath(os.defpath)
2357 2356 sepb = _bytespath(os.pathsep)
2358 2357 for p in osenvironb.get(b'PATH', dpb).split(sepb):
2359 2358 name = os.path.join(p, program)
2360 2359 if os.name == 'nt' or os.access(name, os.X_OK):
2361 2360 return name
2362 2361 return None
2363 2362
2364 2363 def _checktools(self):
2365 2364 """Ensure tools required to run tests are present."""
2366 2365 for p in self.REQUIREDTOOLS:
2367 2366 if os.name == 'nt' and not p.endswith('.exe'):
2368 2367 p += '.exe'
2369 2368 found = self._findprogram(p)
2370 2369 if found:
2371 2370 vlog("# Found prerequisite", p, "at", found)
2372 2371 else:
2373 2372 print("WARNING: Did not find prerequisite tool: %s " % p)
2374 2373
2375 2374 if __name__ == '__main__':
2376 2375 runner = TestRunner()
2377 2376
2378 2377 try:
2379 2378 import msvcrt
2380 2379 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
2381 2380 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
2382 2381 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
2383 2382 except ImportError:
2384 2383 pass
2385 2384
2386 2385 sys.exit(runner.run(sys.argv[1:]))
General Comments 0
You need to be logged in to leave comments. Login now