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