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