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