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