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