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