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