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