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