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