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