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