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