##// END OF EJS Templates
shelve: add a shelve extension to save/restore working changes...
David Soria Parra -
r19854:49d4919d default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (607 lines changed) Show them Hide them
@@ -0,0 +1,607
1 # shelve.py - save/restore working directory state
2 #
3 # Copyright 2013 Facebook, Inc.
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
7
8 """save and restore changes to the working directory
9
10 The "hg shelve" command saves changes made to the working directory
11 and reverts those changes, resetting the working directory to a clean
12 state.
13
14 Later on, the "hg unshelve" command restores the changes saved by "hg
15 shelve". Changes can be restored even after updating to a different
16 parent, in which case Mercurial's merge machinery will resolve any
17 conflicts if necessary.
18
19 You can have more than one shelved change outstanding at a time; each
20 shelved change has a distinct name. For details, see the help for "hg
21 shelve".
22 """
23
24 try:
25 import cPickle as pickle
26 pickle.dump # import now
27 except ImportError:
28 import pickle
29 from mercurial.i18n import _
30 from mercurial.node import nullid
31 from mercurial import changegroup, cmdutil, scmutil, phases
32 from mercurial import error, hg, mdiff, merge, patch, repair, util
33 from mercurial import templatefilters
34 from mercurial import lock as lockmod
35 import errno
36
37 cmdtable = {}
38 command = cmdutil.command(cmdtable)
39 testedwith = 'internal'
40
41 class shelvedfile(object):
42 """Handles common functions on shelve files (.hg/.files/.patch) using
43 the vfs layer"""
44 def __init__(self, repo, name, filetype=None):
45 self.repo = repo
46 self.name = name
47 self.vfs = scmutil.vfs(repo.join('shelved'))
48 if filetype:
49 self.fname = name + '.' + filetype
50 else:
51 self.fname = name
52
53 def exists(self):
54 return self.vfs.exists(self.fname)
55
56 def filename(self):
57 return self.vfs.join(self.fname)
58
59 def unlink(self):
60 util.unlink(self.filename())
61
62 def stat(self):
63 return self.vfs.stat(self.fname)
64
65 def opener(self, mode='rb'):
66 try:
67 return self.vfs(self.fname, mode)
68 except IOError, err:
69 if err.errno != errno.ENOENT:
70 raise
71 if mode[0] in 'wa':
72 try:
73 self.vfs.mkdir()
74 return self.vfs(self.fname, mode)
75 except IOError, err:
76 if err.errno != errno.EEXIST:
77 raise
78 elif mode[0] == 'r':
79 raise util.Abort(_("shelved change '%s' not found") %
80 self.name)
81
82 class shelvedstate(object):
83 """Handles saving and restoring a shelved state. Ensures that different
84 versions of a shelved state are possible and handles them appropriate"""
85 _version = 1
86 _filename = 'shelvedstate'
87
88 @classmethod
89 def load(cls, repo):
90 fp = repo.opener(cls._filename)
91 (version, name, parents, stripnodes) = pickle.load(fp)
92
93 if version != cls._version:
94 raise util.Abort(_('this version of shelve is incompatible '
95 'with the version used in this repo'))
96
97 obj = cls()
98 obj.name = name
99 obj.parents = parents
100 obj.stripnodes = stripnodes
101
102 return obj
103
104 @classmethod
105 def save(cls, repo, name, stripnodes):
106 fp = repo.opener(cls._filename, 'wb')
107 pickle.dump((cls._version, name,
108 repo.dirstate.parents(),
109 stripnodes), fp)
110 fp.close()
111
112 @staticmethod
113 def clear(repo):
114 util.unlinkpath(repo.join('shelvedstate'), ignoremissing=True)
115
116 def createcmd(ui, repo, pats, opts):
117 def publicancestors(ctx):
118 """Compute the heads of the public ancestors of a commit.
119
120 Much faster than the revset heads(ancestors(ctx) - draft())"""
121 seen = set()
122 visit = util.deque()
123 visit.append(ctx)
124 while visit:
125 ctx = visit.popleft()
126 for parent in ctx.parents():
127 rev = parent.rev()
128 if rev not in seen:
129 seen.add(rev)
130 if parent.mutable():
131 visit.append(parent)
132 else:
133 yield parent.node()
134
135 wctx = repo[None]
136 parents = wctx.parents()
137 if len(parents) > 1:
138 raise util.Abort(_('cannot shelve while merging'))
139 parent = parents[0]
140
141 # we never need the user, so we use a generic user for all shelve operations
142 user = 'shelve@localhost'
143 label = repo._bookmarkcurrent or parent.branch() or 'default'
144
145 # slashes aren't allowed in filenames, therefore we rename it
146 origlabel, label = label, label.replace('/', '_')
147
148 def gennames():
149 yield label
150 for i in xrange(1, 100):
151 yield '%s-%02d' % (label, i)
152
153 shelvedfiles = []
154
155 def commitfunc(ui, repo, message, match, opts):
156 # check modified, added, removed, deleted only
157 for flist in repo.status(match=match)[:4]:
158 shelvedfiles.extend(flist)
159 return repo.commit(message, user, opts.get('date'), match)
160
161 if parent.node() != nullid:
162 desc = parent.description().split('\n', 1)[0]
163 desc = _('shelved from %s (%s): %s') % (label, str(parent)[:8], desc)
164 else:
165 desc = '(empty repository)'
166
167 if not opts['message']:
168 opts['message'] = desc
169
170 name = opts['name']
171
172 wlock = lock = tr = None
173 try:
174 wlock = repo.wlock()
175 lock = repo.lock()
176
177 # use an uncommited transaction to generate the bundle to avoid
178 # pull races. ensure we don't print the abort message to stderr.
179 tr = repo.transaction('commit', report=lambda x: None)
180
181 if name:
182 if shelvedfile(repo, name, 'hg').exists():
183 raise util.Abort(_("a shelved change named '%s' already exists")
184 % name)
185 else:
186 for n in gennames():
187 if not shelvedfile(repo, n, 'hg').exists():
188 name = n
189 break
190 else:
191 raise util.Abort(_("too many shelved changes named '%s'") %
192 label)
193
194 # ensure we are not creating a subdirectory or a hidden file
195 if '/' in name or '\\' in name:
196 raise util.Abort(_('shelved change names may not contain slashes'))
197 if name.startswith('.'):
198 raise util.Abort(_("shelved change names may not start with '.'"))
199
200 node = cmdutil.commit(ui, repo, commitfunc, pats, opts)
201
202 if not node:
203 stat = repo.status(match=scmutil.match(repo[None], pats, opts))
204 if stat[3]:
205 ui.status(_("nothing changed (%d missing files, see "
206 "'hg status')\n") % len(stat[3]))
207 else:
208 ui.status(_("nothing changed\n"))
209 return 1
210
211 phases.retractboundary(repo, phases.secret, [node])
212
213 fp = shelvedfile(repo, name, 'files').opener('wb')
214 fp.write('\0'.join(shelvedfiles))
215
216 bases = list(publicancestors(repo[node]))
217 cg = repo.changegroupsubset(bases, [node], 'shelve')
218 changegroup.writebundle(cg, shelvedfile(repo, name, 'hg').filename(),
219 'HG10UN')
220 cmdutil.export(repo, [node],
221 fp=shelvedfile(repo, name, 'patch').opener('wb'),
222 opts=mdiff.diffopts(git=True))
223
224 if ui.formatted():
225 desc = util.ellipsis(desc, ui.termwidth())
226 ui.status(desc + '\n')
227 ui.status(_('shelved as %s\n') % name)
228 hg.update(repo, parent.node())
229 finally:
230 if tr:
231 tr.abort()
232 lockmod.release(lock, wlock)
233
234 def cleanupcmd(ui, repo):
235 wlock = None
236 try:
237 wlock = repo.wlock()
238 for (name, _) in repo.vfs.readdir('shelved'):
239 suffix = name.rsplit('.', 1)[-1]
240 if suffix in ('hg', 'files', 'patch'):
241 shelvedfile(repo, name).unlink()
242 finally:
243 lockmod.release(wlock)
244
245 def deletecmd(ui, repo, pats):
246 if not pats:
247 raise util.Abort(_('no shelved changes specified!'))
248 wlock = None
249 try:
250 wlock = repo.wlock()
251 try:
252 for name in pats:
253 for suffix in 'hg files patch'.split():
254 shelvedfile(repo, name, suffix).unlink()
255 except OSError, err:
256 if err.errno != errno.ENOENT:
257 raise
258 raise util.Abort(_("shelved change '%s' not found") % name)
259 finally:
260 lockmod.release(wlock)
261
262 def listshelves(repo):
263 try:
264 names = repo.vfs.readdir('shelved')
265 except OSError, err:
266 if err.errno != errno.ENOENT:
267 raise
268 return []
269 info = []
270 for (name, _) in names:
271 pfx, sfx = name.rsplit('.', 1)
272 if not pfx or sfx != 'patch':
273 continue
274 st = shelvedfile(repo, name).stat()
275 info.append((st.st_mtime, shelvedfile(repo, pfx).filename()))
276 return sorted(info, reverse=True)
277
278 def listcmd(ui, repo, pats, opts):
279 pats = set(pats)
280 width = 80
281 if not ui.plain():
282 width = ui.termwidth()
283 namelabel = 'shelve.newest'
284 for mtime, name in listshelves(repo):
285 sname = util.split(name)[1]
286 if pats and sname not in pats:
287 continue
288 ui.write(sname, label=namelabel)
289 namelabel = 'shelve.name'
290 if ui.quiet:
291 ui.write('\n')
292 continue
293 ui.write(' ' * (16 - len(sname)))
294 used = 16
295 age = '[%s]' % templatefilters.age(util.makedate(mtime))
296 ui.write(age, label='shelve.age')
297 ui.write(' ' * (18 - len(age)))
298 used += 18
299 fp = open(name + '.patch', 'rb')
300 try:
301 while True:
302 line = fp.readline()
303 if not line:
304 break
305 if not line.startswith('#'):
306 desc = line.rstrip()
307 if ui.formatted():
308 desc = util.ellipsis(desc, width - used)
309 ui.write(desc)
310 break
311 ui.write('\n')
312 if not (opts['patch'] or opts['stat']):
313 continue
314 difflines = fp.readlines()
315 if opts['patch']:
316 for chunk, label in patch.difflabel(iter, difflines):
317 ui.write(chunk, label=label)
318 if opts['stat']:
319 for chunk, label in patch.diffstatui(difflines, width=width,
320 git=True):
321 ui.write(chunk, label=label)
322 finally:
323 fp.close()
324
325 def readshelvedfiles(repo, basename):
326 fp = shelvedfile(repo, basename, 'files').opener()
327 return fp.read().split('\0')
328
329 def checkparents(repo, state):
330 if state.parents != repo.dirstate.parents():
331 raise util.Abort(_('working directory parents do not match unshelve '
332 'state'))
333
334 def unshelveabort(ui, repo, state, opts):
335 wlock = repo.wlock()
336 lock = None
337 try:
338 checkparents(repo, state)
339 lock = repo.lock()
340 merge.mergestate(repo).reset()
341 if opts['keep']:
342 repo.setparents(repo.dirstate.parents()[0])
343 else:
344 revertfiles = readshelvedfiles(repo, state.name)
345 wctx = repo.parents()[0]
346 cmdutil.revert(ui, repo, wctx, [wctx.node(), nullid],
347 *revertfiles, no_backup=True)
348 # fix up the weird dirstate states the merge left behind
349 mf = wctx.manifest()
350 dirstate = repo.dirstate
351 for f in revertfiles:
352 if f in mf:
353 dirstate.normallookup(f)
354 else:
355 dirstate.drop(f)
356 dirstate._pl = (wctx.node(), nullid)
357 dirstate._dirty = True
358 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
359 shelvedstate.clear(repo)
360 ui.warn(_("unshelve of '%s' aborted\n") % state.name)
361 finally:
362 lockmod.release(lock, wlock)
363
364 def unshelvecleanup(ui, repo, name, opts):
365 if not opts['keep']:
366 for filetype in 'hg files patch'.split():
367 shelvedfile(repo, name, filetype).unlink()
368
369 def finishmerge(ui, repo, ms, stripnodes, name, opts):
370 # Reset the working dir so it's no longer in a merge state.
371 dirstate = repo.dirstate
372 for f in ms:
373 if dirstate[f] == 'm':
374 dirstate.normallookup(f)
375 dirstate._pl = (dirstate._pl[0], nullid)
376 dirstate._dirty = dirstate._dirtypl = True
377 shelvedstate.clear(repo)
378
379 def unshelvecontinue(ui, repo, state, opts):
380 # We're finishing off a merge. First parent is our original
381 # parent, second is the temporary "fake" commit we're unshelving.
382 wlock = repo.wlock()
383 lock = None
384 try:
385 checkparents(repo, state)
386 ms = merge.mergestate(repo)
387 if [f for f in ms if ms[f] == 'u']:
388 raise util.Abort(
389 _("unresolved conflicts, can't continue"),
390 hint=_("see 'hg resolve', then 'hg unshelve --continue'"))
391 finishmerge(ui, repo, ms, state.stripnodes, state.name, opts)
392 lock = repo.lock()
393 repair.strip(ui, repo, state.stripnodes, backup='none', topic='shelve')
394 unshelvecleanup(ui, repo, state.name, opts)
395 ui.status(_("unshelve of '%s' complete\n") % state.name)
396 finally:
397 lockmod.release(lock, wlock)
398
399 @command('unshelve',
400 [('a', 'abort', None,
401 _('abort an incomplete unshelve operation')),
402 ('c', 'continue', None,
403 _('continue an incomplete unshelve operation')),
404 ('', 'keep', None,
405 _('keep shelve after unshelving'))],
406 _('hg unshelve [SHELVED]'))
407 def unshelve(ui, repo, *shelved, **opts):
408 """restore a shelved change to the working directory
409
410 This command accepts an optional name of a shelved change to
411 restore. If none is given, the most recent shelved change is used.
412
413 If a shelved change is applied successfully, the bundle that
414 contains the shelved changes is deleted afterwards.
415
416 Since you can restore a shelved change on top of an arbitrary
417 commit, it is possible that unshelving will result in a conflict
418 between your changes and the commits you are unshelving onto. If
419 this occurs, you must resolve the conflict, then use
420 ``--continue`` to complete the unshelve operation. (The bundle
421 will not be deleted until you successfully complete the unshelve.)
422
423 (Alternatively, you can use ``--abort`` to abandon an unshelve
424 that causes a conflict. This reverts the unshelved changes, and
425 does not delete the bundle.)
426 """
427 abortf = opts['abort']
428 continuef = opts['continue']
429 if not abortf and not continuef:
430 cmdutil.checkunfinished(repo)
431
432 if abortf or continuef:
433 if abortf and continuef:
434 raise util.Abort(_('cannot use both abort and continue'))
435 if shelved:
436 raise util.Abort(_('cannot combine abort/continue with '
437 'naming a shelved change'))
438
439 try:
440 state = shelvedstate.load(repo)
441 except IOError, err:
442 if err.errno != errno.ENOENT:
443 raise
444 raise util.Abort(_('no unshelve operation underway'))
445
446 if abortf:
447 return unshelveabort(ui, repo, state, opts)
448 elif continuef:
449 return unshelvecontinue(ui, repo, state, opts)
450 elif len(shelved) > 1:
451 raise util.Abort(_('can only unshelve one change at a time'))
452 elif not shelved:
453 shelved = listshelves(repo)
454 if not shelved:
455 raise util.Abort(_('no shelved changes to apply!'))
456 basename = util.split(shelved[0][1])[1]
457 ui.status(_("unshelving change '%s'\n") % basename)
458 else:
459 basename = shelved[0]
460
461 shelvedfiles = readshelvedfiles(repo, basename)
462
463 m, a, r, d = repo.status()[:4]
464 unsafe = set(m + a + r + d).intersection(shelvedfiles)
465 if unsafe:
466 ui.warn(_('the following shelved files have been modified:\n'))
467 for f in sorted(unsafe):
468 ui.warn(' %s\n' % f)
469 ui.warn(_('you must commit, revert, or shelve your changes before you '
470 'can proceed\n'))
471 raise util.Abort(_('cannot unshelve due to local changes\n'))
472
473 wlock = lock = tr = None
474 try:
475 lock = repo.lock()
476
477 tr = repo.transaction('unshelve', report=lambda x: None)
478 oldtiprev = len(repo)
479 try:
480 fp = shelvedfile(repo, basename, 'hg').opener()
481 gen = changegroup.readbundle(fp, fp.name)
482 repo.addchangegroup(gen, 'unshelve', 'bundle:' + fp.name)
483 nodes = [ctx.node() for ctx in repo.set('%d:', oldtiprev)]
484 phases.retractboundary(repo, phases.secret, nodes)
485 tr.close()
486 finally:
487 fp.close()
488
489 tip = repo['tip']
490 wctx = repo['.']
491 ancestor = tip.ancestor(wctx)
492
493 wlock = repo.wlock()
494
495 if ancestor.node() != wctx.node():
496 conflicts = hg.merge(repo, tip.node(), force=True, remind=False)
497 ms = merge.mergestate(repo)
498 stripnodes = [repo.changelog.node(rev)
499 for rev in xrange(oldtiprev, len(repo))]
500 if conflicts:
501 shelvedstate.save(repo, basename, stripnodes)
502 # Fix up the dirstate entries of files from the second
503 # parent as if we were not merging, except for those
504 # with unresolved conflicts.
505 parents = repo.parents()
506 revertfiles = set(parents[1].files()).difference(ms)
507 cmdutil.revert(ui, repo, parents[1],
508 (parents[0].node(), nullid),
509 *revertfiles, no_backup=True)
510 raise error.InterventionRequired(
511 _("unresolved conflicts (see 'hg resolve', then "
512 "'hg unshelve --continue')"))
513 finishmerge(ui, repo, ms, stripnodes, basename, opts)
514 else:
515 parent = tip.parents()[0]
516 hg.update(repo, parent.node())
517 cmdutil.revert(ui, repo, tip, repo.dirstate.parents(), *tip.files(),
518 no_backup=True)
519
520 prevquiet = ui.quiet
521 ui.quiet = True
522 try:
523 repo.rollback(force=True)
524 finally:
525 ui.quiet = prevquiet
526
527 unshelvecleanup(ui, repo, basename, opts)
528 finally:
529 if tr:
530 tr.release()
531 lockmod.release(lock, wlock)
532
533 @command('shelve',
534 [('A', 'addremove', None,
535 _('mark new/missing files as added/removed before shelving')),
536 ('', 'cleanup', None,
537 _('delete all shelved changes')),
538 ('', 'date', '',
539 _('shelve with the specified commit date'), _('DATE')),
540 ('d', 'delete', None,
541 _('delete the named shelved change(s)')),
542 ('l', 'list', None,
543 _('list current shelves')),
544 ('m', 'message', '',
545 _('use text as shelve message'), _('TEXT')),
546 ('n', 'name', '',
547 _('use the given name for the shelved commit'), _('NAME')),
548 ('p', 'patch', None,
549 _('show patch')),
550 ('', 'stat', None,
551 _('output diffstat-style summary of changes'))],
552 _('hg shelve'))
553 def shelvecmd(ui, repo, *pats, **opts):
554 '''save and set aside changes from the working directory
555
556 Shelving takes files that "hg status" reports as not clean, saves
557 the modifications to a bundle (a shelved change), and reverts the
558 files so that their state in the working directory becomes clean.
559
560 To restore these changes to the working directory, using "hg
561 unshelve"; this will work even if you switch to a different
562 commit.
563
564 When no files are specified, "hg shelve" saves all not-clean
565 files. If specific files or directories are named, only changes to
566 those files are shelved.
567
568 Each shelved change has a name that makes it easier to find later.
569 The name of a shelved change defaults to being based on the active
570 bookmark, or if there is no active bookmark, the current named
571 branch. To specify a different name, use ``--name``.
572
573 To see a list of existing shelved changes, use the ``--list``
574 option. For each shelved change, this will print its name, age,
575 and description; use ``--patch`` or ``--stat`` for more details.
576
577 To delete specific shelved changes, use ``--delete``. To delete
578 all shelved changes, use ``--cleanup``.
579 '''
580 cmdutil.checkunfinished(repo)
581
582 def checkopt(opt, incompatible):
583 if opts[opt]:
584 for i in incompatible.split():
585 if opts[i]:
586 raise util.Abort(_("options '--%s' and '--%s' may not be "
587 "used together") % (opt, i))
588 return True
589 if checkopt('cleanup', 'addremove delete list message name patch stat'):
590 if pats:
591 raise util.Abort(_("cannot specify names when using '--cleanup'"))
592 return cleanupcmd(ui, repo)
593 elif checkopt('delete', 'addremove cleanup list message name patch stat'):
594 return deletecmd(ui, repo, pats)
595 elif checkopt('list', 'addremove cleanup delete message name'):
596 return listcmd(ui, repo, pats, opts)
597 else:
598 for i in ('patch', 'stat'):
599 if opts[i]:
600 raise util.Abort(_("option '--%s' may not be "
601 "used when shelving a change") % (i,))
602 return createcmd(ui, repo, pats, opts)
603
604 def extsetup(ui):
605 cmdutil.unfinishedstates.append(
606 [shelvedstate._filename, False, True, _('unshelve already in progress'),
607 _("use 'hg unshelve --continue' or 'hg unshelve --abort'")])
@@ -0,0 +1,420
1 $ echo "[extensions]" >> $HGRCPATH
2 $ echo "shelve=" >> $HGRCPATH
3 $ echo "[defaults]" >> $HGRCPATH
4 $ echo "diff = --nodates --git" >> $HGRCPATH
5
6 $ hg init repo
7 $ cd repo
8 $ mkdir a b
9 $ echo a > a/a
10 $ echo b > b/b
11 $ echo c > c
12 $ echo d > d
13 $ echo x > x
14 $ hg addremove -q
15
16 shelving in an empty repo should be possible
17
18 $ hg shelve
19 (empty repository)
20 shelved as default
21 0 files updated, 0 files merged, 5 files removed, 0 files unresolved
22
23 $ hg unshelve
24 unshelving change 'default'
25 adding changesets
26 adding manifests
27 adding file changes
28 added 1 changesets with 5 changes to 5 files
29 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
30
31 $ hg commit -q -m 'initial commit'
32
33 $ hg shelve
34 nothing changed
35 [1]
36
37 create another commit
38
39 $ echo n > n
40 $ hg add n
41 $ hg commit n -m second
42
43 shelve a change that we will delete later
44
45 $ echo a >> a/a
46 $ hg shelve
47 shelved from default (bb4fec6d): second
48 shelved as default
49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
50
51 set up some more complex changes to shelve
52
53 $ echo a >> a/a
54 $ hg mv b b.rename
55 moving b/b to b.rename/b (glob)
56 $ hg cp c c.copy
57 $ hg status -C
58 M a/a
59 A b.rename/b
60 b/b
61 A c.copy
62 c
63 R b/b
64
65 prevent some foot-shooting
66
67 $ hg shelve -n foo/bar
68 abort: shelved change names may not contain slashes
69 [255]
70 $ hg shelve -n .baz
71 abort: shelved change names may not start with '.'
72 [255]
73
74 the common case - no options or filenames
75
76 $ hg shelve
77 shelved from default (bb4fec6d): second
78 shelved as default-01
79 2 files updated, 0 files merged, 2 files removed, 0 files unresolved
80 $ hg status -C
81
82 ensure that our shelved changes exist
83
84 $ hg shelve -l
85 default-01 [*] shelved from default (bb4fec6d): second (glob)
86 default [*] shelved from default (bb4fec6d): second (glob)
87
88 $ hg shelve -l -p default
89 default [*] shelved from default (bb4fec6d): second (glob)
90
91 diff --git a/a/a b/a/a
92 --- a/a/a
93 +++ b/a/a
94 @@ -1,1 +1,2 @@
95 a
96 +a
97
98 delete our older shelved change
99
100 $ hg shelve -d default
101
102 local edits should prevent a shelved change from applying
103
104 $ echo e>>a/a
105 $ hg unshelve
106 unshelving change 'default-01'
107 the following shelved files have been modified:
108 a/a
109 you must commit, revert, or shelve your changes before you can proceed
110 abort: cannot unshelve due to local changes
111
112 [255]
113
114 $ hg revert -C a/a
115
116 apply it and make sure our state is as expected
117
118 $ hg unshelve
119 unshelving change 'default-01'
120 adding changesets
121 adding manifests
122 adding file changes
123 added 1 changesets with 3 changes to 8 files
124 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
125 $ hg status -C
126 M a/a
127 A b.rename/b
128 b/b
129 A c.copy
130 c
131 R b/b
132 $ hg shelve -l
133
134 $ hg unshelve
135 abort: no shelved changes to apply!
136 [255]
137 $ hg unshelve foo
138 abort: shelved change 'foo' not found
139 [255]
140
141 named shelves, specific filenames, and "commit messages" should all work
142
143 $ hg status -C
144 M a/a
145 A b.rename/b
146 b/b
147 A c.copy
148 c
149 R b/b
150 $ hg shelve -q -n wibble -m wat a
151
152 expect "a" to no longer be present, but status otherwise unchanged
153
154 $ hg status -C
155 A b.rename/b
156 b/b
157 A c.copy
158 c
159 R b/b
160 $ hg shelve -l --stat
161 wibble [*] wat (glob)
162 a/a | 1 +
163 1 files changed, 1 insertions(+), 0 deletions(-)
164
165 and now "a/a" should reappear
166
167 $ hg unshelve -q wibble
168 $ hg status -C
169 M a/a
170 A b.rename/b
171 b/b
172 A c.copy
173 c
174 R b/b
175
176 cause unshelving to result in a merge with 'a' conflicting
177
178 $ hg shelve -q
179 $ echo c>>a/a
180 $ hg commit -m second
181 $ hg tip --template '{files}\n'
182 a/a
183
184 add an unrelated change that should be preserved
185
186 $ mkdir foo
187 $ echo foo > foo/foo
188 $ hg add foo/foo
189
190 force a conflicted merge to occur
191
192 $ hg unshelve
193 unshelving change 'default'
194 adding changesets
195 adding manifests
196 adding file changes
197 added 1 changesets with 3 changes to 8 files (+1 heads)
198 merging a/a
199 warning: conflicts during merge.
200 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
201 2 files updated, 0 files merged, 1 files removed, 1 files unresolved
202 use 'hg resolve' to retry unresolved file merges or 'hg update -C .' to abandon
203 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
204 [1]
205
206 ensure that we have a merge with unresolved conflicts
207
208 $ hg heads -q
209 3:7ec047b69dc0
210 2:ceefc37abe1e
211 $ hg parents -q
212 2:ceefc37abe1e
213 3:7ec047b69dc0
214 $ hg status
215 M a/a
216 M b.rename/b
217 M c.copy
218 A foo/foo
219 R b/b
220 ? a/a.orig
221 $ hg diff
222 diff --git a/a/a b/a/a
223 --- a/a/a
224 +++ b/a/a
225 @@ -1,2 +1,6 @@
226 a
227 +<<<<<<< local
228 c
229 +=======
230 +a
231 +>>>>>>> other
232 diff --git a/b.rename/b b/b.rename/b
233 --- /dev/null
234 +++ b/b.rename/b
235 @@ -0,0 +1,1 @@
236 +b
237 diff --git a/b/b b/b/b
238 deleted file mode 100644
239 --- a/b/b
240 +++ /dev/null
241 @@ -1,1 +0,0 @@
242 -b
243 diff --git a/c.copy b/c.copy
244 --- /dev/null
245 +++ b/c.copy
246 @@ -0,0 +1,1 @@
247 +c
248 diff --git a/foo/foo b/foo/foo
249 new file mode 100644
250 --- /dev/null
251 +++ b/foo/foo
252 @@ -0,0 +1,1 @@
253 +foo
254 $ hg resolve -l
255 U a/a
256
257 $ hg shelve
258 abort: unshelve already in progress
259 (use 'hg unshelve --continue' or 'hg unshelve --abort')
260 [255]
261
262 abort the unshelve and be happy
263
264 $ hg status
265 M a/a
266 M b.rename/b
267 M c.copy
268 A foo/foo
269 R b/b
270 ? a/a.orig
271 $ hg unshelve -a
272 unshelve of 'default' aborted
273 $ hg heads -q
274 2:ceefc37abe1e
275 $ hg parents
276 changeset: 2:ceefc37abe1e
277 tag: tip
278 user: test
279 date: Thu Jan 01 00:00:00 1970 +0000
280 summary: second
281
282 $ hg resolve -l
283 $ hg status
284 A foo/foo
285 ? a/a.orig
286
287 try to continue with no unshelve underway
288
289 $ hg unshelve -c
290 abort: no unshelve operation underway
291 [255]
292 $ hg status
293 A foo/foo
294 ? a/a.orig
295
296 redo the unshelve to get a conflict
297
298 $ hg unshelve -q
299 warning: conflicts during merge.
300 merging a/a incomplete! (edit conflicts, then use 'hg resolve --mark')
301 unresolved conflicts (see 'hg resolve', then 'hg unshelve --continue')
302 [1]
303
304 attempt to continue
305
306 $ hg unshelve -c
307 abort: unresolved conflicts, can't continue
308 (see 'hg resolve', then 'hg unshelve --continue')
309 [255]
310
311 $ hg revert -r . a/a
312 $ hg resolve -m a/a
313
314 $ hg unshelve -c
315 unshelve of 'default' complete
316
317 ensure the repo is as we hope
318
319 $ hg parents
320 changeset: 2:ceefc37abe1e
321 tag: tip
322 user: test
323 date: Thu Jan 01 00:00:00 1970 +0000
324 summary: second
325
326 $ hg heads -q
327 2:ceefc37abe1e
328
329 $ hg status -C
330 M a/a
331 M b.rename/b
332 b/b
333 M c.copy
334 c
335 A foo/foo
336 R b/b
337 ? a/a.orig
338
339 there should be no shelves left
340
341 $ hg shelve -l
342
343 $ hg commit -m whee a/a
344
345 #if execbit
346
347 ensure that metadata-only changes are shelved
348
349 $ chmod +x a/a
350 $ hg shelve -q -n execbit a/a
351 $ hg status a/a
352 $ hg unshelve -q execbit
353 $ hg status a/a
354 M a/a
355 $ hg revert a/a
356
357 #endif
358
359 #if symlink
360
361 $ rm a/a
362 $ ln -s foo a/a
363 $ hg shelve -q -n symlink a/a
364 $ hg status a/a
365 $ hg unshelve -q symlink
366 $ hg status a/a
367 M a/a
368 $ hg revert a/a
369
370 #endif
371
372 set up another conflict between a commit and a shelved change
373
374 $ hg revert -q -C -a
375 $ echo a >> a/a
376 $ hg shelve -q
377 $ echo x >> a/a
378 $ hg ci -m 'create conflict'
379 $ hg add foo/foo
380
381 if we resolve a conflict while unshelving, the unshelve should succeed
382
383 $ HGMERGE=true hg unshelve
384 unshelving change 'default'
385 adding changesets
386 adding manifests
387 adding file changes
388 added 1 changesets with 1 changes to 6 files (+1 heads)
389 merging a/a
390 0 files updated, 1 files merged, 0 files removed, 0 files unresolved
391 $ hg parents -q
392 4:be7e79683c99
393 $ hg shelve -l
394 $ hg status
395 M a/a
396 A foo/foo
397 $ cat a/a
398 a
399 c
400 x
401
402 test keep and cleanup
403
404 $ hg shelve
405 shelved from default (be7e7968): create conflict
406 shelved as default
407 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
408 $ hg shelve --list
409 default [*] shelved from default (be7e7968): create conflict (glob)
410 $ hg unshelve --keep
411 unshelving change 'default'
412 adding changesets
413 adding manifests
414 adding file changes
415 added 1 changesets with 1 changes to 7 files
416 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
417 $ hg shelve --list
418 default [*] shelved from default (be7e7968): create conflict (glob)
419 $ hg shelve --cleanup
420 $ hg shelve --list
@@ -1,552 +1,559
1 1 # color.py color output for the status and qseries commands
2 2 #
3 3 # Copyright (C) 2007 Kevin Christen <kevin.christen@gmail.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 '''colorize output from some commands
9 9
10 10 This extension modifies the status and resolve commands to add color
11 11 to their output to reflect file status, the qseries command to add
12 12 color to reflect patch status (applied, unapplied, missing), and to
13 13 diff-related commands to highlight additions, removals, diff headers,
14 14 and trailing whitespace.
15 15
16 16 Other effects in addition to color, like bold and underlined text, are
17 17 also available. By default, the terminfo database is used to find the
18 18 terminal codes used to change color and effect. If terminfo is not
19 19 available, then effects are rendered with the ECMA-48 SGR control
20 20 function (aka ANSI escape codes).
21 21
22 22 Default effects may be overridden from your configuration file::
23 23
24 24 [color]
25 25 status.modified = blue bold underline red_background
26 26 status.added = green bold
27 27 status.removed = red bold blue_background
28 28 status.deleted = cyan bold underline
29 29 status.unknown = magenta bold underline
30 30 status.ignored = black bold
31 31
32 32 # 'none' turns off all effects
33 33 status.clean = none
34 34 status.copied = none
35 35
36 36 qseries.applied = blue bold underline
37 37 qseries.unapplied = black bold
38 38 qseries.missing = red bold
39 39
40 40 diff.diffline = bold
41 41 diff.extended = cyan bold
42 42 diff.file_a = red bold
43 43 diff.file_b = green bold
44 44 diff.hunk = magenta
45 45 diff.deleted = red
46 46 diff.inserted = green
47 47 diff.changed = white
48 48 diff.trailingwhitespace = bold red_background
49 49
50 50 resolve.unresolved = red bold
51 51 resolve.resolved = green bold
52 52
53 53 bookmarks.current = green
54 54
55 55 branches.active = none
56 56 branches.closed = black bold
57 57 branches.current = green
58 58 branches.inactive = none
59 59
60 60 tags.normal = green
61 61 tags.local = black bold
62 62
63 63 rebase.rebased = blue
64 64 rebase.remaining = red bold
65 65
66 shelve.age = cyan
67 shelve.newest = green bold
68 shelve.name = blue bold
69
66 70 histedit.remaining = red bold
67 71
68 72 The available effects in terminfo mode are 'blink', 'bold', 'dim',
69 73 'inverse', 'invisible', 'italic', 'standout', and 'underline'; in
70 74 ECMA-48 mode, the options are 'bold', 'inverse', 'italic', and
71 75 'underline'. How each is rendered depends on the terminal emulator.
72 76 Some may not be available for a given terminal type, and will be
73 77 silently ignored.
74 78
75 79 Note that on some systems, terminfo mode may cause problems when using
76 80 color with the pager extension and less -R. less with the -R option
77 81 will only display ECMA-48 color codes, and terminfo mode may sometimes
78 82 emit codes that less doesn't understand. You can work around this by
79 83 either using ansi mode (or auto mode), or by using less -r (which will
80 84 pass through all terminal control codes, not just color control
81 85 codes).
82 86
83 87 Because there are only eight standard colors, this module allows you
84 88 to define color names for other color slots which might be available
85 89 for your terminal type, assuming terminfo mode. For instance::
86 90
87 91 color.brightblue = 12
88 92 color.pink = 207
89 93 color.orange = 202
90 94
91 95 to set 'brightblue' to color slot 12 (useful for 16 color terminals
92 96 that have brighter colors defined in the upper eight) and, 'pink' and
93 97 'orange' to colors in 256-color xterm's default color cube. These
94 98 defined colors may then be used as any of the pre-defined eight,
95 99 including appending '_background' to set the background to that color.
96 100
97 101 By default, the color extension will use ANSI mode (or win32 mode on
98 102 Windows) if it detects a terminal. To override auto mode (to enable
99 103 terminfo mode, for example), set the following configuration option::
100 104
101 105 [color]
102 106 mode = terminfo
103 107
104 108 Any value other than 'ansi', 'win32', 'terminfo', or 'auto' will
105 109 disable color.
106 110 '''
107 111
108 112 import os
109 113
110 114 from mercurial import commands, dispatch, extensions, ui as uimod, util
111 115 from mercurial import templater, error
112 116 from mercurial.i18n import _
113 117
114 118 testedwith = 'internal'
115 119
116 120 # start and stop parameters for effects
117 121 _effects = {'none': 0, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33,
118 122 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, 'bold': 1,
119 123 'italic': 3, 'underline': 4, 'inverse': 7,
120 124 'black_background': 40, 'red_background': 41,
121 125 'green_background': 42, 'yellow_background': 43,
122 126 'blue_background': 44, 'purple_background': 45,
123 127 'cyan_background': 46, 'white_background': 47}
124 128
125 129 def _terminfosetup(ui, mode):
126 130 '''Initialize terminfo data and the terminal if we're in terminfo mode.'''
127 131
128 132 global _terminfo_params
129 133 # If we failed to load curses, we go ahead and return.
130 134 if not _terminfo_params:
131 135 return
132 136 # Otherwise, see what the config file says.
133 137 if mode not in ('auto', 'terminfo'):
134 138 return
135 139
136 140 _terminfo_params.update((key[6:], (False, int(val)))
137 141 for key, val in ui.configitems('color')
138 142 if key.startswith('color.'))
139 143
140 144 try:
141 145 curses.setupterm()
142 146 except curses.error, e:
143 147 _terminfo_params = {}
144 148 return
145 149
146 150 for key, (b, e) in _terminfo_params.items():
147 151 if not b:
148 152 continue
149 153 if not curses.tigetstr(e):
150 154 # Most terminals don't support dim, invis, etc, so don't be
151 155 # noisy and use ui.debug().
152 156 ui.debug("no terminfo entry for %s\n" % e)
153 157 del _terminfo_params[key]
154 158 if not curses.tigetstr('setaf') or not curses.tigetstr('setab'):
155 159 # Only warn about missing terminfo entries if we explicitly asked for
156 160 # terminfo mode.
157 161 if mode == "terminfo":
158 162 ui.warn(_("no terminfo entry for setab/setaf: reverting to "
159 163 "ECMA-48 color\n"))
160 164 _terminfo_params = {}
161 165
162 166 def _modesetup(ui, coloropt):
163 167 global _terminfo_params
164 168
165 169 auto = coloropt == 'auto'
166 170 always = not auto and util.parsebool(coloropt)
167 171 if not always and not auto:
168 172 return None
169 173
170 174 formatted = always or (os.environ.get('TERM') != 'dumb' and ui.formatted())
171 175
172 176 mode = ui.config('color', 'mode', 'auto')
173 177 realmode = mode
174 178 if mode == 'auto':
175 179 if os.name == 'nt' and 'TERM' not in os.environ:
176 180 # looks line a cmd.exe console, use win32 API or nothing
177 181 realmode = 'win32'
178 182 else:
179 183 realmode = 'ansi'
180 184
181 185 if realmode == 'win32':
182 186 _terminfo_params = {}
183 187 if not w32effects:
184 188 if mode == 'win32':
185 189 # only warn if color.mode is explicitly set to win32
186 190 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
187 191 return None
188 192 _effects.update(w32effects)
189 193 elif realmode == 'ansi':
190 194 _terminfo_params = {}
191 195 elif realmode == 'terminfo':
192 196 _terminfosetup(ui, mode)
193 197 if not _terminfo_params:
194 198 if mode == 'terminfo':
195 199 ## FIXME Shouldn't we return None in this case too?
196 200 # only warn if color.mode is explicitly set to win32
197 201 ui.warn(_('warning: failed to set color mode to %s\n') % mode)
198 202 realmode = 'ansi'
199 203 else:
200 204 return None
201 205
202 206 if always or (auto and formatted):
203 207 return realmode
204 208 return None
205 209
206 210 try:
207 211 import curses
208 212 # Mapping from effect name to terminfo attribute name or color number.
209 213 # This will also force-load the curses module.
210 214 _terminfo_params = {'none': (True, 'sgr0'),
211 215 'standout': (True, 'smso'),
212 216 'underline': (True, 'smul'),
213 217 'reverse': (True, 'rev'),
214 218 'inverse': (True, 'rev'),
215 219 'blink': (True, 'blink'),
216 220 'dim': (True, 'dim'),
217 221 'bold': (True, 'bold'),
218 222 'invisible': (True, 'invis'),
219 223 'italic': (True, 'sitm'),
220 224 'black': (False, curses.COLOR_BLACK),
221 225 'red': (False, curses.COLOR_RED),
222 226 'green': (False, curses.COLOR_GREEN),
223 227 'yellow': (False, curses.COLOR_YELLOW),
224 228 'blue': (False, curses.COLOR_BLUE),
225 229 'magenta': (False, curses.COLOR_MAGENTA),
226 230 'cyan': (False, curses.COLOR_CYAN),
227 231 'white': (False, curses.COLOR_WHITE)}
228 232 except ImportError:
229 233 _terminfo_params = False
230 234
231 235 _styles = {'grep.match': 'red bold',
232 236 'grep.linenumber': 'green',
233 237 'grep.rev': 'green',
234 238 'grep.change': 'green',
235 239 'grep.sep': 'cyan',
236 240 'grep.filename': 'magenta',
237 241 'grep.user': 'magenta',
238 242 'grep.date': 'magenta',
239 243 'bookmarks.current': 'green',
240 244 'branches.active': 'none',
241 245 'branches.closed': 'black bold',
242 246 'branches.current': 'green',
243 247 'branches.inactive': 'none',
244 248 'diff.changed': 'white',
245 249 'diff.deleted': 'red',
246 250 'diff.diffline': 'bold',
247 251 'diff.extended': 'cyan bold',
248 252 'diff.file_a': 'red bold',
249 253 'diff.file_b': 'green bold',
250 254 'diff.hunk': 'magenta',
251 255 'diff.inserted': 'green',
252 256 'diff.trailingwhitespace': 'bold red_background',
253 257 'diffstat.deleted': 'red',
254 258 'diffstat.inserted': 'green',
255 259 'histedit.remaining': 'red bold',
256 260 'ui.prompt': 'yellow',
257 261 'log.changeset': 'yellow',
258 262 'rebase.rebased': 'blue',
259 263 'rebase.remaining': 'red bold',
260 264 'resolve.resolved': 'green bold',
261 265 'resolve.unresolved': 'red bold',
266 'shelve.age': 'cyan',
267 'shelve.newest': 'green bold',
268 'shelve.name': 'blue bold',
262 269 'status.added': 'green bold',
263 270 'status.clean': 'none',
264 271 'status.copied': 'none',
265 272 'status.deleted': 'cyan bold underline',
266 273 'status.ignored': 'black bold',
267 274 'status.modified': 'blue bold',
268 275 'status.removed': 'red bold',
269 276 'status.unknown': 'magenta bold underline',
270 277 'tags.normal': 'green',
271 278 'tags.local': 'black bold'}
272 279
273 280
274 281 def _effect_str(effect):
275 282 '''Helper function for render_effects().'''
276 283
277 284 bg = False
278 285 if effect.endswith('_background'):
279 286 bg = True
280 287 effect = effect[:-11]
281 288 attr, val = _terminfo_params[effect]
282 289 if attr:
283 290 return curses.tigetstr(val)
284 291 elif bg:
285 292 return curses.tparm(curses.tigetstr('setab'), val)
286 293 else:
287 294 return curses.tparm(curses.tigetstr('setaf'), val)
288 295
289 296 def render_effects(text, effects):
290 297 'Wrap text in commands to turn on each effect.'
291 298 if not text:
292 299 return text
293 300 if not _terminfo_params:
294 301 start = [str(_effects[e]) for e in ['none'] + effects.split()]
295 302 start = '\033[' + ';'.join(start) + 'm'
296 303 stop = '\033[' + str(_effects['none']) + 'm'
297 304 else:
298 305 start = ''.join(_effect_str(effect)
299 306 for effect in ['none'] + effects.split())
300 307 stop = _effect_str('none')
301 308 return ''.join([start, text, stop])
302 309
303 310 def extstyles():
304 311 for name, ext in extensions.extensions():
305 312 _styles.update(getattr(ext, 'colortable', {}))
306 313
307 314 def configstyles(ui):
308 315 for status, cfgeffects in ui.configitems('color'):
309 316 if '.' not in status or status.startswith('color.'):
310 317 continue
311 318 cfgeffects = ui.configlist('color', status)
312 319 if cfgeffects:
313 320 good = []
314 321 for e in cfgeffects:
315 322 if not _terminfo_params and e in _effects:
316 323 good.append(e)
317 324 elif e in _terminfo_params or e[:-11] in _terminfo_params:
318 325 good.append(e)
319 326 else:
320 327 ui.warn(_("ignoring unknown color/effect %r "
321 328 "(configured in color.%s)\n")
322 329 % (e, status))
323 330 _styles[status] = ' '.join(good)
324 331
325 332 class colorui(uimod.ui):
326 333 def popbuffer(self, labeled=False):
327 334 if self._colormode is None:
328 335 return super(colorui, self).popbuffer(labeled)
329 336
330 337 if labeled:
331 338 return ''.join(self.label(a, label) for a, label
332 339 in self._buffers.pop())
333 340 return ''.join(a for a, label in self._buffers.pop())
334 341
335 342 _colormode = 'ansi'
336 343 def write(self, *args, **opts):
337 344 if self._colormode is None:
338 345 return super(colorui, self).write(*args, **opts)
339 346
340 347 label = opts.get('label', '')
341 348 if self._buffers:
342 349 self._buffers[-1].extend([(str(a), label) for a in args])
343 350 elif self._colormode == 'win32':
344 351 for a in args:
345 352 win32print(a, super(colorui, self).write, **opts)
346 353 else:
347 354 return super(colorui, self).write(
348 355 *[self.label(str(a), label) for a in args], **opts)
349 356
350 357 def write_err(self, *args, **opts):
351 358 if self._colormode is None:
352 359 return super(colorui, self).write_err(*args, **opts)
353 360
354 361 label = opts.get('label', '')
355 362 if self._colormode == 'win32':
356 363 for a in args:
357 364 win32print(a, super(colorui, self).write_err, **opts)
358 365 else:
359 366 return super(colorui, self).write_err(
360 367 *[self.label(str(a), label) for a in args], **opts)
361 368
362 369 def label(self, msg, label):
363 370 if self._colormode is None:
364 371 return super(colorui, self).label(msg, label)
365 372
366 373 effects = []
367 374 for l in label.split():
368 375 s = _styles.get(l, '')
369 376 if s:
370 377 effects.append(s)
371 378 effects = ' '.join(effects)
372 379 if effects:
373 380 return '\n'.join([render_effects(s, effects)
374 381 for s in msg.split('\n')])
375 382 return msg
376 383
377 384 def templatelabel(context, mapping, args):
378 385 if len(args) != 2:
379 386 # i18n: "label" is a keyword
380 387 raise error.ParseError(_("label expects two arguments"))
381 388
382 389 thing = templater.stringify(args[1][0](context, mapping, args[1][1]))
383 390 thing = templater.runtemplate(context, mapping,
384 391 templater.compiletemplate(thing, context))
385 392
386 393 # apparently, repo could be a string that is the favicon?
387 394 repo = mapping.get('repo', '')
388 395 if isinstance(repo, str):
389 396 return thing
390 397
391 398 label = templater.stringify(args[0][0](context, mapping, args[0][1]))
392 399 label = templater.runtemplate(context, mapping,
393 400 templater.compiletemplate(label, context))
394 401
395 402 thing = templater.stringify(thing)
396 403 label = templater.stringify(label)
397 404
398 405 return repo.ui.label(thing, label)
399 406
400 407 def uisetup(ui):
401 408 if ui.plain():
402 409 return
403 410 if not isinstance(ui, colorui):
404 411 colorui.__bases__ = (ui.__class__,)
405 412 ui.__class__ = colorui
406 413 def colorcmd(orig, ui_, opts, cmd, cmdfunc):
407 414 mode = _modesetup(ui_, opts['color'])
408 415 colorui._colormode = mode
409 416 if mode:
410 417 extstyles()
411 418 configstyles(ui_)
412 419 return orig(ui_, opts, cmd, cmdfunc)
413 420 extensions.wrapfunction(dispatch, '_runcommand', colorcmd)
414 421 templater.funcs['label'] = templatelabel
415 422
416 423 def extsetup(ui):
417 424 commands.globalopts.append(
418 425 ('', 'color', 'auto',
419 426 # i18n: 'always', 'auto', and 'never' are keywords and should
420 427 # not be translated
421 428 _("when to colorize (boolean, always, auto, or never)"),
422 429 _('TYPE')))
423 430
424 431 if os.name != 'nt':
425 432 w32effects = None
426 433 else:
427 434 import re, ctypes
428 435
429 436 _kernel32 = ctypes.windll.kernel32
430 437
431 438 _WORD = ctypes.c_ushort
432 439
433 440 _INVALID_HANDLE_VALUE = -1
434 441
435 442 class _COORD(ctypes.Structure):
436 443 _fields_ = [('X', ctypes.c_short),
437 444 ('Y', ctypes.c_short)]
438 445
439 446 class _SMALL_RECT(ctypes.Structure):
440 447 _fields_ = [('Left', ctypes.c_short),
441 448 ('Top', ctypes.c_short),
442 449 ('Right', ctypes.c_short),
443 450 ('Bottom', ctypes.c_short)]
444 451
445 452 class _CONSOLE_SCREEN_BUFFER_INFO(ctypes.Structure):
446 453 _fields_ = [('dwSize', _COORD),
447 454 ('dwCursorPosition', _COORD),
448 455 ('wAttributes', _WORD),
449 456 ('srWindow', _SMALL_RECT),
450 457 ('dwMaximumWindowSize', _COORD)]
451 458
452 459 _STD_OUTPUT_HANDLE = 0xfffffff5L # (DWORD)-11
453 460 _STD_ERROR_HANDLE = 0xfffffff4L # (DWORD)-12
454 461
455 462 _FOREGROUND_BLUE = 0x0001
456 463 _FOREGROUND_GREEN = 0x0002
457 464 _FOREGROUND_RED = 0x0004
458 465 _FOREGROUND_INTENSITY = 0x0008
459 466
460 467 _BACKGROUND_BLUE = 0x0010
461 468 _BACKGROUND_GREEN = 0x0020
462 469 _BACKGROUND_RED = 0x0040
463 470 _BACKGROUND_INTENSITY = 0x0080
464 471
465 472 _COMMON_LVB_REVERSE_VIDEO = 0x4000
466 473 _COMMON_LVB_UNDERSCORE = 0x8000
467 474
468 475 # http://msdn.microsoft.com/en-us/library/ms682088%28VS.85%29.aspx
469 476 w32effects = {
470 477 'none': -1,
471 478 'black': 0,
472 479 'red': _FOREGROUND_RED,
473 480 'green': _FOREGROUND_GREEN,
474 481 'yellow': _FOREGROUND_RED | _FOREGROUND_GREEN,
475 482 'blue': _FOREGROUND_BLUE,
476 483 'magenta': _FOREGROUND_BLUE | _FOREGROUND_RED,
477 484 'cyan': _FOREGROUND_BLUE | _FOREGROUND_GREEN,
478 485 'white': _FOREGROUND_RED | _FOREGROUND_GREEN | _FOREGROUND_BLUE,
479 486 'bold': _FOREGROUND_INTENSITY,
480 487 'black_background': 0x100, # unused value > 0x0f
481 488 'red_background': _BACKGROUND_RED,
482 489 'green_background': _BACKGROUND_GREEN,
483 490 'yellow_background': _BACKGROUND_RED | _BACKGROUND_GREEN,
484 491 'blue_background': _BACKGROUND_BLUE,
485 492 'purple_background': _BACKGROUND_BLUE | _BACKGROUND_RED,
486 493 'cyan_background': _BACKGROUND_BLUE | _BACKGROUND_GREEN,
487 494 'white_background': (_BACKGROUND_RED | _BACKGROUND_GREEN |
488 495 _BACKGROUND_BLUE),
489 496 'bold_background': _BACKGROUND_INTENSITY,
490 497 'underline': _COMMON_LVB_UNDERSCORE, # double-byte charsets only
491 498 'inverse': _COMMON_LVB_REVERSE_VIDEO, # double-byte charsets only
492 499 }
493 500
494 501 passthrough = set([_FOREGROUND_INTENSITY,
495 502 _BACKGROUND_INTENSITY,
496 503 _COMMON_LVB_UNDERSCORE,
497 504 _COMMON_LVB_REVERSE_VIDEO])
498 505
499 506 stdout = _kernel32.GetStdHandle(
500 507 _STD_OUTPUT_HANDLE) # don't close the handle returned
501 508 if stdout is None or stdout == _INVALID_HANDLE_VALUE:
502 509 w32effects = None
503 510 else:
504 511 csbi = _CONSOLE_SCREEN_BUFFER_INFO()
505 512 if not _kernel32.GetConsoleScreenBufferInfo(
506 513 stdout, ctypes.byref(csbi)):
507 514 # stdout may not support GetConsoleScreenBufferInfo()
508 515 # when called from subprocess or redirected
509 516 w32effects = None
510 517 else:
511 518 origattr = csbi.wAttributes
512 519 ansire = re.compile('\033\[([^m]*)m([^\033]*)(.*)',
513 520 re.MULTILINE | re.DOTALL)
514 521
515 522 def win32print(text, orig, **opts):
516 523 label = opts.get('label', '')
517 524 attr = origattr
518 525
519 526 def mapcolor(val, attr):
520 527 if val == -1:
521 528 return origattr
522 529 elif val in passthrough:
523 530 return attr | val
524 531 elif val > 0x0f:
525 532 return (val & 0x70) | (attr & 0x8f)
526 533 else:
527 534 return (val & 0x07) | (attr & 0xf8)
528 535
529 536 # determine console attributes based on labels
530 537 for l in label.split():
531 538 style = _styles.get(l, '')
532 539 for effect in style.split():
533 540 attr = mapcolor(w32effects[effect], attr)
534 541
535 542 # hack to ensure regexp finds data
536 543 if not text.startswith('\033['):
537 544 text = '\033[m' + text
538 545
539 546 # Look for ANSI-like codes embedded in text
540 547 m = re.match(ansire, text)
541 548
542 549 try:
543 550 while m:
544 551 for sattr in m.group(1).split(';'):
545 552 if sattr:
546 553 attr = mapcolor(int(sattr), attr)
547 554 _kernel32.SetConsoleTextAttribute(stdout, attr)
548 555 orig(m.group(2), **opts)
549 556 m = re.match(ansire, m.group(3))
550 557 finally:
551 558 # Explicitly reset original attributes
552 559 _kernel32.SetConsoleTextAttribute(stdout, origattr)
@@ -1,1270 +1,1271
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
61 61 processlock = threading.Lock()
62 62
63 63 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
64 64 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
65 65 # zombies but it's pretty harmless even if we do.
66 66 if sys.version_info[1] < 5:
67 67 subprocess._cleanup = lambda: None
68 68
69 69 closefds = os.name == 'posix'
70 70 def Popen4(cmd, wd, timeout, env=None):
71 71 processlock.acquire()
72 72 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
73 73 close_fds=closefds,
74 74 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
75 75 stderr=subprocess.STDOUT)
76 76 processlock.release()
77 77
78 78 p.fromchild = p.stdout
79 79 p.tochild = p.stdin
80 80 p.childerr = p.stderr
81 81
82 82 p.timeout = False
83 83 if timeout:
84 84 def t():
85 85 start = time.time()
86 86 while time.time() - start < timeout and p.returncode is None:
87 87 time.sleep(.1)
88 88 p.timeout = True
89 89 if p.returncode is None:
90 90 terminate(p)
91 91 threading.Thread(target=t).start()
92 92
93 93 return p
94 94
95 95 # reserved exit code to skip test (used by hghave)
96 96 SKIPPED_STATUS = 80
97 97 SKIPPED_PREFIX = 'skipped: '
98 98 FAILED_PREFIX = 'hghave check failed: '
99 99 PYTHON = sys.executable.replace('\\', '/')
100 100 IMPL_PATH = 'PYTHONPATH'
101 101 if 'java' in sys.platform:
102 102 IMPL_PATH = 'JYTHONPATH'
103 103
104 104 requiredtools = [os.path.basename(sys.executable), "diff", "grep", "unzip",
105 105 "gunzip", "bunzip2", "sed"]
106 106
107 107 defaults = {
108 108 'jobs': ('HGTEST_JOBS', 1),
109 109 'timeout': ('HGTEST_TIMEOUT', 180),
110 110 'port': ('HGTEST_PORT', 20059),
111 111 'shell': ('HGTEST_SHELL', 'sh'),
112 112 }
113 113
114 114 def parselistfiles(files, listtype, warn=True):
115 115 entries = dict()
116 116 for filename in files:
117 117 try:
118 118 path = os.path.expanduser(os.path.expandvars(filename))
119 119 f = open(path, "r")
120 120 except IOError, err:
121 121 if err.errno != errno.ENOENT:
122 122 raise
123 123 if warn:
124 124 print "warning: no such %s file: %s" % (listtype, filename)
125 125 continue
126 126
127 127 for line in f.readlines():
128 128 line = line.split('#', 1)[0].strip()
129 129 if line:
130 130 entries[line] = filename
131 131
132 132 f.close()
133 133 return entries
134 134
135 135 def parseargs():
136 136 parser = optparse.OptionParser("%prog [options] [tests]")
137 137
138 138 # keep these sorted
139 139 parser.add_option("--blacklist", action="append",
140 140 help="skip tests listed in the specified blacklist file")
141 141 parser.add_option("--whitelist", action="append",
142 142 help="always run tests listed in the specified whitelist file")
143 143 parser.add_option("-C", "--annotate", action="store_true",
144 144 help="output files annotated with coverage")
145 145 parser.add_option("-c", "--cover", action="store_true",
146 146 help="print a test coverage report")
147 147 parser.add_option("-d", "--debug", action="store_true",
148 148 help="debug mode: write output of test scripts to console"
149 149 " rather than capturing and diff'ing it (disables timeout)")
150 150 parser.add_option("-f", "--first", action="store_true",
151 151 help="exit on the first test failure")
152 152 parser.add_option("-H", "--htmlcov", action="store_true",
153 153 help="create an HTML report of the coverage of the files")
154 154 parser.add_option("--inotify", action="store_true",
155 155 help="enable inotify extension when running tests")
156 156 parser.add_option("-i", "--interactive", action="store_true",
157 157 help="prompt to accept changed output")
158 158 parser.add_option("-j", "--jobs", type="int",
159 159 help="number of jobs to run in parallel"
160 160 " (default: $%s or %d)" % defaults['jobs'])
161 161 parser.add_option("--keep-tmpdir", action="store_true",
162 162 help="keep temporary directory after running tests")
163 163 parser.add_option("-k", "--keywords",
164 164 help="run tests matching keywords")
165 165 parser.add_option("-l", "--local", action="store_true",
166 166 help="shortcut for --with-hg=<testdir>/../hg")
167 167 parser.add_option("--loop", action="store_true",
168 168 help="loop tests repeatedly")
169 169 parser.add_option("-n", "--nodiff", action="store_true",
170 170 help="skip showing test changes")
171 171 parser.add_option("-p", "--port", type="int",
172 172 help="port on which servers should listen"
173 173 " (default: $%s or %d)" % defaults['port'])
174 174 parser.add_option("--compiler", type="string",
175 175 help="compiler to build with")
176 176 parser.add_option("--pure", action="store_true",
177 177 help="use pure Python code instead of C extensions")
178 178 parser.add_option("-R", "--restart", action="store_true",
179 179 help="restart at last error")
180 180 parser.add_option("-r", "--retest", action="store_true",
181 181 help="retest failed tests")
182 182 parser.add_option("-S", "--noskips", action="store_true",
183 183 help="don't report skip tests verbosely")
184 184 parser.add_option("--shell", type="string",
185 185 help="shell to use (default: $%s or %s)" % defaults['shell'])
186 186 parser.add_option("-t", "--timeout", type="int",
187 187 help="kill errant tests after TIMEOUT seconds"
188 188 " (default: $%s or %d)" % defaults['timeout'])
189 189 parser.add_option("--time", action="store_true",
190 190 help="time how long each test takes")
191 191 parser.add_option("--tmpdir", type="string",
192 192 help="run tests in the given temporary directory"
193 193 " (implies --keep-tmpdir)")
194 194 parser.add_option("-v", "--verbose", action="store_true",
195 195 help="output verbose messages")
196 196 parser.add_option("--view", type="string",
197 197 help="external diff viewer")
198 198 parser.add_option("--with-hg", type="string",
199 199 metavar="HG",
200 200 help="test using specified hg script rather than a "
201 201 "temporary installation")
202 202 parser.add_option("-3", "--py3k-warnings", action="store_true",
203 203 help="enable Py3k warnings on Python 2.6+")
204 204 parser.add_option('--extra-config-opt', action="append",
205 205 help='set the given config opt in the test hgrc')
206 206 parser.add_option('--random', action="store_true",
207 207 help='run tests in random order')
208 208
209 209 for option, (envvar, default) in defaults.items():
210 210 defaults[option] = type(default)(os.environ.get(envvar, default))
211 211 parser.set_defaults(**defaults)
212 212 (options, args) = parser.parse_args()
213 213
214 214 # jython is always pure
215 215 if 'java' in sys.platform or '__pypy__' in sys.modules:
216 216 options.pure = True
217 217
218 218 if options.with_hg:
219 219 options.with_hg = os.path.expanduser(options.with_hg)
220 220 if not (os.path.isfile(options.with_hg) and
221 221 os.access(options.with_hg, os.X_OK)):
222 222 parser.error('--with-hg must specify an executable hg script')
223 223 if not os.path.basename(options.with_hg) == 'hg':
224 224 sys.stderr.write('warning: --with-hg should specify an hg script\n')
225 225 if options.local:
226 226 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
227 227 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
228 228 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
229 229 parser.error('--local specified, but %r not found or not executable'
230 230 % hgbin)
231 231 options.with_hg = hgbin
232 232
233 233 options.anycoverage = options.cover or options.annotate or options.htmlcov
234 234 if options.anycoverage:
235 235 try:
236 236 import coverage
237 237 covver = version.StrictVersion(coverage.__version__).version
238 238 if covver < (3, 3):
239 239 parser.error('coverage options require coverage 3.3 or later')
240 240 except ImportError:
241 241 parser.error('coverage options now require the coverage package')
242 242
243 243 if options.anycoverage and options.local:
244 244 # this needs some path mangling somewhere, I guess
245 245 parser.error("sorry, coverage options do not work when --local "
246 246 "is specified")
247 247
248 248 global verbose
249 249 if options.verbose:
250 250 verbose = ''
251 251
252 252 if options.tmpdir:
253 253 options.tmpdir = os.path.expanduser(options.tmpdir)
254 254
255 255 if options.jobs < 1:
256 256 parser.error('--jobs must be positive')
257 257 if options.interactive and options.debug:
258 258 parser.error("-i/--interactive and -d/--debug are incompatible")
259 259 if options.debug:
260 260 if options.timeout != defaults['timeout']:
261 261 sys.stderr.write(
262 262 'warning: --timeout option ignored with --debug\n')
263 263 options.timeout = 0
264 264 if options.py3k_warnings:
265 265 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
266 266 parser.error('--py3k-warnings can only be used on Python 2.6+')
267 267 if options.blacklist:
268 268 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
269 269 if options.whitelist:
270 270 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
271 271 else:
272 272 options.whitelisted = {}
273 273
274 274 return (options, args)
275 275
276 276 def rename(src, dst):
277 277 """Like os.rename(), trade atomicity and opened files friendliness
278 278 for existing destination support.
279 279 """
280 280 shutil.copy(src, dst)
281 281 os.remove(src)
282 282
283 283 def parsehghaveoutput(lines):
284 284 '''Parse hghave log lines.
285 285 Return tuple of lists (missing, failed):
286 286 * the missing/unknown features
287 287 * the features for which existence check failed'''
288 288 missing = []
289 289 failed = []
290 290 for line in lines:
291 291 if line.startswith(SKIPPED_PREFIX):
292 292 line = line.splitlines()[0]
293 293 missing.append(line[len(SKIPPED_PREFIX):])
294 294 elif line.startswith(FAILED_PREFIX):
295 295 line = line.splitlines()[0]
296 296 failed.append(line[len(FAILED_PREFIX):])
297 297
298 298 return missing, failed
299 299
300 300 def showdiff(expected, output, ref, err):
301 301 print
302 302 for line in difflib.unified_diff(expected, output, ref, err):
303 303 sys.stdout.write(line)
304 304
305 305 verbose = False
306 306 def vlog(*msg):
307 307 if verbose is not False:
308 308 iolock.acquire()
309 309 if verbose:
310 310 print verbose,
311 311 for m in msg:
312 312 print m,
313 313 print
314 314 sys.stdout.flush()
315 315 iolock.release()
316 316
317 317 def log(*msg):
318 318 iolock.acquire()
319 319 if verbose:
320 320 print verbose,
321 321 for m in msg:
322 322 print m,
323 323 print
324 324 sys.stdout.flush()
325 325 iolock.release()
326 326
327 327 def findprogram(program):
328 328 """Search PATH for a executable program"""
329 329 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
330 330 name = os.path.join(p, program)
331 331 if os.name == 'nt' or os.access(name, os.X_OK):
332 332 return name
333 333 return None
334 334
335 335 def createhgrc(path, options):
336 336 # create a fresh hgrc
337 337 hgrc = open(path, 'w')
338 338 hgrc.write('[ui]\n')
339 339 hgrc.write('slash = True\n')
340 340 hgrc.write('interactive = False\n')
341 341 hgrc.write('[defaults]\n')
342 342 hgrc.write('backout = -d "0 0"\n')
343 343 hgrc.write('commit = -d "0 0"\n')
344 hgrc.write('shelve = --date "0 0"\n')
344 345 hgrc.write('tag = -d "0 0"\n')
345 346 if options.inotify:
346 347 hgrc.write('[extensions]\n')
347 348 hgrc.write('inotify=\n')
348 349 hgrc.write('[inotify]\n')
349 350 hgrc.write('pidfile=daemon.pids')
350 351 hgrc.write('appendpid=True\n')
351 352 if options.extra_config_opt:
352 353 for opt in options.extra_config_opt:
353 354 section, key = opt.split('.', 1)
354 355 assert '=' in key, ('extra config opt %s must '
355 356 'have an = for assignment' % opt)
356 357 hgrc.write('[%s]\n%s\n' % (section, key))
357 358 hgrc.close()
358 359
359 360 def createenv(options, testtmp, threadtmp, port):
360 361 env = os.environ.copy()
361 362 env['TESTTMP'] = testtmp
362 363 env['HOME'] = testtmp
363 364 env["HGPORT"] = str(port)
364 365 env["HGPORT1"] = str(port + 1)
365 366 env["HGPORT2"] = str(port + 2)
366 367 env["HGRCPATH"] = os.path.join(threadtmp, '.hgrc')
367 368 env["DAEMON_PIDS"] = os.path.join(threadtmp, 'daemon.pids')
368 369 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
369 370 env["HGMERGE"] = "internal:merge"
370 371 env["HGUSER"] = "test"
371 372 env["HGENCODING"] = "ascii"
372 373 env["HGENCODINGMODE"] = "strict"
373 374
374 375 # Reset some environment variables to well-known values so that
375 376 # the tests produce repeatable output.
376 377 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
377 378 env['TZ'] = 'GMT'
378 379 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
379 380 env['COLUMNS'] = '80'
380 381 env['TERM'] = 'xterm'
381 382
382 383 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
383 384 'NO_PROXY').split():
384 385 if k in env:
385 386 del env[k]
386 387
387 388 # unset env related to hooks
388 389 for k in env.keys():
389 390 if k.startswith('HG_'):
390 391 del env[k]
391 392
392 393 return env
393 394
394 395 def checktools():
395 396 # Before we go any further, check for pre-requisite tools
396 397 # stuff from coreutils (cat, rm, etc) are not tested
397 398 for p in requiredtools:
398 399 if os.name == 'nt' and not p.endswith('.exe'):
399 400 p += '.exe'
400 401 found = findprogram(p)
401 402 if found:
402 403 vlog("# Found prerequisite", p, "at", found)
403 404 else:
404 405 print "WARNING: Did not find prerequisite tool: "+p
405 406
406 407 def terminate(proc):
407 408 """Terminate subprocess (with fallback for Python versions < 2.6)"""
408 409 vlog('# Terminating process %d' % proc.pid)
409 410 try:
410 411 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
411 412 except OSError:
412 413 pass
413 414
414 415 def killdaemons(pidfile):
415 416 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
416 417 logfn=vlog)
417 418
418 419 def cleanup(options):
419 420 if not options.keep_tmpdir:
420 421 vlog("# Cleaning up HGTMP", HGTMP)
421 422 shutil.rmtree(HGTMP, True)
422 423
423 424 def usecorrectpython():
424 425 # some tests run python interpreter. they must use same
425 426 # interpreter we use or bad things will happen.
426 427 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
427 428 if getattr(os, 'symlink', None):
428 429 vlog("# Making python executable in test path a symlink to '%s'" %
429 430 sys.executable)
430 431 mypython = os.path.join(BINDIR, pyexename)
431 432 try:
432 433 if os.readlink(mypython) == sys.executable:
433 434 return
434 435 os.unlink(mypython)
435 436 except OSError, err:
436 437 if err.errno != errno.ENOENT:
437 438 raise
438 439 if findprogram(pyexename) != sys.executable:
439 440 try:
440 441 os.symlink(sys.executable, mypython)
441 442 except OSError, err:
442 443 # child processes may race, which is harmless
443 444 if err.errno != errno.EEXIST:
444 445 raise
445 446 else:
446 447 exedir, exename = os.path.split(sys.executable)
447 448 vlog("# Modifying search path to find %s as %s in '%s'" %
448 449 (exename, pyexename, exedir))
449 450 path = os.environ['PATH'].split(os.pathsep)
450 451 while exedir in path:
451 452 path.remove(exedir)
452 453 os.environ['PATH'] = os.pathsep.join([exedir] + path)
453 454 if not findprogram(pyexename):
454 455 print "WARNING: Cannot find %s in search path" % pyexename
455 456
456 457 def installhg(options):
457 458 vlog("# Performing temporary installation of HG")
458 459 installerrs = os.path.join("tests", "install.err")
459 460 compiler = ''
460 461 if options.compiler:
461 462 compiler = '--compiler ' + options.compiler
462 463 pure = options.pure and "--pure" or ""
463 464 py3 = ''
464 465 if sys.version_info[0] == 3:
465 466 py3 = '--c2to3'
466 467
467 468 # Run installer in hg root
468 469 script = os.path.realpath(sys.argv[0])
469 470 hgroot = os.path.dirname(os.path.dirname(script))
470 471 os.chdir(hgroot)
471 472 nohome = '--home=""'
472 473 if os.name == 'nt':
473 474 # The --home="" trick works only on OS where os.sep == '/'
474 475 # because of a distutils convert_path() fast-path. Avoid it at
475 476 # least on Windows for now, deal with .pydistutils.cfg bugs
476 477 # when they happen.
477 478 nohome = ''
478 479 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
479 480 ' build %(compiler)s --build-base="%(base)s"'
480 481 ' install --force --prefix="%(prefix)s" --install-lib="%(libdir)s"'
481 482 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
482 483 % dict(exe=sys.executable, py3=py3, pure=pure, compiler=compiler,
483 484 base=os.path.join(HGTMP, "build"),
484 485 prefix=INST, libdir=PYTHONDIR, bindir=BINDIR,
485 486 nohome=nohome, logfile=installerrs))
486 487 vlog("# Running", cmd)
487 488 if os.system(cmd) == 0:
488 489 if not options.verbose:
489 490 os.remove(installerrs)
490 491 else:
491 492 f = open(installerrs)
492 493 for line in f:
493 494 print line,
494 495 f.close()
495 496 sys.exit(1)
496 497 os.chdir(TESTDIR)
497 498
498 499 usecorrectpython()
499 500
500 501 vlog("# Installing dummy diffstat")
501 502 f = open(os.path.join(BINDIR, 'diffstat'), 'w')
502 503 f.write('#!' + sys.executable + '\n'
503 504 'import sys\n'
504 505 'files = 0\n'
505 506 'for line in sys.stdin:\n'
506 507 ' if line.startswith("diff "):\n'
507 508 ' files += 1\n'
508 509 'sys.stdout.write("files patched: %d\\n" % files)\n')
509 510 f.close()
510 511 os.chmod(os.path.join(BINDIR, 'diffstat'), 0700)
511 512
512 513 if options.py3k_warnings and not options.anycoverage:
513 514 vlog("# Updating hg command to enable Py3k Warnings switch")
514 515 f = open(os.path.join(BINDIR, 'hg'), 'r')
515 516 lines = [line.rstrip() for line in f]
516 517 lines[0] += ' -3'
517 518 f.close()
518 519 f = open(os.path.join(BINDIR, 'hg'), 'w')
519 520 for line in lines:
520 521 f.write(line + '\n')
521 522 f.close()
522 523
523 524 hgbat = os.path.join(BINDIR, 'hg.bat')
524 525 if os.path.isfile(hgbat):
525 526 # hg.bat expects to be put in bin/scripts while run-tests.py
526 527 # installation layout put it in bin/ directly. Fix it
527 528 f = open(hgbat, 'rb')
528 529 data = f.read()
529 530 f.close()
530 531 if '"%~dp0..\python" "%~dp0hg" %*' in data:
531 532 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
532 533 '"%~dp0python" "%~dp0hg" %*')
533 534 f = open(hgbat, 'wb')
534 535 f.write(data)
535 536 f.close()
536 537 else:
537 538 print 'WARNING: cannot fix hg.bat reference to python.exe'
538 539
539 540 if options.anycoverage:
540 541 custom = os.path.join(TESTDIR, 'sitecustomize.py')
541 542 target = os.path.join(PYTHONDIR, 'sitecustomize.py')
542 543 vlog('# Installing coverage trigger to %s' % target)
543 544 shutil.copyfile(custom, target)
544 545 rc = os.path.join(TESTDIR, '.coveragerc')
545 546 vlog('# Installing coverage rc to %s' % rc)
546 547 os.environ['COVERAGE_PROCESS_START'] = rc
547 548 fn = os.path.join(INST, '..', '.coverage')
548 549 os.environ['COVERAGE_FILE'] = fn
549 550
550 551 def outputtimes(options):
551 552 vlog('# Producing time report')
552 553 times.sort(key=lambda t: (t[1], t[0]), reverse=True)
553 554 cols = '%7.3f %s'
554 555 print '\n%-7s %s' % ('Time', 'Test')
555 556 for test, timetaken in times:
556 557 print cols % (timetaken, test)
557 558
558 559 def outputcoverage(options):
559 560
560 561 vlog('# Producing coverage report')
561 562 os.chdir(PYTHONDIR)
562 563
563 564 def covrun(*args):
564 565 cmd = 'coverage %s' % ' '.join(args)
565 566 vlog('# Running: %s' % cmd)
566 567 os.system(cmd)
567 568
568 569 covrun('-c')
569 570 omit = ','.join(os.path.join(x, '*') for x in [BINDIR, TESTDIR])
570 571 covrun('-i', '-r', '"--omit=%s"' % omit) # report
571 572 if options.htmlcov:
572 573 htmldir = os.path.join(TESTDIR, 'htmlcov')
573 574 covrun('-i', '-b', '"--directory=%s"' % htmldir, '"--omit=%s"' % omit)
574 575 if options.annotate:
575 576 adir = os.path.join(TESTDIR, 'annotated')
576 577 if not os.path.isdir(adir):
577 578 os.mkdir(adir)
578 579 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
579 580
580 581 def pytest(test, wd, options, replacements, env):
581 582 py3kswitch = options.py3k_warnings and ' -3' or ''
582 583 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, test)
583 584 vlog("# Running", cmd)
584 585 if os.name == 'nt':
585 586 replacements.append((r'\r\n', '\n'))
586 587 return run(cmd, wd, options, replacements, env)
587 588
588 589 needescape = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
589 590 escapesub = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
590 591 escapemap = dict((chr(i), r'\x%02x' % i) for i in range(256))
591 592 escapemap.update({'\\': '\\\\', '\r': r'\r'})
592 593 def escapef(m):
593 594 return escapemap[m.group(0)]
594 595 def stringescape(s):
595 596 return escapesub(escapef, s)
596 597
597 598 def rematch(el, l):
598 599 try:
599 600 # use \Z to ensure that the regex matches to the end of the string
600 601 if os.name == 'nt':
601 602 return re.match(el + r'\r?\n\Z', l)
602 603 return re.match(el + r'\n\Z', l)
603 604 except re.error:
604 605 # el is an invalid regex
605 606 return False
606 607
607 608 def globmatch(el, l):
608 609 # The only supported special characters are * and ? plus / which also
609 610 # matches \ on windows. Escaping of these caracters is supported.
610 611 if el + '\n' == l:
611 612 if os.altsep:
612 613 # matching on "/" is not needed for this line
613 614 log("\nInfo, unnecessary glob: %s (glob)" % el)
614 615 return True
615 616 i, n = 0, len(el)
616 617 res = ''
617 618 while i < n:
618 619 c = el[i]
619 620 i += 1
620 621 if c == '\\' and el[i] in '*?\\/':
621 622 res += el[i - 1:i + 1]
622 623 i += 1
623 624 elif c == '*':
624 625 res += '.*'
625 626 elif c == '?':
626 627 res += '.'
627 628 elif c == '/' and os.altsep:
628 629 res += '[/\\\\]'
629 630 else:
630 631 res += re.escape(c)
631 632 return rematch(res, l)
632 633
633 634 def linematch(el, l):
634 635 if el == l: # perfect match (fast)
635 636 return True
636 637 if el:
637 638 if el.endswith(" (esc)\n"):
638 639 el = el[:-7].decode('string-escape') + '\n'
639 640 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
640 641 return True
641 642 if (el.endswith(" (re)\n") and rematch(el[:-6], l) or
642 643 el.endswith(" (glob)\n") and globmatch(el[:-8], l)):
643 644 return True
644 645 return False
645 646
646 647 def tsttest(test, wd, options, replacements, env):
647 648 # We generate a shell script which outputs unique markers to line
648 649 # up script results with our source. These markers include input
649 650 # line number and the last return code
650 651 salt = "SALT" + str(time.time())
651 652 def addsalt(line, inpython):
652 653 if inpython:
653 654 script.append('%s %d 0\n' % (salt, line))
654 655 else:
655 656 script.append('echo %s %s $?\n' % (salt, line))
656 657
657 658 # After we run the shell script, we re-unify the script output
658 659 # with non-active parts of the source, with synchronization by our
659 660 # SALT line number markers. The after table contains the
660 661 # non-active components, ordered by line number
661 662 after = {}
662 663 pos = prepos = -1
663 664
664 665 # Expected shellscript output
665 666 expected = {}
666 667
667 668 # We keep track of whether or not we're in a Python block so we
668 669 # can generate the surrounding doctest magic
669 670 inpython = False
670 671
671 672 # True or False when in a true or false conditional section
672 673 skipping = None
673 674
674 675 def hghave(reqs):
675 676 # TODO: do something smarter when all other uses of hghave is gone
676 677 tdir = TESTDIR.replace('\\', '/')
677 678 proc = Popen4('%s -c "%s/hghave %s"' %
678 679 (options.shell, tdir, ' '.join(reqs)), wd, 0)
679 680 stdout, stderr = proc.communicate()
680 681 ret = proc.wait()
681 682 if wifexited(ret):
682 683 ret = os.WEXITSTATUS(ret)
683 684 if ret == 2:
684 685 print stdout
685 686 sys.exit(1)
686 687 return ret == 0
687 688
688 689 f = open(test)
689 690 t = f.readlines()
690 691 f.close()
691 692
692 693 script = []
693 694 if options.debug:
694 695 script.append('set -x\n')
695 696 if os.getenv('MSYSTEM'):
696 697 script.append('alias pwd="pwd -W"\n')
697 698 n = 0
698 699 for n, l in enumerate(t):
699 700 if not l.endswith('\n'):
700 701 l += '\n'
701 702 if l.startswith('#if'):
702 703 if skipping is not None:
703 704 after.setdefault(pos, []).append(' !!! nested #if\n')
704 705 skipping = not hghave(l.split()[1:])
705 706 after.setdefault(pos, []).append(l)
706 707 elif l.startswith('#else'):
707 708 if skipping is None:
708 709 after.setdefault(pos, []).append(' !!! missing #if\n')
709 710 skipping = not skipping
710 711 after.setdefault(pos, []).append(l)
711 712 elif l.startswith('#endif'):
712 713 if skipping is None:
713 714 after.setdefault(pos, []).append(' !!! missing #if\n')
714 715 skipping = None
715 716 after.setdefault(pos, []).append(l)
716 717 elif skipping:
717 718 after.setdefault(pos, []).append(l)
718 719 elif l.startswith(' >>> '): # python inlines
719 720 after.setdefault(pos, []).append(l)
720 721 prepos = pos
721 722 pos = n
722 723 if not inpython:
723 724 # we've just entered a Python block, add the header
724 725 inpython = True
725 726 addsalt(prepos, False) # make sure we report the exit code
726 727 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
727 728 addsalt(n, True)
728 729 script.append(l[2:])
729 730 elif l.startswith(' ... '): # python inlines
730 731 after.setdefault(prepos, []).append(l)
731 732 script.append(l[2:])
732 733 elif l.startswith(' $ '): # commands
733 734 if inpython:
734 735 script.append("EOF\n")
735 736 inpython = False
736 737 after.setdefault(pos, []).append(l)
737 738 prepos = pos
738 739 pos = n
739 740 addsalt(n, False)
740 741 cmd = l[4:].split()
741 742 if len(cmd) == 2 and cmd[0] == 'cd':
742 743 l = ' $ cd %s || exit 1\n' % cmd[1]
743 744 script.append(l[4:])
744 745 elif l.startswith(' > '): # continuations
745 746 after.setdefault(prepos, []).append(l)
746 747 script.append(l[4:])
747 748 elif l.startswith(' '): # results
748 749 # queue up a list of expected results
749 750 expected.setdefault(pos, []).append(l[2:])
750 751 else:
751 752 if inpython:
752 753 script.append("EOF\n")
753 754 inpython = False
754 755 # non-command/result - queue up for merged output
755 756 after.setdefault(pos, []).append(l)
756 757
757 758 if inpython:
758 759 script.append("EOF\n")
759 760 if skipping is not None:
760 761 after.setdefault(pos, []).append(' !!! missing #endif\n')
761 762 addsalt(n + 1, False)
762 763
763 764 # Write out the script and execute it
764 765 fd, name = tempfile.mkstemp(suffix='hg-tst')
765 766 try:
766 767 for l in script:
767 768 os.write(fd, l)
768 769 os.close(fd)
769 770
770 771 cmd = '%s "%s"' % (options.shell, name)
771 772 vlog("# Running", cmd)
772 773 exitcode, output = run(cmd, wd, options, replacements, env)
773 774 # do not merge output if skipped, return hghave message instead
774 775 # similarly, with --debug, output is None
775 776 if exitcode == SKIPPED_STATUS or output is None:
776 777 return exitcode, output
777 778 finally:
778 779 os.remove(name)
779 780
780 781 # Merge the script output back into a unified test
781 782
782 783 pos = -1
783 784 postout = []
784 785 ret = 0
785 786 for l in output:
786 787 lout, lcmd = l, None
787 788 if salt in l:
788 789 lout, lcmd = l.split(salt, 1)
789 790
790 791 if lout:
791 792 if not lout.endswith('\n'):
792 793 lout += ' (no-eol)\n'
793 794
794 795 # find the expected output at the current position
795 796 el = None
796 797 if pos in expected and expected[pos]:
797 798 el = expected[pos].pop(0)
798 799
799 800 if linematch(el, lout):
800 801 postout.append(" " + el)
801 802 else:
802 803 if needescape(lout):
803 804 lout = stringescape(lout.rstrip('\n')) + " (esc)\n"
804 805 postout.append(" " + lout) # let diff deal with it
805 806
806 807 if lcmd:
807 808 # add on last return code
808 809 ret = int(lcmd.split()[1])
809 810 if ret != 0:
810 811 postout.append(" [%s]\n" % ret)
811 812 if pos in after:
812 813 # merge in non-active test bits
813 814 postout += after.pop(pos)
814 815 pos = int(lcmd.split()[0])
815 816
816 817 if pos in after:
817 818 postout += after.pop(pos)
818 819
819 820 return exitcode, postout
820 821
821 822 wifexited = getattr(os, "WIFEXITED", lambda x: False)
822 823 def run(cmd, wd, options, replacements, env):
823 824 """Run command in a sub-process, capturing the output (stdout and stderr).
824 825 Return a tuple (exitcode, output). output is None in debug mode."""
825 826 # TODO: Use subprocess.Popen if we're running on Python 2.4
826 827 if options.debug:
827 828 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
828 829 ret = proc.wait()
829 830 return (ret, None)
830 831
831 832 proc = Popen4(cmd, wd, options.timeout, env)
832 833 def cleanup():
833 834 terminate(proc)
834 835 ret = proc.wait()
835 836 if ret == 0:
836 837 ret = signal.SIGTERM << 8
837 838 killdaemons(env['DAEMON_PIDS'])
838 839 return ret
839 840
840 841 output = ''
841 842 proc.tochild.close()
842 843
843 844 try:
844 845 output = proc.fromchild.read()
845 846 except KeyboardInterrupt:
846 847 vlog('# Handling keyboard interrupt')
847 848 cleanup()
848 849 raise
849 850
850 851 ret = proc.wait()
851 852 if wifexited(ret):
852 853 ret = os.WEXITSTATUS(ret)
853 854
854 855 if proc.timeout:
855 856 ret = 'timeout'
856 857
857 858 if ret:
858 859 killdaemons(env['DAEMON_PIDS'])
859 860
860 861 if abort:
861 862 raise KeyboardInterrupt()
862 863
863 864 for s, r in replacements:
864 865 output = re.sub(s, r, output)
865 866 return ret, output.splitlines(True)
866 867
867 868 def runone(options, test, count):
868 869 '''returns a result element: (code, test, msg)'''
869 870
870 871 def skip(msg):
871 872 if options.verbose:
872 873 log("\nSkipping %s: %s" % (testpath, msg))
873 874 return 's', test, msg
874 875
875 876 def fail(msg, ret):
876 877 if not options.nodiff:
877 878 log("\nERROR: %s %s" % (testpath, msg))
878 879 if (not ret and options.interactive
879 880 and os.path.exists(testpath + ".err")):
880 881 iolock.acquire()
881 882 print "Accept this change? [n] ",
882 883 answer = sys.stdin.readline().strip()
883 884 iolock.release()
884 885 if answer.lower() in "y yes".split():
885 886 if test.endswith(".t"):
886 887 rename(testpath + ".err", testpath)
887 888 else:
888 889 rename(testpath + ".err", testpath + ".out")
889 890 return '.', test, ''
890 891 return '!', test, msg
891 892
892 893 def success():
893 894 return '.', test, ''
894 895
895 896 def ignore(msg):
896 897 return 'i', test, msg
897 898
898 899 def describe(ret):
899 900 if ret < 0:
900 901 return 'killed by signal %d' % -ret
901 902 return 'returned error code %d' % ret
902 903
903 904 testpath = os.path.join(TESTDIR, test)
904 905 err = os.path.join(TESTDIR, test + ".err")
905 906 lctest = test.lower()
906 907
907 908 if not os.path.exists(testpath):
908 909 return skip("doesn't exist")
909 910
910 911 if not (options.whitelisted and test in options.whitelisted):
911 912 if options.blacklist and test in options.blacklist:
912 913 return skip("blacklisted")
913 914
914 915 if options.retest and not os.path.exists(test + ".err"):
915 916 return ignore("not retesting")
916 917
917 918 if options.keywords:
918 919 fp = open(test)
919 920 t = fp.read().lower() + test.lower()
920 921 fp.close()
921 922 for k in options.keywords.lower().split():
922 923 if k in t:
923 924 break
924 925 else:
925 926 return ignore("doesn't match keyword")
926 927
927 928 if not lctest.startswith("test-"):
928 929 return skip("not a test file")
929 930 for ext, func, out in testtypes:
930 931 if lctest.endswith(ext):
931 932 runner = func
932 933 ref = os.path.join(TESTDIR, test + out)
933 934 break
934 935 else:
935 936 return skip("unknown test type")
936 937
937 938 vlog("# Test", test)
938 939
939 940 if os.path.exists(err):
940 941 os.remove(err) # Remove any previous output files
941 942
942 943 # Make a tmp subdirectory to work in
943 944 threadtmp = os.path.join(HGTMP, "child%d" % count)
944 945 testtmp = os.path.join(threadtmp, os.path.basename(test))
945 946 os.mkdir(threadtmp)
946 947 os.mkdir(testtmp)
947 948
948 949 port = options.port + count * 3
949 950 replacements = [
950 951 (r':%s\b' % port, ':$HGPORT'),
951 952 (r':%s\b' % (port + 1), ':$HGPORT1'),
952 953 (r':%s\b' % (port + 2), ':$HGPORT2'),
953 954 ]
954 955 if os.name == 'nt':
955 956 replacements.append(
956 957 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
957 958 c in '/\\' and r'[/\\]' or
958 959 c.isdigit() and c or
959 960 '\\' + c
960 961 for c in testtmp), '$TESTTMP'))
961 962 else:
962 963 replacements.append((re.escape(testtmp), '$TESTTMP'))
963 964
964 965 env = createenv(options, testtmp, threadtmp, port)
965 966 createhgrc(env['HGRCPATH'], options)
966 967
967 968 starttime = time.time()
968 969 try:
969 970 ret, out = runner(testpath, testtmp, options, replacements, env)
970 971 except KeyboardInterrupt:
971 972 endtime = time.time()
972 973 log('INTERRUPTED: %s (after %d seconds)' % (test, endtime - starttime))
973 974 raise
974 975 endtime = time.time()
975 976 times.append((test, endtime - starttime))
976 977 vlog("# Ret was:", ret)
977 978
978 979 killdaemons(env['DAEMON_PIDS'])
979 980
980 981 skipped = (ret == SKIPPED_STATUS)
981 982
982 983 # If we're not in --debug mode and reference output file exists,
983 984 # check test output against it.
984 985 if options.debug:
985 986 refout = None # to match "out is None"
986 987 elif os.path.exists(ref):
987 988 f = open(ref, "r")
988 989 refout = f.read().splitlines(True)
989 990 f.close()
990 991 else:
991 992 refout = []
992 993
993 994 if (ret != 0 or out != refout) and not skipped and not options.debug:
994 995 # Save errors to a file for diagnosis
995 996 f = open(err, "wb")
996 997 for line in out:
997 998 f.write(line)
998 999 f.close()
999 1000
1000 1001 if skipped:
1001 1002 if out is None: # debug mode: nothing to parse
1002 1003 missing = ['unknown']
1003 1004 failed = None
1004 1005 else:
1005 1006 missing, failed = parsehghaveoutput(out)
1006 1007 if not missing:
1007 1008 missing = ['irrelevant']
1008 1009 if failed:
1009 1010 result = fail("hghave failed checking for %s" % failed[-1], ret)
1010 1011 skipped = False
1011 1012 else:
1012 1013 result = skip(missing[-1])
1013 1014 elif ret == 'timeout':
1014 1015 result = fail("timed out", ret)
1015 1016 elif out != refout:
1016 1017 if not options.nodiff:
1017 1018 iolock.acquire()
1018 1019 if options.view:
1019 1020 os.system("%s %s %s" % (options.view, ref, err))
1020 1021 else:
1021 1022 showdiff(refout, out, ref, err)
1022 1023 iolock.release()
1023 1024 if ret:
1024 1025 result = fail("output changed and " + describe(ret), ret)
1025 1026 else:
1026 1027 result = fail("output changed", ret)
1027 1028 elif ret:
1028 1029 result = fail(describe(ret), ret)
1029 1030 else:
1030 1031 result = success()
1031 1032
1032 1033 if not options.verbose:
1033 1034 iolock.acquire()
1034 1035 sys.stdout.write(result[0])
1035 1036 sys.stdout.flush()
1036 1037 iolock.release()
1037 1038
1038 1039 if not options.keep_tmpdir:
1039 1040 shutil.rmtree(threadtmp, True)
1040 1041 return result
1041 1042
1042 1043 _hgpath = None
1043 1044
1044 1045 def _gethgpath():
1045 1046 """Return the path to the mercurial package that is actually found by
1046 1047 the current Python interpreter."""
1047 1048 global _hgpath
1048 1049 if _hgpath is not None:
1049 1050 return _hgpath
1050 1051
1051 1052 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1052 1053 pipe = os.popen(cmd % PYTHON)
1053 1054 try:
1054 1055 _hgpath = pipe.read().strip()
1055 1056 finally:
1056 1057 pipe.close()
1057 1058 return _hgpath
1058 1059
1059 1060 def _checkhglib(verb):
1060 1061 """Ensure that the 'mercurial' package imported by python is
1061 1062 the one we expect it to be. If not, print a warning to stderr."""
1062 1063 expecthg = os.path.join(PYTHONDIR, 'mercurial')
1063 1064 actualhg = _gethgpath()
1064 1065 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1065 1066 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1066 1067 ' (expected %s)\n'
1067 1068 % (verb, actualhg, expecthg))
1068 1069
1069 1070 results = {'.':[], '!':[], 's':[], 'i':[]}
1070 1071 times = []
1071 1072 iolock = threading.Lock()
1072 1073 abort = False
1073 1074
1074 1075 def scheduletests(options, tests):
1075 1076 jobs = options.jobs
1076 1077 done = queue.Queue()
1077 1078 running = 0
1078 1079 count = 0
1079 1080 global abort
1080 1081
1081 1082 def job(test, count):
1082 1083 try:
1083 1084 done.put(runone(options, test, count))
1084 1085 except KeyboardInterrupt:
1085 1086 pass
1086 1087
1087 1088 try:
1088 1089 while tests or running:
1089 1090 if not done.empty() or running == jobs or not tests:
1090 1091 try:
1091 1092 code, test, msg = done.get(True, 1)
1092 1093 results[code].append((test, msg))
1093 1094 if options.first and code not in '.si':
1094 1095 break
1095 1096 except queue.Empty:
1096 1097 continue
1097 1098 running -= 1
1098 1099 if tests and not running == jobs:
1099 1100 test = tests.pop(0)
1100 1101 if options.loop:
1101 1102 tests.append(test)
1102 1103 t = threading.Thread(target=job, args=(test, count))
1103 1104 t.start()
1104 1105 running += 1
1105 1106 count += 1
1106 1107 except KeyboardInterrupt:
1107 1108 abort = True
1108 1109
1109 1110 def runtests(options, tests):
1110 1111 try:
1111 1112 if INST:
1112 1113 installhg(options)
1113 1114 _checkhglib("Testing")
1114 1115 else:
1115 1116 usecorrectpython()
1116 1117
1117 1118 if options.restart:
1118 1119 orig = list(tests)
1119 1120 while tests:
1120 1121 if os.path.exists(tests[0] + ".err"):
1121 1122 break
1122 1123 tests.pop(0)
1123 1124 if not tests:
1124 1125 print "running all tests"
1125 1126 tests = orig
1126 1127
1127 1128 scheduletests(options, tests)
1128 1129
1129 1130 failed = len(results['!'])
1130 1131 tested = len(results['.']) + failed
1131 1132 skipped = len(results['s'])
1132 1133 ignored = len(results['i'])
1133 1134
1134 1135 print
1135 1136 if not options.noskips:
1136 1137 for s in results['s']:
1137 1138 print "Skipped %s: %s" % s
1138 1139 for s in results['!']:
1139 1140 print "Failed %s: %s" % s
1140 1141 _checkhglib("Tested")
1141 1142 print "# Ran %d tests, %d skipped, %d failed." % (
1142 1143 tested, skipped + ignored, failed)
1143 1144 if options.time:
1144 1145 outputtimes(options)
1145 1146
1146 1147 if options.anycoverage:
1147 1148 outputcoverage(options)
1148 1149 except KeyboardInterrupt:
1149 1150 failed = True
1150 1151 print "\ninterrupted!"
1151 1152
1152 1153 if failed:
1153 1154 sys.exit(1)
1154 1155
1155 1156 testtypes = [('.py', pytest, '.out'),
1156 1157 ('.t', tsttest, '')]
1157 1158
1158 1159 def main():
1159 1160 (options, args) = parseargs()
1160 1161 os.umask(022)
1161 1162
1162 1163 checktools()
1163 1164
1164 1165 if len(args) == 0:
1165 1166 args = [t for t in os.listdir(".")
1166 1167 if t.startswith("test-")
1167 1168 and (t.endswith(".py") or t.endswith(".t"))]
1168 1169
1169 1170 tests = args
1170 1171
1171 1172 if options.random:
1172 1173 random.shuffle(tests)
1173 1174 else:
1174 1175 # keywords for slow tests
1175 1176 slow = 'svn gendoc check-code-hg'.split()
1176 1177 def sortkey(f):
1177 1178 # run largest tests first, as they tend to take the longest
1178 1179 try:
1179 1180 val = -os.stat(f).st_size
1180 1181 except OSError, e:
1181 1182 if e.errno != errno.ENOENT:
1182 1183 raise
1183 1184 return -1e9 # file does not exist, tell early
1184 1185 for kw in slow:
1185 1186 if kw in f:
1186 1187 val *= 10
1187 1188 return val
1188 1189 tests.sort(key=sortkey)
1189 1190
1190 1191 if 'PYTHONHASHSEED' not in os.environ:
1191 1192 # use a random python hash seed all the time
1192 1193 # we do the randomness ourself to know what seed is used
1193 1194 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1194 1195 print 'python hash seed:', os.environ['PYTHONHASHSEED']
1195 1196
1196 1197 global TESTDIR, HGTMP, INST, BINDIR, PYTHONDIR, COVERAGE_FILE
1197 1198 TESTDIR = os.environ["TESTDIR"] = os.getcwd()
1198 1199 if options.tmpdir:
1199 1200 options.keep_tmpdir = True
1200 1201 tmpdir = options.tmpdir
1201 1202 if os.path.exists(tmpdir):
1202 1203 # Meaning of tmpdir has changed since 1.3: we used to create
1203 1204 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1204 1205 # tmpdir already exists.
1205 1206 sys.exit("error: temp dir %r already exists" % tmpdir)
1206 1207
1207 1208 # Automatically removing tmpdir sounds convenient, but could
1208 1209 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1209 1210 # or "--tmpdir=$HOME".
1210 1211 #vlog("# Removing temp dir", tmpdir)
1211 1212 #shutil.rmtree(tmpdir)
1212 1213 os.makedirs(tmpdir)
1213 1214 else:
1214 1215 d = None
1215 1216 if os.name == 'nt':
1216 1217 # without this, we get the default temp dir location, but
1217 1218 # in all lowercase, which causes troubles with paths (issue3490)
1218 1219 d = os.getenv('TMP')
1219 1220 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1220 1221 HGTMP = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1221 1222
1222 1223 if options.with_hg:
1223 1224 INST = None
1224 1225 BINDIR = os.path.dirname(os.path.realpath(options.with_hg))
1225 1226
1226 1227 # This looks redundant with how Python initializes sys.path from
1227 1228 # the location of the script being executed. Needed because the
1228 1229 # "hg" specified by --with-hg is not the only Python script
1229 1230 # executed in the test suite that needs to import 'mercurial'
1230 1231 # ... which means it's not really redundant at all.
1231 1232 PYTHONDIR = BINDIR
1232 1233 else:
1233 1234 INST = os.path.join(HGTMP, "install")
1234 1235 BINDIR = os.environ["BINDIR"] = os.path.join(INST, "bin")
1235 1236 PYTHONDIR = os.path.join(INST, "lib", "python")
1236 1237
1237 1238 os.environ["BINDIR"] = BINDIR
1238 1239 os.environ["PYTHON"] = PYTHON
1239 1240
1240 1241 path = [BINDIR] + os.environ["PATH"].split(os.pathsep)
1241 1242 os.environ["PATH"] = os.pathsep.join(path)
1242 1243
1243 1244 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1244 1245 # can run .../tests/run-tests.py test-foo where test-foo
1245 1246 # adds an extension to HGRC
1246 1247 pypath = [PYTHONDIR, TESTDIR]
1247 1248 # We have to augment PYTHONPATH, rather than simply replacing
1248 1249 # it, in case external libraries are only available via current
1249 1250 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1250 1251 # are in /opt/subversion.)
1251 1252 oldpypath = os.environ.get(IMPL_PATH)
1252 1253 if oldpypath:
1253 1254 pypath.append(oldpypath)
1254 1255 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1255 1256
1256 1257 COVERAGE_FILE = os.path.join(TESTDIR, ".coverage")
1257 1258
1258 1259 vlog("# Using TESTDIR", TESTDIR)
1259 1260 vlog("# Using HGTMP", HGTMP)
1260 1261 vlog("# Using PATH", os.environ["PATH"])
1261 1262 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1262 1263
1263 1264 try:
1264 1265 runtests(options, tests)
1265 1266 finally:
1266 1267 time.sleep(.1)
1267 1268 cleanup(options)
1268 1269
1269 1270 if __name__ == '__main__':
1270 1271 main()
@@ -1,191 +1,193
1 1
2 2 testing hellomessage:
3 3
4 4 o, 'capabilities: getencoding runcommand\nencoding: ***'
5 5 runcommand id
6 6 000000000000 tip
7 7
8 8 testing unknowncommand:
9 9
10 10 abort: unknown command unknowncommand
11 11
12 12 testing checkruncommand:
13 13
14 14 runcommand
15 15 Mercurial Distributed SCM
16 16
17 17 basic commands:
18 18
19 19 add add the specified files on the next commit
20 20 annotate show changeset information by line for each file
21 21 clone make a copy of an existing repository
22 22 commit commit the specified files or all outstanding changes
23 23 diff diff repository (or selected files)
24 24 export dump the header and diffs for one or more changesets
25 25 forget forget the specified files on the next commit
26 26 init create a new repository in the given directory
27 27 log show revision history of entire repository or files
28 28 merge merge working directory with another revision
29 29 pull pull changes from the specified source
30 30 push push changes to the specified destination
31 31 remove remove the specified files on the next commit
32 32 serve start stand-alone webserver
33 33 status show changed files in the working directory
34 34 summary summarize working directory state
35 35 update update working directory (or switch revisions)
36 36
37 37 use "hg help" for the full list of commands or "hg -v" for details
38 38 runcommand id --quiet
39 39 000000000000
40 40 runcommand id
41 41 000000000000 tip
42 42 runcommand id --config ui.quiet=True
43 43 000000000000
44 44 runcommand id
45 45 000000000000 tip
46 46
47 47 testing inputeof:
48 48
49 49 server exit code = 1
50 50
51 51 testing serverinput:
52 52
53 53 runcommand import -
54 54 applying patch from stdin
55 55 runcommand log
56 56 changeset: 0:eff892de26ec
57 57 tag: tip
58 58 user: test
59 59 date: Thu Jan 01 00:00:00 1970 +0000
60 60 summary: 1
61 61
62 62
63 63 testing cwd:
64 64
65 65 runcommand --cwd foo st bar
66 66 ? bar
67 67 runcommand st foo/bar
68 68 ? foo/bar
69 69
70 70 testing localhgrc:
71 71
72 72 runcommand showconfig
73 73 bundle.mainreporoot=$TESTTMP
74 74 defaults.backout=-d "0 0"
75 75 defaults.commit=-d "0 0"
76 defaults.shelve=--date "0 0"
76 77 defaults.tag=-d "0 0"
77 78 ui.slash=True
78 79 ui.interactive=False
79 80 ui.foo=bar
80 81 runcommand init foo
81 82 runcommand -R foo showconfig ui defaults
82 83 defaults.backout=-d "0 0"
83 84 defaults.commit=-d "0 0"
85 defaults.shelve=--date "0 0"
84 86 defaults.tag=-d "0 0"
85 87 ui.slash=True
86 88 ui.interactive=False
87 89
88 90 testing hookoutput:
89 91
90 92 runcommand --config hooks.pre-identify=python:test-commandserver.hook id
91 93 hook talking
92 94 now try to read something: 'some input'
93 95 eff892de26ec tip
94 96
95 97 testing outsidechanges:
96 98
97 99 runcommand status
98 100 M a
99 101 runcommand tip
100 102 changeset: 1:d3a0a68be6de
101 103 tag: tip
102 104 user: test
103 105 date: Thu Jan 01 00:00:00 1970 +0000
104 106 summary: 2
105 107
106 108 runcommand status
107 109
108 110 testing bookmarks:
109 111
110 112 runcommand bookmarks
111 113 no bookmarks set
112 114 runcommand bookmarks
113 115 bm1 1:d3a0a68be6de
114 116 bm2 1:d3a0a68be6de
115 117 runcommand bookmarks
116 118 * bm1 1:d3a0a68be6de
117 119 bm2 1:d3a0a68be6de
118 120 runcommand bookmarks bm3
119 121 runcommand commit -Amm
120 122 runcommand bookmarks
121 123 bm1 1:d3a0a68be6de
122 124 bm2 1:d3a0a68be6de
123 125 * bm3 2:aef17e88f5f0
124 126
125 127 testing tagscache:
126 128
127 129 runcommand id -t -r 0
128 130
129 131 runcommand id -t -r 0
130 132 foo
131 133
132 134 testing setphase:
133 135
134 136 runcommand phase -r .
135 137 3: draft
136 138 runcommand phase -r .
137 139 3: public
138 140
139 141 testing rollback:
140 142
141 143 runcommand phase -r . -p
142 144 no phases changed
143 145 runcommand commit -Am.
144 146 runcommand rollback
145 147 repository tip rolled back to revision 3 (undo commit)
146 148 working directory now based on revision 3
147 149 runcommand phase -r .
148 150 3: public
149 151
150 152 testing branch:
151 153
152 154 runcommand branch
153 155 default
154 156 marked working directory as branch foo
155 157 (branches are permanent and global, did you want a bookmark?)
156 158 runcommand branch
157 159 foo
158 160 marked working directory as branch default
159 161 (branches are permanent and global, did you want a bookmark?)
160 162
161 163 testing hgignore:
162 164
163 165 runcommand commit -Am.
164 166 adding .hgignore
165 167 runcommand status -i -u
166 168 I ignored-file
167 169
168 170 testing phasecacheafterstrip:
169 171
170 172 runcommand update -C 0
171 173 1 files updated, 0 files merged, 2 files removed, 0 files unresolved
172 174 runcommand commit -Am. a
173 175 created new head
174 176 runcommand log -Gq
175 177 @ 5:731265503d86
176 178 |
177 179 | o 4:7966c8e3734d
178 180 | |
179 181 | o 3:b9b85890c400
180 182 | |
181 183 | o 2:aef17e88f5f0
182 184 | |
183 185 | o 1:d3a0a68be6de
184 186 |/
185 187 o 0:eff892de26ec
186 188
187 189 runcommand phase -p .
188 190 runcommand phase .
189 191 5: public
190 192 runcommand branches
191 193 default 1:731265503d86
General Comments 0
You need to be logged in to leave comments. Login now