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