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