##// END OF EJS Templates
merge with stable
Matt Mackall -
r22165:3ddfb9b3 merge default
parent child Browse files
Show More
@@ -1,950 +1,945 b''
1 # histedit.py - interactive history editing for mercurial
1 # histedit.py - interactive history editing for mercurial
2 #
2 #
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
3 # Copyright 2009 Augie Fackler <raf@durin42.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
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.
6 # GNU General Public License version 2 or any later version.
7 """interactive history editing
7 """interactive history editing
8
8
9 With this extension installed, Mercurial gains one new command: histedit. Usage
9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 is as follows, assuming the following history::
10 is as follows, assuming the following history::
11
11
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 | Add delta
13 | Add delta
14 |
14 |
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 | Add gamma
16 | Add gamma
17 |
17 |
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 | Add beta
19 | Add beta
20 |
20 |
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 Add alpha
22 Add alpha
23
23
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 file open in your editor::
25 file open in your editor::
26
26
27 pick c561b4e977df Add beta
27 pick c561b4e977df Add beta
28 pick 030b686bedc4 Add gamma
28 pick 030b686bedc4 Add gamma
29 pick 7c2fd3b9020c Add delta
29 pick 7c2fd3b9020c Add delta
30
30
31 # Edit history between c561b4e977df and 7c2fd3b9020c
31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 #
32 #
33 # Commits are listed from least to most recent
33 # Commits are listed from least to most recent
34 #
34 #
35 # Commands:
35 # Commands:
36 # p, pick = use commit
36 # p, pick = use commit
37 # e, edit = use commit, but stop for amending
37 # e, edit = use commit, but stop for amending
38 # f, fold = use commit, but combine it with the one above
38 # f, fold = use commit, but combine it with the one above
39 # r, roll = like fold, but discard this commit's description
39 # r, roll = like fold, but discard this commit's description
40 # d, drop = remove commit from history
40 # d, drop = remove commit from history
41 # m, mess = edit message without changing commit content
41 # m, mess = edit message without changing commit content
42 #
42 #
43
43
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
44 In this file, lines beginning with ``#`` are ignored. You must specify a rule
45 for each revision in your history. For example, if you had meant to add gamma
45 for each revision in your history. For example, if you had meant to add gamma
46 before beta, and then wanted to add delta in the same revision as beta, you
46 before beta, and then wanted to add delta in the same revision as beta, you
47 would reorganize the file to look like this::
47 would reorganize the file to look like this::
48
48
49 pick 030b686bedc4 Add gamma
49 pick 030b686bedc4 Add gamma
50 pick c561b4e977df Add beta
50 pick c561b4e977df Add beta
51 fold 7c2fd3b9020c Add delta
51 fold 7c2fd3b9020c Add delta
52
52
53 # Edit history between c561b4e977df and 7c2fd3b9020c
53 # Edit history between c561b4e977df and 7c2fd3b9020c
54 #
54 #
55 # Commits are listed from least to most recent
55 # Commits are listed from least to most recent
56 #
56 #
57 # Commands:
57 # Commands:
58 # p, pick = use commit
58 # p, pick = use commit
59 # e, edit = use commit, but stop for amending
59 # e, edit = use commit, but stop for amending
60 # f, fold = use commit, but combine it with the one above
60 # f, fold = use commit, but combine it with the one above
61 # r, roll = like fold, but discard this commit's description
61 # r, roll = like fold, but discard this commit's description
62 # d, drop = remove commit from history
62 # d, drop = remove commit from history
63 # m, mess = edit message without changing commit content
63 # m, mess = edit message without changing commit content
64 #
64 #
65
65
66 At which point you close the editor and ``histedit`` starts working. When you
66 At which point you close the editor and ``histedit`` starts working. When you
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
67 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
68 those revisions together, offering you a chance to clean up the commit message::
68 those revisions together, offering you a chance to clean up the commit message::
69
69
70 Add beta
70 Add beta
71 ***
71 ***
72 Add delta
72 Add delta
73
73
74 Edit the commit message to your liking, then close the editor. For
74 Edit the commit message to your liking, then close the editor. For
75 this example, let's assume that the commit message was changed to
75 this example, let's assume that the commit message was changed to
76 ``Add beta and delta.`` After histedit has run and had a chance to
76 ``Add beta and delta.`` After histedit has run and had a chance to
77 remove any old or temporary revisions it needed, the history looks
77 remove any old or temporary revisions it needed, the history looks
78 like this::
78 like this::
79
79
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
80 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
81 | Add beta and delta.
81 | Add beta and delta.
82 |
82 |
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
83 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
84 | Add gamma
84 | Add gamma
85 |
85 |
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
86 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
87 Add alpha
87 Add alpha
88
88
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
89 Note that ``histedit`` does *not* remove any revisions (even its own temporary
90 ones) until after it has completed all the editing operations, so it will
90 ones) until after it has completed all the editing operations, so it will
91 probably perform several strip operations when it's done. For the above example,
91 probably perform several strip operations when it's done. For the above example,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
92 it had to run strip twice. Strip can be slow depending on a variety of factors,
93 so you might need to be a little patient. You can choose to keep the original
93 so you might need to be a little patient. You can choose to keep the original
94 revisions by passing the ``--keep`` flag.
94 revisions by passing the ``--keep`` flag.
95
95
96 The ``edit`` operation will drop you back to a command prompt,
96 The ``edit`` operation will drop you back to a command prompt,
97 allowing you to edit files freely, or even use ``hg record`` to commit
97 allowing you to edit files freely, or even use ``hg record`` to commit
98 some changes as a separate commit. When you're done, any remaining
98 some changes as a separate commit. When you're done, any remaining
99 uncommitted changes will be committed as well. When done, run ``hg
99 uncommitted changes will be committed as well. When done, run ``hg
100 histedit --continue`` to finish this step. You'll be prompted for a
100 histedit --continue`` to finish this step. You'll be prompted for a
101 new commit message, but the default commit message will be the
101 new commit message, but the default commit message will be the
102 original message for the ``edit`` ed revision.
102 original message for the ``edit`` ed revision.
103
103
104 The ``message`` operation will give you a chance to revise a commit
104 The ``message`` operation will give you a chance to revise a commit
105 message without changing the contents. It's a shortcut for doing
105 message without changing the contents. It's a shortcut for doing
106 ``edit`` immediately followed by `hg histedit --continue``.
106 ``edit`` immediately followed by `hg histedit --continue``.
107
107
108 If ``histedit`` encounters a conflict when moving a revision (while
108 If ``histedit`` encounters a conflict when moving a revision (while
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
109 handling ``pick`` or ``fold``), it'll stop in a similar manner to
110 ``edit`` with the difference that it won't prompt you for a commit
110 ``edit`` with the difference that it won't prompt you for a commit
111 message when done. If you decide at this point that you don't like how
111 message when done. If you decide at this point that you don't like how
112 much work it will be to rearrange history, or that you made a mistake,
112 much work it will be to rearrange history, or that you made a mistake,
113 you can use ``hg histedit --abort`` to abandon the new changes you
113 you can use ``hg histedit --abort`` to abandon the new changes you
114 have made and return to the state before you attempted to edit your
114 have made and return to the state before you attempted to edit your
115 history.
115 history.
116
116
117 If we clone the histedit-ed example repository above and add four more
117 If we clone the histedit-ed example repository above and add four more
118 changes, such that we have the following history::
118 changes, such that we have the following history::
119
119
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
120 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
121 | Add theta
121 | Add theta
122 |
122 |
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
123 o 5 140988835471 2009-04-27 18:04 -0500 stefan
124 | Add eta
124 | Add eta
125 |
125 |
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
126 o 4 122930637314 2009-04-27 18:04 -0500 stefan
127 | Add zeta
127 | Add zeta
128 |
128 |
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
129 o 3 836302820282 2009-04-27 18:04 -0500 stefan
130 | Add epsilon
130 | Add epsilon
131 |
131 |
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
132 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
133 | Add beta and delta.
133 | Add beta and delta.
134 |
134 |
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
135 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
136 | Add gamma
136 | Add gamma
137 |
137 |
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
138 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
139 Add alpha
139 Add alpha
140
140
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
141 If you run ``hg histedit --outgoing`` on the clone then it is the same
142 as running ``hg histedit 836302820282``. If you need plan to push to a
142 as running ``hg histedit 836302820282``. If you need plan to push to a
143 repository that Mercurial does not detect to be related to the source
143 repository that Mercurial does not detect to be related to the source
144 repo, you can add a ``--force`` option.
144 repo, you can add a ``--force`` option.
145 """
145 """
146
146
147 try:
147 try:
148 import cPickle as pickle
148 import cPickle as pickle
149 pickle.dump # import now
149 pickle.dump # import now
150 except ImportError:
150 except ImportError:
151 import pickle
151 import pickle
152 import os
152 import os
153 import sys
153 import sys
154
154
155 from mercurial import cmdutil
155 from mercurial import cmdutil
156 from mercurial import discovery
156 from mercurial import discovery
157 from mercurial import error
157 from mercurial import error
158 from mercurial import copies
158 from mercurial import copies
159 from mercurial import context
159 from mercurial import context
160 from mercurial import hg
160 from mercurial import hg
161 from mercurial import node
161 from mercurial import node
162 from mercurial import repair
162 from mercurial import repair
163 from mercurial import scmutil
163 from mercurial import scmutil
164 from mercurial import util
164 from mercurial import util
165 from mercurial import obsolete
165 from mercurial import obsolete
166 from mercurial import merge as mergemod
166 from mercurial import merge as mergemod
167 from mercurial.lock import release
167 from mercurial.lock import release
168 from mercurial.i18n import _
168 from mercurial.i18n import _
169
169
170 cmdtable = {}
170 cmdtable = {}
171 command = cmdutil.command(cmdtable)
171 command = cmdutil.command(cmdtable)
172
172
173 testedwith = 'internal'
173 testedwith = 'internal'
174
174
175 # i18n: command names and abbreviations must remain untranslated
175 # i18n: command names and abbreviations must remain untranslated
176 editcomment = _("""# Edit history between %s and %s
176 editcomment = _("""# Edit history between %s and %s
177 #
177 #
178 # Commits are listed from least to most recent
178 # Commits are listed from least to most recent
179 #
179 #
180 # Commands:
180 # Commands:
181 # p, pick = use commit
181 # p, pick = use commit
182 # e, edit = use commit, but stop for amending
182 # e, edit = use commit, but stop for amending
183 # f, fold = use commit, but combine it with the one above
183 # f, fold = use commit, but combine it with the one above
184 # r, roll = like fold, but discard this commit's description
184 # r, roll = like fold, but discard this commit's description
185 # d, drop = remove commit from history
185 # d, drop = remove commit from history
186 # m, mess = edit message without changing commit content
186 # m, mess = edit message without changing commit content
187 #
187 #
188 """)
188 """)
189
189
190 def commitfuncfor(repo, src):
190 def commitfuncfor(repo, src):
191 """Build a commit function for the replacement of <src>
191 """Build a commit function for the replacement of <src>
192
192
193 This function ensure we apply the same treatment to all changesets.
193 This function ensure we apply the same treatment to all changesets.
194
194
195 - Add a 'histedit_source' entry in extra.
195 - Add a 'histedit_source' entry in extra.
196
196
197 Note that fold have its own separated logic because its handling is a bit
197 Note that fold have its own separated logic because its handling is a bit
198 different and not easily factored out of the fold method.
198 different and not easily factored out of the fold method.
199 """
199 """
200 phasemin = src.phase()
200 phasemin = src.phase()
201 def commitfunc(**kwargs):
201 def commitfunc(**kwargs):
202 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
202 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
203 try:
203 try:
204 repo.ui.setconfig('phases', 'new-commit', phasemin,
204 repo.ui.setconfig('phases', 'new-commit', phasemin,
205 'histedit')
205 'histedit')
206 extra = kwargs.get('extra', {}).copy()
206 extra = kwargs.get('extra', {}).copy()
207 extra['histedit_source'] = src.hex()
207 extra['histedit_source'] = src.hex()
208 kwargs['extra'] = extra
208 kwargs['extra'] = extra
209 return repo.commit(**kwargs)
209 return repo.commit(**kwargs)
210 finally:
210 finally:
211 repo.ui.restoreconfig(phasebackup)
211 repo.ui.restoreconfig(phasebackup)
212 return commitfunc
212 return commitfunc
213
213
214 def applychanges(ui, repo, ctx, opts):
214 def applychanges(ui, repo, ctx, opts):
215 """Merge changeset from ctx (only) in the current working directory"""
215 """Merge changeset from ctx (only) in the current working directory"""
216 wcpar = repo.dirstate.parents()[0]
216 wcpar = repo.dirstate.parents()[0]
217 if ctx.p1().node() == wcpar:
217 if ctx.p1().node() == wcpar:
218 # edition ar "in place" we do not need to make any merge,
218 # edition ar "in place" we do not need to make any merge,
219 # just applies changes on parent for edition
219 # just applies changes on parent for edition
220 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
220 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
221 stats = None
221 stats = None
222 else:
222 else:
223 try:
223 try:
224 # ui.forcemerge is an internal variable, do not document
224 # ui.forcemerge is an internal variable, do not document
225 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
225 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
226 'histedit')
226 'histedit')
227 stats = mergemod.update(repo, ctx.node(), True, True, False,
227 stats = mergemod.update(repo, ctx.node(), True, True, False,
228 ctx.p1().node())
228 ctx.p1().node())
229 finally:
229 finally:
230 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
230 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
231 repo.setparents(wcpar, node.nullid)
231 repo.setparents(wcpar, node.nullid)
232 repo.dirstate.write()
232 repo.dirstate.write()
233 # fix up dirstate for copies and renames
233 # fix up dirstate for copies and renames
234 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
234 cmdutil.duplicatecopies(repo, ctx.rev(), ctx.p1().rev())
235 return stats
235 return stats
236
236
237 def collapse(repo, first, last, commitopts):
237 def collapse(repo, first, last, commitopts):
238 """collapse the set of revisions from first to last as new one.
238 """collapse the set of revisions from first to last as new one.
239
239
240 Expected commit options are:
240 Expected commit options are:
241 - message
241 - message
242 - date
242 - date
243 - username
243 - username
244 Commit message is edited in all cases.
244 Commit message is edited in all cases.
245
245
246 This function works in memory."""
246 This function works in memory."""
247 ctxs = list(repo.set('%d::%d', first, last))
247 ctxs = list(repo.set('%d::%d', first, last))
248 if not ctxs:
248 if not ctxs:
249 return None
249 return None
250 base = first.parents()[0]
250 base = first.parents()[0]
251
251
252 # commit a new version of the old changeset, including the update
252 # commit a new version of the old changeset, including the update
253 # collect all files which might be affected
253 # collect all files which might be affected
254 files = set()
254 files = set()
255 for ctx in ctxs:
255 for ctx in ctxs:
256 files.update(ctx.files())
256 files.update(ctx.files())
257
257
258 # Recompute copies (avoid recording a -> b -> a)
258 # Recompute copies (avoid recording a -> b -> a)
259 copied = copies.pathcopies(base, last)
259 copied = copies.pathcopies(base, last)
260
260
261 # prune files which were reverted by the updates
261 # prune files which were reverted by the updates
262 def samefile(f):
262 def samefile(f):
263 if f in last.manifest():
263 if f in last.manifest():
264 a = last.filectx(f)
264 a = last.filectx(f)
265 if f in base.manifest():
265 if f in base.manifest():
266 b = base.filectx(f)
266 b = base.filectx(f)
267 return (a.data() == b.data()
267 return (a.data() == b.data()
268 and a.flags() == b.flags())
268 and a.flags() == b.flags())
269 else:
269 else:
270 return False
270 return False
271 else:
271 else:
272 return f not in base.manifest()
272 return f not in base.manifest()
273 files = [f for f in files if not samefile(f)]
273 files = [f for f in files if not samefile(f)]
274 # commit version of these files as defined by head
274 # commit version of these files as defined by head
275 headmf = last.manifest()
275 headmf = last.manifest()
276 def filectxfn(repo, ctx, path):
276 def filectxfn(repo, ctx, path):
277 if path in headmf:
277 if path in headmf:
278 fctx = last[path]
278 fctx = last[path]
279 flags = fctx.flags()
279 flags = fctx.flags()
280 mctx = context.memfilectx(repo,
280 mctx = context.memfilectx(repo,
281 fctx.path(), fctx.data(),
281 fctx.path(), fctx.data(),
282 islink='l' in flags,
282 islink='l' in flags,
283 isexec='x' in flags,
283 isexec='x' in flags,
284 copied=copied.get(path))
284 copied=copied.get(path))
285 return mctx
285 return mctx
286 raise IOError()
286 raise IOError()
287
287
288 if commitopts.get('message'):
288 if commitopts.get('message'):
289 message = commitopts['message']
289 message = commitopts['message']
290 else:
290 else:
291 message = first.description()
291 message = first.description()
292 user = commitopts.get('user')
292 user = commitopts.get('user')
293 date = commitopts.get('date')
293 date = commitopts.get('date')
294 extra = commitopts.get('extra')
294 extra = commitopts.get('extra')
295
295
296 parents = (first.p1().node(), first.p2().node())
296 parents = (first.p1().node(), first.p2().node())
297 editor = None
297 editor = None
298 if not commitopts.get('rollup'):
298 if not commitopts.get('rollup'):
299 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
299 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
300 new = context.memctx(repo,
300 new = context.memctx(repo,
301 parents=parents,
301 parents=parents,
302 text=message,
302 text=message,
303 files=files,
303 files=files,
304 filectxfn=filectxfn,
304 filectxfn=filectxfn,
305 user=user,
305 user=user,
306 date=date,
306 date=date,
307 extra=extra,
307 extra=extra,
308 editor=editor)
308 editor=editor)
309 return repo.commitctx(new)
309 return repo.commitctx(new)
310
310
311 def pick(ui, repo, ctx, ha, opts):
311 def pick(ui, repo, ctx, ha, opts):
312 oldctx = repo[ha]
312 oldctx = repo[ha]
313 if oldctx.parents()[0] == ctx:
313 if oldctx.parents()[0] == ctx:
314 ui.debug('node %s unchanged\n' % ha)
314 ui.debug('node %s unchanged\n' % ha)
315 return oldctx, []
315 return oldctx, []
316 hg.update(repo, ctx.node())
316 hg.update(repo, ctx.node())
317 stats = applychanges(ui, repo, oldctx, opts)
317 stats = applychanges(ui, repo, oldctx, opts)
318 if stats and stats[3] > 0:
318 if stats and stats[3] > 0:
319 raise error.InterventionRequired(_('Fix up the change and run '
319 raise error.InterventionRequired(_('Fix up the change and run '
320 'hg histedit --continue'))
320 'hg histedit --continue'))
321 # drop the second merge parent
321 # drop the second merge parent
322 commit = commitfuncfor(repo, oldctx)
322 commit = commitfuncfor(repo, oldctx)
323 n = commit(text=oldctx.description(), user=oldctx.user(),
323 n = commit(text=oldctx.description(), user=oldctx.user(),
324 date=oldctx.date(), extra=oldctx.extra())
324 date=oldctx.date(), extra=oldctx.extra())
325 if n is None:
325 if n is None:
326 ui.warn(_('%s: empty changeset\n')
326 ui.warn(_('%s: empty changeset\n')
327 % node.hex(ha))
327 % node.hex(ha))
328 return ctx, []
328 return ctx, []
329 new = repo[n]
329 new = repo[n]
330 return new, [(oldctx.node(), (n,))]
330 return new, [(oldctx.node(), (n,))]
331
331
332
332
333 def edit(ui, repo, ctx, ha, opts):
333 def edit(ui, repo, ctx, ha, opts):
334 oldctx = repo[ha]
334 oldctx = repo[ha]
335 hg.update(repo, ctx.node())
335 hg.update(repo, ctx.node())
336 applychanges(ui, repo, oldctx, opts)
336 applychanges(ui, repo, oldctx, opts)
337 raise error.InterventionRequired(
337 raise error.InterventionRequired(
338 _('Make changes as needed, you may commit or record as needed now.\n'
338 _('Make changes as needed, you may commit or record as needed now.\n'
339 'When you are finished, run hg histedit --continue to resume.'))
339 'When you are finished, run hg histedit --continue to resume.'))
340
340
341 def rollup(ui, repo, ctx, ha, opts):
341 def rollup(ui, repo, ctx, ha, opts):
342 rollupopts = opts.copy()
342 rollupopts = opts.copy()
343 rollupopts['rollup'] = True
343 rollupopts['rollup'] = True
344 return fold(ui, repo, ctx, ha, rollupopts)
344 return fold(ui, repo, ctx, ha, rollupopts)
345
345
346 def fold(ui, repo, ctx, ha, opts):
346 def fold(ui, repo, ctx, ha, opts):
347 oldctx = repo[ha]
347 oldctx = repo[ha]
348 hg.update(repo, ctx.node())
348 hg.update(repo, ctx.node())
349 stats = applychanges(ui, repo, oldctx, opts)
349 stats = applychanges(ui, repo, oldctx, opts)
350 if stats and stats[3] > 0:
350 if stats and stats[3] > 0:
351 raise error.InterventionRequired(
351 raise error.InterventionRequired(
352 _('Fix up the change and run hg histedit --continue'))
352 _('Fix up the change and run hg histedit --continue'))
353 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
353 n = repo.commit(text='fold-temp-revision %s' % ha, user=oldctx.user(),
354 date=oldctx.date(), extra=oldctx.extra())
354 date=oldctx.date(), extra=oldctx.extra())
355 if n is None:
355 if n is None:
356 ui.warn(_('%s: empty changeset')
356 ui.warn(_('%s: empty changeset')
357 % node.hex(ha))
357 % node.hex(ha))
358 return ctx, []
358 return ctx, []
359 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
359 return finishfold(ui, repo, ctx, oldctx, n, opts, [])
360
360
361 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
361 def finishfold(ui, repo, ctx, oldctx, newnode, opts, internalchanges):
362 parent = ctx.parents()[0].node()
362 parent = ctx.parents()[0].node()
363 hg.update(repo, parent)
363 hg.update(repo, parent)
364 ### prepare new commit data
364 ### prepare new commit data
365 commitopts = opts.copy()
365 commitopts = opts.copy()
366 # username
366 commitopts['user'] = ctx.user()
367 if ctx.user() == oldctx.user():
368 username = ctx.user()
369 else:
370 username = ui.username()
371 commitopts['user'] = username
372 # commit message
367 # commit message
373 if opts.get('rollup'):
368 if opts.get('rollup'):
374 newmessage = ctx.description()
369 newmessage = ctx.description()
375 else:
370 else:
376 newmessage = '\n***\n'.join(
371 newmessage = '\n***\n'.join(
377 [ctx.description()] +
372 [ctx.description()] +
378 [repo[r].description() for r in internalchanges] +
373 [repo[r].description() for r in internalchanges] +
379 [oldctx.description()]) + '\n'
374 [oldctx.description()]) + '\n'
380 commitopts['message'] = newmessage
375 commitopts['message'] = newmessage
381 # date
376 # date
382 commitopts['date'] = max(ctx.date(), oldctx.date())
377 commitopts['date'] = max(ctx.date(), oldctx.date())
383 extra = ctx.extra().copy()
378 extra = ctx.extra().copy()
384 # histedit_source
379 # histedit_source
385 # note: ctx is likely a temporary commit but that the best we can do here
380 # note: ctx is likely a temporary commit but that the best we can do here
386 # This is sufficient to solve issue3681 anyway
381 # This is sufficient to solve issue3681 anyway
387 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
382 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
388 commitopts['extra'] = extra
383 commitopts['extra'] = extra
389 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
384 phasebackup = repo.ui.backupconfig('phases', 'new-commit')
390 try:
385 try:
391 phasemin = max(ctx.phase(), oldctx.phase())
386 phasemin = max(ctx.phase(), oldctx.phase())
392 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
387 repo.ui.setconfig('phases', 'new-commit', phasemin, 'histedit')
393 n = collapse(repo, ctx, repo[newnode], commitopts)
388 n = collapse(repo, ctx, repo[newnode], commitopts)
394 finally:
389 finally:
395 repo.ui.restoreconfig(phasebackup)
390 repo.ui.restoreconfig(phasebackup)
396 if n is None:
391 if n is None:
397 return ctx, []
392 return ctx, []
398 hg.update(repo, n)
393 hg.update(repo, n)
399 replacements = [(oldctx.node(), (newnode,)),
394 replacements = [(oldctx.node(), (newnode,)),
400 (ctx.node(), (n,)),
395 (ctx.node(), (n,)),
401 (newnode, (n,)),
396 (newnode, (n,)),
402 ]
397 ]
403 for ich in internalchanges:
398 for ich in internalchanges:
404 replacements.append((ich, (n,)))
399 replacements.append((ich, (n,)))
405 return repo[n], replacements
400 return repo[n], replacements
406
401
407 def drop(ui, repo, ctx, ha, opts):
402 def drop(ui, repo, ctx, ha, opts):
408 return ctx, [(repo[ha].node(), ())]
403 return ctx, [(repo[ha].node(), ())]
409
404
410
405
411 def message(ui, repo, ctx, ha, opts):
406 def message(ui, repo, ctx, ha, opts):
412 oldctx = repo[ha]
407 oldctx = repo[ha]
413 hg.update(repo, ctx.node())
408 hg.update(repo, ctx.node())
414 stats = applychanges(ui, repo, oldctx, opts)
409 stats = applychanges(ui, repo, oldctx, opts)
415 if stats and stats[3] > 0:
410 if stats and stats[3] > 0:
416 raise error.InterventionRequired(
411 raise error.InterventionRequired(
417 _('Fix up the change and run hg histedit --continue'))
412 _('Fix up the change and run hg histedit --continue'))
418 message = oldctx.description()
413 message = oldctx.description()
419 commit = commitfuncfor(repo, oldctx)
414 commit = commitfuncfor(repo, oldctx)
420 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
415 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
421 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
416 new = commit(text=message, user=oldctx.user(), date=oldctx.date(),
422 extra=oldctx.extra(),
417 extra=oldctx.extra(),
423 editor=editor)
418 editor=editor)
424 newctx = repo[new]
419 newctx = repo[new]
425 if oldctx.node() != newctx.node():
420 if oldctx.node() != newctx.node():
426 return newctx, [(oldctx.node(), (new,))]
421 return newctx, [(oldctx.node(), (new,))]
427 # We didn't make an edit, so just indicate no replaced nodes
422 # We didn't make an edit, so just indicate no replaced nodes
428 return newctx, []
423 return newctx, []
429
424
430 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
425 def findoutgoing(ui, repo, remote=None, force=False, opts={}):
431 """utility function to find the first outgoing changeset
426 """utility function to find the first outgoing changeset
432
427
433 Used by initialisation code"""
428 Used by initialisation code"""
434 dest = ui.expandpath(remote or 'default-push', remote or 'default')
429 dest = ui.expandpath(remote or 'default-push', remote or 'default')
435 dest, revs = hg.parseurl(dest, None)[:2]
430 dest, revs = hg.parseurl(dest, None)[:2]
436 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
431 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
437
432
438 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
433 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
439 other = hg.peer(repo, opts, dest)
434 other = hg.peer(repo, opts, dest)
440
435
441 if revs:
436 if revs:
442 revs = [repo.lookup(rev) for rev in revs]
437 revs = [repo.lookup(rev) for rev in revs]
443
438
444 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
439 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
445 if not outgoing.missing:
440 if not outgoing.missing:
446 raise util.Abort(_('no outgoing ancestors'))
441 raise util.Abort(_('no outgoing ancestors'))
447 roots = list(repo.revs("roots(%ln)", outgoing.missing))
442 roots = list(repo.revs("roots(%ln)", outgoing.missing))
448 if 1 < len(roots):
443 if 1 < len(roots):
449 msg = _('there are ambiguous outgoing revisions')
444 msg = _('there are ambiguous outgoing revisions')
450 hint = _('see "hg help histedit" for more detail')
445 hint = _('see "hg help histedit" for more detail')
451 raise util.Abort(msg, hint=hint)
446 raise util.Abort(msg, hint=hint)
452 return repo.lookup(roots[0])
447 return repo.lookup(roots[0])
453
448
454 actiontable = {'p': pick,
449 actiontable = {'p': pick,
455 'pick': pick,
450 'pick': pick,
456 'e': edit,
451 'e': edit,
457 'edit': edit,
452 'edit': edit,
458 'f': fold,
453 'f': fold,
459 'fold': fold,
454 'fold': fold,
460 'r': rollup,
455 'r': rollup,
461 'roll': rollup,
456 'roll': rollup,
462 'd': drop,
457 'd': drop,
463 'drop': drop,
458 'drop': drop,
464 'm': message,
459 'm': message,
465 'mess': message,
460 'mess': message,
466 }
461 }
467
462
468 @command('histedit',
463 @command('histedit',
469 [('', 'commands', '',
464 [('', 'commands', '',
470 _('Read history edits from the specified file.')),
465 _('Read history edits from the specified file.')),
471 ('c', 'continue', False, _('continue an edit already in progress')),
466 ('c', 'continue', False, _('continue an edit already in progress')),
472 ('k', 'keep', False,
467 ('k', 'keep', False,
473 _("don't strip old nodes after edit is complete")),
468 _("don't strip old nodes after edit is complete")),
474 ('', 'abort', False, _('abort an edit in progress')),
469 ('', 'abort', False, _('abort an edit in progress')),
475 ('o', 'outgoing', False, _('changesets not found in destination')),
470 ('o', 'outgoing', False, _('changesets not found in destination')),
476 ('f', 'force', False,
471 ('f', 'force', False,
477 _('force outgoing even for unrelated repositories')),
472 _('force outgoing even for unrelated repositories')),
478 ('r', 'rev', [], _('first revision to be edited'))],
473 ('r', 'rev', [], _('first revision to be edited'))],
479 _("ANCESTOR | --outgoing [URL]"))
474 _("ANCESTOR | --outgoing [URL]"))
480 def histedit(ui, repo, *freeargs, **opts):
475 def histedit(ui, repo, *freeargs, **opts):
481 """interactively edit changeset history
476 """interactively edit changeset history
482
477
483 This command edits changesets between ANCESTOR and the parent of
478 This command edits changesets between ANCESTOR and the parent of
484 the working directory.
479 the working directory.
485
480
486 With --outgoing, this edits changesets not found in the
481 With --outgoing, this edits changesets not found in the
487 destination repository. If URL of the destination is omitted, the
482 destination repository. If URL of the destination is omitted, the
488 'default-push' (or 'default') path will be used.
483 'default-push' (or 'default') path will be used.
489
484
490 For safety, this command is aborted, also if there are ambiguous
485 For safety, this command is aborted, also if there are ambiguous
491 outgoing revisions which may confuse users: for example, there are
486 outgoing revisions which may confuse users: for example, there are
492 multiple branches containing outgoing revisions.
487 multiple branches containing outgoing revisions.
493
488
494 Use "min(outgoing() and ::.)" or similar revset specification
489 Use "min(outgoing() and ::.)" or similar revset specification
495 instead of --outgoing to specify edit target revision exactly in
490 instead of --outgoing to specify edit target revision exactly in
496 such ambiguous situation. See :hg:`help revsets` for detail about
491 such ambiguous situation. See :hg:`help revsets` for detail about
497 selecting revisions.
492 selecting revisions.
498
493
499 Returns 0 on success, 1 if user intervention is required (not only
494 Returns 0 on success, 1 if user intervention is required (not only
500 for intentional "edit" command, but also for resolving unexpected
495 for intentional "edit" command, but also for resolving unexpected
501 conflicts).
496 conflicts).
502 """
497 """
503 lock = wlock = None
498 lock = wlock = None
504 try:
499 try:
505 wlock = repo.wlock()
500 wlock = repo.wlock()
506 lock = repo.lock()
501 lock = repo.lock()
507 _histedit(ui, repo, *freeargs, **opts)
502 _histedit(ui, repo, *freeargs, **opts)
508 finally:
503 finally:
509 release(lock, wlock)
504 release(lock, wlock)
510
505
511 def _histedit(ui, repo, *freeargs, **opts):
506 def _histedit(ui, repo, *freeargs, **opts):
512 # TODO only abort if we try and histedit mq patches, not just
507 # TODO only abort if we try and histedit mq patches, not just
513 # blanket if mq patches are applied somewhere
508 # blanket if mq patches are applied somewhere
514 mq = getattr(repo, 'mq', None)
509 mq = getattr(repo, 'mq', None)
515 if mq and mq.applied:
510 if mq and mq.applied:
516 raise util.Abort(_('source has mq patches applied'))
511 raise util.Abort(_('source has mq patches applied'))
517
512
518 # basic argument incompatibility processing
513 # basic argument incompatibility processing
519 outg = opts.get('outgoing')
514 outg = opts.get('outgoing')
520 cont = opts.get('continue')
515 cont = opts.get('continue')
521 abort = opts.get('abort')
516 abort = opts.get('abort')
522 force = opts.get('force')
517 force = opts.get('force')
523 rules = opts.get('commands', '')
518 rules = opts.get('commands', '')
524 revs = opts.get('rev', [])
519 revs = opts.get('rev', [])
525 goal = 'new' # This invocation goal, in new, continue, abort
520 goal = 'new' # This invocation goal, in new, continue, abort
526 if force and not outg:
521 if force and not outg:
527 raise util.Abort(_('--force only allowed with --outgoing'))
522 raise util.Abort(_('--force only allowed with --outgoing'))
528 if cont:
523 if cont:
529 if util.any((outg, abort, revs, freeargs, rules)):
524 if util.any((outg, abort, revs, freeargs, rules)):
530 raise util.Abort(_('no arguments allowed with --continue'))
525 raise util.Abort(_('no arguments allowed with --continue'))
531 goal = 'continue'
526 goal = 'continue'
532 elif abort:
527 elif abort:
533 if util.any((outg, revs, freeargs, rules)):
528 if util.any((outg, revs, freeargs, rules)):
534 raise util.Abort(_('no arguments allowed with --abort'))
529 raise util.Abort(_('no arguments allowed with --abort'))
535 goal = 'abort'
530 goal = 'abort'
536 else:
531 else:
537 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
532 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
538 raise util.Abort(_('history edit already in progress, try '
533 raise util.Abort(_('history edit already in progress, try '
539 '--continue or --abort'))
534 '--continue or --abort'))
540 if outg:
535 if outg:
541 if revs:
536 if revs:
542 raise util.Abort(_('no revisions allowed with --outgoing'))
537 raise util.Abort(_('no revisions allowed with --outgoing'))
543 if len(freeargs) > 1:
538 if len(freeargs) > 1:
544 raise util.Abort(
539 raise util.Abort(
545 _('only one repo argument allowed with --outgoing'))
540 _('only one repo argument allowed with --outgoing'))
546 else:
541 else:
547 revs.extend(freeargs)
542 revs.extend(freeargs)
548 if len(revs) != 1:
543 if len(revs) != 1:
549 raise util.Abort(
544 raise util.Abort(
550 _('histedit requires exactly one ancestor revision'))
545 _('histedit requires exactly one ancestor revision'))
551
546
552
547
553 if goal == 'continue':
548 if goal == 'continue':
554 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
549 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
555 parentctx = repo[parentctxnode]
550 parentctx = repo[parentctxnode]
556 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
551 parentctx, repl = bootstrapcontinue(ui, repo, parentctx, rules, opts)
557 replacements.extend(repl)
552 replacements.extend(repl)
558 elif goal == 'abort':
553 elif goal == 'abort':
559 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
554 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
560 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
555 mapping, tmpnodes, leafs, _ntm = processreplacement(repo, replacements)
561 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
556 ui.debug('restore wc to old parent %s\n' % node.short(topmost))
562 # check whether we should update away
557 # check whether we should update away
563 parentnodes = [c.node() for c in repo[None].parents()]
558 parentnodes = [c.node() for c in repo[None].parents()]
564 for n in leafs | set([parentctxnode]):
559 for n in leafs | set([parentctxnode]):
565 if n in parentnodes:
560 if n in parentnodes:
566 hg.clean(repo, topmost)
561 hg.clean(repo, topmost)
567 break
562 break
568 else:
563 else:
569 pass
564 pass
570 cleanupnode(ui, repo, 'created', tmpnodes)
565 cleanupnode(ui, repo, 'created', tmpnodes)
571 cleanupnode(ui, repo, 'temp', leafs)
566 cleanupnode(ui, repo, 'temp', leafs)
572 os.unlink(os.path.join(repo.path, 'histedit-state'))
567 os.unlink(os.path.join(repo.path, 'histedit-state'))
573 return
568 return
574 else:
569 else:
575 cmdutil.checkunfinished(repo)
570 cmdutil.checkunfinished(repo)
576 cmdutil.bailifchanged(repo)
571 cmdutil.bailifchanged(repo)
577
572
578 topmost, empty = repo.dirstate.parents()
573 topmost, empty = repo.dirstate.parents()
579 if outg:
574 if outg:
580 if freeargs:
575 if freeargs:
581 remote = freeargs[0]
576 remote = freeargs[0]
582 else:
577 else:
583 remote = None
578 remote = None
584 root = findoutgoing(ui, repo, remote, force, opts)
579 root = findoutgoing(ui, repo, remote, force, opts)
585 else:
580 else:
586 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
581 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
587 if len(rr) != 1:
582 if len(rr) != 1:
588 raise util.Abort(_('The specified revisions must have '
583 raise util.Abort(_('The specified revisions must have '
589 'exactly one common root'))
584 'exactly one common root'))
590 root = rr[0].node()
585 root = rr[0].node()
591
586
592 keep = opts.get('keep', False)
587 keep = opts.get('keep', False)
593 revs = between(repo, root, topmost, keep)
588 revs = between(repo, root, topmost, keep)
594 if not revs:
589 if not revs:
595 raise util.Abort(_('%s is not an ancestor of working directory') %
590 raise util.Abort(_('%s is not an ancestor of working directory') %
596 node.short(root))
591 node.short(root))
597
592
598 ctxs = [repo[r] for r in revs]
593 ctxs = [repo[r] for r in revs]
599 if not rules:
594 if not rules:
600 rules = '\n'.join([makedesc(c) for c in ctxs])
595 rules = '\n'.join([makedesc(c) for c in ctxs])
601 rules += '\n\n'
596 rules += '\n\n'
602 rules += editcomment % (node.short(root), node.short(topmost))
597 rules += editcomment % (node.short(root), node.short(topmost))
603 rules = ui.edit(rules, ui.username())
598 rules = ui.edit(rules, ui.username())
604 # Save edit rules in .hg/histedit-last-edit.txt in case
599 # Save edit rules in .hg/histedit-last-edit.txt in case
605 # the user needs to ask for help after something
600 # the user needs to ask for help after something
606 # surprising happens.
601 # surprising happens.
607 f = open(repo.join('histedit-last-edit.txt'), 'w')
602 f = open(repo.join('histedit-last-edit.txt'), 'w')
608 f.write(rules)
603 f.write(rules)
609 f.close()
604 f.close()
610 else:
605 else:
611 if rules == '-':
606 if rules == '-':
612 f = sys.stdin
607 f = sys.stdin
613 else:
608 else:
614 f = open(rules)
609 f = open(rules)
615 rules = f.read()
610 rules = f.read()
616 f.close()
611 f.close()
617 rules = [l for l in (r.strip() for r in rules.splitlines())
612 rules = [l for l in (r.strip() for r in rules.splitlines())
618 if l and not l[0] == '#']
613 if l and not l[0] == '#']
619 rules = verifyrules(rules, repo, ctxs)
614 rules = verifyrules(rules, repo, ctxs)
620
615
621 parentctx = repo[root].parents()[0]
616 parentctx = repo[root].parents()[0]
622 keep = opts.get('keep', False)
617 keep = opts.get('keep', False)
623 replacements = []
618 replacements = []
624
619
625
620
626 while rules:
621 while rules:
627 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
622 writestate(repo, parentctx.node(), rules, keep, topmost, replacements)
628 action, ha = rules.pop(0)
623 action, ha = rules.pop(0)
629 ui.debug('histedit: processing %s %s\n' % (action, ha))
624 ui.debug('histedit: processing %s %s\n' % (action, ha))
630 actfunc = actiontable[action]
625 actfunc = actiontable[action]
631 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
626 parentctx, replacement_ = actfunc(ui, repo, parentctx, ha, opts)
632 replacements.extend(replacement_)
627 replacements.extend(replacement_)
633
628
634 hg.update(repo, parentctx.node())
629 hg.update(repo, parentctx.node())
635
630
636 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
631 mapping, tmpnodes, created, ntm = processreplacement(repo, replacements)
637 if mapping:
632 if mapping:
638 for prec, succs in mapping.iteritems():
633 for prec, succs in mapping.iteritems():
639 if not succs:
634 if not succs:
640 ui.debug('histedit: %s is dropped\n' % node.short(prec))
635 ui.debug('histedit: %s is dropped\n' % node.short(prec))
641 else:
636 else:
642 ui.debug('histedit: %s is replaced by %s\n' % (
637 ui.debug('histedit: %s is replaced by %s\n' % (
643 node.short(prec), node.short(succs[0])))
638 node.short(prec), node.short(succs[0])))
644 if len(succs) > 1:
639 if len(succs) > 1:
645 m = 'histedit: %s'
640 m = 'histedit: %s'
646 for n in succs[1:]:
641 for n in succs[1:]:
647 ui.debug(m % node.short(n))
642 ui.debug(m % node.short(n))
648
643
649 if not keep:
644 if not keep:
650 if mapping:
645 if mapping:
651 movebookmarks(ui, repo, mapping, topmost, ntm)
646 movebookmarks(ui, repo, mapping, topmost, ntm)
652 # TODO update mq state
647 # TODO update mq state
653 if obsolete._enabled:
648 if obsolete._enabled:
654 markers = []
649 markers = []
655 # sort by revision number because it sound "right"
650 # sort by revision number because it sound "right"
656 for prec in sorted(mapping, key=repo.changelog.rev):
651 for prec in sorted(mapping, key=repo.changelog.rev):
657 succs = mapping[prec]
652 succs = mapping[prec]
658 markers.append((repo[prec],
653 markers.append((repo[prec],
659 tuple(repo[s] for s in succs)))
654 tuple(repo[s] for s in succs)))
660 if markers:
655 if markers:
661 obsolete.createmarkers(repo, markers)
656 obsolete.createmarkers(repo, markers)
662 else:
657 else:
663 cleanupnode(ui, repo, 'replaced', mapping)
658 cleanupnode(ui, repo, 'replaced', mapping)
664
659
665 cleanupnode(ui, repo, 'temp', tmpnodes)
660 cleanupnode(ui, repo, 'temp', tmpnodes)
666 os.unlink(os.path.join(repo.path, 'histedit-state'))
661 os.unlink(os.path.join(repo.path, 'histedit-state'))
667 if os.path.exists(repo.sjoin('undo')):
662 if os.path.exists(repo.sjoin('undo')):
668 os.unlink(repo.sjoin('undo'))
663 os.unlink(repo.sjoin('undo'))
669
664
670 def gatherchildren(repo, ctx):
665 def gatherchildren(repo, ctx):
671 # is there any new commit between the expected parent and "."
666 # is there any new commit between the expected parent and "."
672 #
667 #
673 # note: does not take non linear new change in account (but previous
668 # note: does not take non linear new change in account (but previous
674 # implementation didn't used them anyway (issue3655)
669 # implementation didn't used them anyway (issue3655)
675 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
670 newchildren = [c.node() for c in repo.set('(%d::.)', ctx)]
676 if ctx.node() != node.nullid:
671 if ctx.node() != node.nullid:
677 if not newchildren:
672 if not newchildren:
678 # `ctx` should match but no result. This means that
673 # `ctx` should match but no result. This means that
679 # currentnode is not a descendant from ctx.
674 # currentnode is not a descendant from ctx.
680 msg = _('%s is not an ancestor of working directory')
675 msg = _('%s is not an ancestor of working directory')
681 hint = _('use "histedit --abort" to clear broken state')
676 hint = _('use "histedit --abort" to clear broken state')
682 raise util.Abort(msg % ctx, hint=hint)
677 raise util.Abort(msg % ctx, hint=hint)
683 newchildren.pop(0) # remove ctx
678 newchildren.pop(0) # remove ctx
684 return newchildren
679 return newchildren
685
680
686 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
681 def bootstrapcontinue(ui, repo, parentctx, rules, opts):
687 action, currentnode = rules.pop(0)
682 action, currentnode = rules.pop(0)
688 ctx = repo[currentnode]
683 ctx = repo[currentnode]
689
684
690 newchildren = gatherchildren(repo, parentctx)
685 newchildren = gatherchildren(repo, parentctx)
691
686
692 # Commit dirty working directory if necessary
687 # Commit dirty working directory if necessary
693 new = None
688 new = None
694 m, a, r, d = repo.status()[:4]
689 m, a, r, d = repo.status()[:4]
695 if m or a or r or d:
690 if m or a or r or d:
696 # prepare the message for the commit to comes
691 # prepare the message for the commit to comes
697 if action in ('f', 'fold', 'r', 'roll'):
692 if action in ('f', 'fold', 'r', 'roll'):
698 message = 'fold-temp-revision %s' % currentnode
693 message = 'fold-temp-revision %s' % currentnode
699 else:
694 else:
700 message = ctx.description()
695 message = ctx.description()
701 editopt = action in ('e', 'edit', 'm', 'mess')
696 editopt = action in ('e', 'edit', 'm', 'mess')
702 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
697 canonaction = {'e': 'edit', 'm': 'mess', 'p': 'pick'}
703 editform = 'histedit.%s' % canonaction.get(action, action)
698 editform = 'histedit.%s' % canonaction.get(action, action)
704 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
699 editor = cmdutil.getcommiteditor(edit=editopt, editform=editform)
705 commit = commitfuncfor(repo, ctx)
700 commit = commitfuncfor(repo, ctx)
706 new = commit(text=message, user=ctx.user(),
701 new = commit(text=message, user=ctx.user(),
707 date=ctx.date(), extra=ctx.extra(),
702 date=ctx.date(), extra=ctx.extra(),
708 editor=editor)
703 editor=editor)
709 if new is not None:
704 if new is not None:
710 newchildren.append(new)
705 newchildren.append(new)
711
706
712 replacements = []
707 replacements = []
713 # track replacements
708 # track replacements
714 if ctx.node() not in newchildren:
709 if ctx.node() not in newchildren:
715 # note: new children may be empty when the changeset is dropped.
710 # note: new children may be empty when the changeset is dropped.
716 # this happen e.g during conflicting pick where we revert content
711 # this happen e.g during conflicting pick where we revert content
717 # to parent.
712 # to parent.
718 replacements.append((ctx.node(), tuple(newchildren)))
713 replacements.append((ctx.node(), tuple(newchildren)))
719
714
720 if action in ('f', 'fold', 'r', 'roll'):
715 if action in ('f', 'fold', 'r', 'roll'):
721 if newchildren:
716 if newchildren:
722 # finalize fold operation if applicable
717 # finalize fold operation if applicable
723 if new is None:
718 if new is None:
724 new = newchildren[-1]
719 new = newchildren[-1]
725 else:
720 else:
726 newchildren.pop() # remove new from internal changes
721 newchildren.pop() # remove new from internal changes
727 foldopts = opts
722 foldopts = opts
728 if action in ('r', 'roll'):
723 if action in ('r', 'roll'):
729 foldopts = foldopts.copy()
724 foldopts = foldopts.copy()
730 foldopts['rollup'] = True
725 foldopts['rollup'] = True
731 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
726 parentctx, repl = finishfold(ui, repo, parentctx, ctx, new,
732 foldopts, newchildren)
727 foldopts, newchildren)
733 replacements.extend(repl)
728 replacements.extend(repl)
734 else:
729 else:
735 # newchildren is empty if the fold did not result in any commit
730 # newchildren is empty if the fold did not result in any commit
736 # this happen when all folded change are discarded during the
731 # this happen when all folded change are discarded during the
737 # merge.
732 # merge.
738 replacements.append((ctx.node(), (parentctx.node(),)))
733 replacements.append((ctx.node(), (parentctx.node(),)))
739 elif newchildren:
734 elif newchildren:
740 # otherwise update "parentctx" before proceeding to further operation
735 # otherwise update "parentctx" before proceeding to further operation
741 parentctx = repo[newchildren[-1]]
736 parentctx = repo[newchildren[-1]]
742 return parentctx, replacements
737 return parentctx, replacements
743
738
744
739
745 def between(repo, old, new, keep):
740 def between(repo, old, new, keep):
746 """select and validate the set of revision to edit
741 """select and validate the set of revision to edit
747
742
748 When keep is false, the specified set can't have children."""
743 When keep is false, the specified set can't have children."""
749 ctxs = list(repo.set('%n::%n', old, new))
744 ctxs = list(repo.set('%n::%n', old, new))
750 if ctxs and not keep:
745 if ctxs and not keep:
751 if (not obsolete._enabled and
746 if (not obsolete._enabled and
752 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
747 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
753 raise util.Abort(_('cannot edit history that would orphan nodes'))
748 raise util.Abort(_('cannot edit history that would orphan nodes'))
754 if repo.revs('(%ld) and merge()', ctxs):
749 if repo.revs('(%ld) and merge()', ctxs):
755 raise util.Abort(_('cannot edit history that contains merges'))
750 raise util.Abort(_('cannot edit history that contains merges'))
756 root = ctxs[0] # list is already sorted by repo.set
751 root = ctxs[0] # list is already sorted by repo.set
757 if not root.phase():
752 if not root.phase():
758 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
753 raise util.Abort(_('cannot edit immutable changeset: %s') % root)
759 return [c.node() for c in ctxs]
754 return [c.node() for c in ctxs]
760
755
761
756
762 def writestate(repo, parentnode, rules, keep, topmost, replacements):
757 def writestate(repo, parentnode, rules, keep, topmost, replacements):
763 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
758 fp = open(os.path.join(repo.path, 'histedit-state'), 'w')
764 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
759 pickle.dump((parentnode, rules, keep, topmost, replacements), fp)
765 fp.close()
760 fp.close()
766
761
767 def readstate(repo):
762 def readstate(repo):
768 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
763 """Returns a tuple of (parentnode, rules, keep, topmost, replacements).
769 """
764 """
770 fp = open(os.path.join(repo.path, 'histedit-state'))
765 fp = open(os.path.join(repo.path, 'histedit-state'))
771 return pickle.load(fp)
766 return pickle.load(fp)
772
767
773
768
774 def makedesc(c):
769 def makedesc(c):
775 """build a initial action line for a ctx `c`
770 """build a initial action line for a ctx `c`
776
771
777 line are in the form:
772 line are in the form:
778
773
779 pick <hash> <rev> <summary>
774 pick <hash> <rev> <summary>
780 """
775 """
781 summary = ''
776 summary = ''
782 if c.description():
777 if c.description():
783 summary = c.description().splitlines()[0]
778 summary = c.description().splitlines()[0]
784 line = 'pick %s %d %s' % (c, c.rev(), summary)
779 line = 'pick %s %d %s' % (c, c.rev(), summary)
785 # trim to 80 columns so it's not stupidly wide in my editor
780 # trim to 80 columns so it's not stupidly wide in my editor
786 return util.ellipsis(line, 80)
781 return util.ellipsis(line, 80)
787
782
788 def verifyrules(rules, repo, ctxs):
783 def verifyrules(rules, repo, ctxs):
789 """Verify that there exists exactly one edit rule per given changeset.
784 """Verify that there exists exactly one edit rule per given changeset.
790
785
791 Will abort if there are to many or too few rules, a malformed rule,
786 Will abort if there are to many or too few rules, a malformed rule,
792 or a rule on a changeset outside of the user-given range.
787 or a rule on a changeset outside of the user-given range.
793 """
788 """
794 parsed = []
789 parsed = []
795 expected = set(str(c) for c in ctxs)
790 expected = set(str(c) for c in ctxs)
796 seen = set()
791 seen = set()
797 for r in rules:
792 for r in rules:
798 if ' ' not in r:
793 if ' ' not in r:
799 raise util.Abort(_('malformed line "%s"') % r)
794 raise util.Abort(_('malformed line "%s"') % r)
800 action, rest = r.split(' ', 1)
795 action, rest = r.split(' ', 1)
801 ha = rest.strip().split(' ', 1)[0]
796 ha = rest.strip().split(' ', 1)[0]
802 try:
797 try:
803 ha = str(repo[ha]) # ensure its a short hash
798 ha = str(repo[ha]) # ensure its a short hash
804 except error.RepoError:
799 except error.RepoError:
805 raise util.Abort(_('unknown changeset %s listed') % ha)
800 raise util.Abort(_('unknown changeset %s listed') % ha)
806 if ha not in expected:
801 if ha not in expected:
807 raise util.Abort(
802 raise util.Abort(
808 _('may not use changesets other than the ones listed'))
803 _('may not use changesets other than the ones listed'))
809 if ha in seen:
804 if ha in seen:
810 raise util.Abort(_('duplicated command for changeset %s') % ha)
805 raise util.Abort(_('duplicated command for changeset %s') % ha)
811 seen.add(ha)
806 seen.add(ha)
812 if action not in actiontable:
807 if action not in actiontable:
813 raise util.Abort(_('unknown action "%s"') % action)
808 raise util.Abort(_('unknown action "%s"') % action)
814 parsed.append([action, ha])
809 parsed.append([action, ha])
815 missing = sorted(expected - seen) # sort to stabilize output
810 missing = sorted(expected - seen) # sort to stabilize output
816 if missing:
811 if missing:
817 raise util.Abort(_('missing rules for changeset %s') % missing[0],
812 raise util.Abort(_('missing rules for changeset %s') % missing[0],
818 hint=_('do you want to use the drop action?'))
813 hint=_('do you want to use the drop action?'))
819 return parsed
814 return parsed
820
815
821 def processreplacement(repo, replacements):
816 def processreplacement(repo, replacements):
822 """process the list of replacements to return
817 """process the list of replacements to return
823
818
824 1) the final mapping between original and created nodes
819 1) the final mapping between original and created nodes
825 2) the list of temporary node created by histedit
820 2) the list of temporary node created by histedit
826 3) the list of new commit created by histedit"""
821 3) the list of new commit created by histedit"""
827 allsuccs = set()
822 allsuccs = set()
828 replaced = set()
823 replaced = set()
829 fullmapping = {}
824 fullmapping = {}
830 # initialise basic set
825 # initialise basic set
831 # fullmapping record all operation recorded in replacement
826 # fullmapping record all operation recorded in replacement
832 for rep in replacements:
827 for rep in replacements:
833 allsuccs.update(rep[1])
828 allsuccs.update(rep[1])
834 replaced.add(rep[0])
829 replaced.add(rep[0])
835 fullmapping.setdefault(rep[0], set()).update(rep[1])
830 fullmapping.setdefault(rep[0], set()).update(rep[1])
836 new = allsuccs - replaced
831 new = allsuccs - replaced
837 tmpnodes = allsuccs & replaced
832 tmpnodes = allsuccs & replaced
838 # Reduce content fullmapping into direct relation between original nodes
833 # Reduce content fullmapping into direct relation between original nodes
839 # and final node created during history edition
834 # and final node created during history edition
840 # Dropped changeset are replaced by an empty list
835 # Dropped changeset are replaced by an empty list
841 toproceed = set(fullmapping)
836 toproceed = set(fullmapping)
842 final = {}
837 final = {}
843 while toproceed:
838 while toproceed:
844 for x in list(toproceed):
839 for x in list(toproceed):
845 succs = fullmapping[x]
840 succs = fullmapping[x]
846 for s in list(succs):
841 for s in list(succs):
847 if s in toproceed:
842 if s in toproceed:
848 # non final node with unknown closure
843 # non final node with unknown closure
849 # We can't process this now
844 # We can't process this now
850 break
845 break
851 elif s in final:
846 elif s in final:
852 # non final node, replace with closure
847 # non final node, replace with closure
853 succs.remove(s)
848 succs.remove(s)
854 succs.update(final[s])
849 succs.update(final[s])
855 else:
850 else:
856 final[x] = succs
851 final[x] = succs
857 toproceed.remove(x)
852 toproceed.remove(x)
858 # remove tmpnodes from final mapping
853 # remove tmpnodes from final mapping
859 for n in tmpnodes:
854 for n in tmpnodes:
860 del final[n]
855 del final[n]
861 # we expect all changes involved in final to exist in the repo
856 # we expect all changes involved in final to exist in the repo
862 # turn `final` into list (topologically sorted)
857 # turn `final` into list (topologically sorted)
863 nm = repo.changelog.nodemap
858 nm = repo.changelog.nodemap
864 for prec, succs in final.items():
859 for prec, succs in final.items():
865 final[prec] = sorted(succs, key=nm.get)
860 final[prec] = sorted(succs, key=nm.get)
866
861
867 # computed topmost element (necessary for bookmark)
862 # computed topmost element (necessary for bookmark)
868 if new:
863 if new:
869 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
864 newtopmost = sorted(new, key=repo.changelog.rev)[-1]
870 elif not final:
865 elif not final:
871 # Nothing rewritten at all. we won't need `newtopmost`
866 # Nothing rewritten at all. we won't need `newtopmost`
872 # It is the same as `oldtopmost` and `processreplacement` know it
867 # It is the same as `oldtopmost` and `processreplacement` know it
873 newtopmost = None
868 newtopmost = None
874 else:
869 else:
875 # every body died. The newtopmost is the parent of the root.
870 # every body died. The newtopmost is the parent of the root.
876 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
871 newtopmost = repo[sorted(final, key=repo.changelog.rev)[0]].p1().node()
877
872
878 return final, tmpnodes, new, newtopmost
873 return final, tmpnodes, new, newtopmost
879
874
880 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
875 def movebookmarks(ui, repo, mapping, oldtopmost, newtopmost):
881 """Move bookmark from old to newly created node"""
876 """Move bookmark from old to newly created node"""
882 if not mapping:
877 if not mapping:
883 # if nothing got rewritten there is not purpose for this function
878 # if nothing got rewritten there is not purpose for this function
884 return
879 return
885 moves = []
880 moves = []
886 for bk, old in sorted(repo._bookmarks.iteritems()):
881 for bk, old in sorted(repo._bookmarks.iteritems()):
887 if old == oldtopmost:
882 if old == oldtopmost:
888 # special case ensure bookmark stay on tip.
883 # special case ensure bookmark stay on tip.
889 #
884 #
890 # This is arguably a feature and we may only want that for the
885 # This is arguably a feature and we may only want that for the
891 # active bookmark. But the behavior is kept compatible with the old
886 # active bookmark. But the behavior is kept compatible with the old
892 # version for now.
887 # version for now.
893 moves.append((bk, newtopmost))
888 moves.append((bk, newtopmost))
894 continue
889 continue
895 base = old
890 base = old
896 new = mapping.get(base, None)
891 new = mapping.get(base, None)
897 if new is None:
892 if new is None:
898 continue
893 continue
899 while not new:
894 while not new:
900 # base is killed, trying with parent
895 # base is killed, trying with parent
901 base = repo[base].p1().node()
896 base = repo[base].p1().node()
902 new = mapping.get(base, (base,))
897 new = mapping.get(base, (base,))
903 # nothing to move
898 # nothing to move
904 moves.append((bk, new[-1]))
899 moves.append((bk, new[-1]))
905 if moves:
900 if moves:
906 marks = repo._bookmarks
901 marks = repo._bookmarks
907 for mark, new in moves:
902 for mark, new in moves:
908 old = marks[mark]
903 old = marks[mark]
909 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
904 ui.note(_('histedit: moving bookmarks %s from %s to %s\n')
910 % (mark, node.short(old), node.short(new)))
905 % (mark, node.short(old), node.short(new)))
911 marks[mark] = new
906 marks[mark] = new
912 marks.write()
907 marks.write()
913
908
914 def cleanupnode(ui, repo, name, nodes):
909 def cleanupnode(ui, repo, name, nodes):
915 """strip a group of nodes from the repository
910 """strip a group of nodes from the repository
916
911
917 The set of node to strip may contains unknown nodes."""
912 The set of node to strip may contains unknown nodes."""
918 ui.debug('should strip %s nodes %s\n' %
913 ui.debug('should strip %s nodes %s\n' %
919 (name, ', '.join([node.short(n) for n in nodes])))
914 (name, ', '.join([node.short(n) for n in nodes])))
920 lock = None
915 lock = None
921 try:
916 try:
922 lock = repo.lock()
917 lock = repo.lock()
923 # Find all node that need to be stripped
918 # Find all node that need to be stripped
924 # (we hg %lr instead of %ln to silently ignore unknown item
919 # (we hg %lr instead of %ln to silently ignore unknown item
925 nm = repo.changelog.nodemap
920 nm = repo.changelog.nodemap
926 nodes = [n for n in nodes if n in nm]
921 nodes = [n for n in nodes if n in nm]
927 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
922 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
928 for c in roots:
923 for c in roots:
929 # We should process node in reverse order to strip tip most first.
924 # We should process node in reverse order to strip tip most first.
930 # but this trigger a bug in changegroup hook.
925 # but this trigger a bug in changegroup hook.
931 # This would reduce bundle overhead
926 # This would reduce bundle overhead
932 repair.strip(ui, repo, c)
927 repair.strip(ui, repo, c)
933 finally:
928 finally:
934 release(lock)
929 release(lock)
935
930
936 def summaryhook(ui, repo):
931 def summaryhook(ui, repo):
937 if not os.path.exists(repo.join('histedit-state')):
932 if not os.path.exists(repo.join('histedit-state')):
938 return
933 return
939 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
934 (parentctxnode, rules, keep, topmost, replacements) = readstate(repo)
940 if rules:
935 if rules:
941 # i18n: column positioning for "hg summary"
936 # i18n: column positioning for "hg summary"
942 ui.write(_('hist: %s (histedit --continue)\n') %
937 ui.write(_('hist: %s (histedit --continue)\n') %
943 (ui.label(_('%d remaining'), 'histedit.remaining') %
938 (ui.label(_('%d remaining'), 'histedit.remaining') %
944 len(rules)))
939 len(rules)))
945
940
946 def extsetup(ui):
941 def extsetup(ui):
947 cmdutil.summaryhooks.add('histedit', summaryhook)
942 cmdutil.summaryhooks.add('histedit', summaryhook)
948 cmdutil.unfinishedstates.append(
943 cmdutil.unfinishedstates.append(
949 ['histedit-state', False, True, _('histedit in progress'),
944 ['histedit-state', False, True, _('histedit in progress'),
950 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
945 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
@@ -1,1921 +1,1922 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # run-tests.py - Run a set of tests on Mercurial
3 # run-tests.py - Run a set of tests on Mercurial
4 #
4 #
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 # Modifying this script is tricky because it has many modes:
10 # Modifying this script is tricky because it has many modes:
11 # - serial (default) vs parallel (-jN, N > 1)
11 # - serial (default) vs parallel (-jN, N > 1)
12 # - no coverage (default) vs coverage (-c, -C, -s)
12 # - no coverage (default) vs coverage (-c, -C, -s)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
13 # - temp install (default) vs specific hg script (--with-hg, --local)
14 # - tests are a mix of shell scripts and Python scripts
14 # - tests are a mix of shell scripts and Python scripts
15 #
15 #
16 # If you change this script, it is recommended that you ensure you
16 # If you change this script, it is recommended that you ensure you
17 # haven't broken it by running it in various modes with a representative
17 # haven't broken it by running it in various modes with a representative
18 # sample of test scripts. For example:
18 # sample of test scripts. For example:
19 #
19 #
20 # 1) serial, no coverage, temp install:
20 # 1) serial, no coverage, temp install:
21 # ./run-tests.py test-s*
21 # ./run-tests.py test-s*
22 # 2) serial, no coverage, local hg:
22 # 2) serial, no coverage, local hg:
23 # ./run-tests.py --local test-s*
23 # ./run-tests.py --local test-s*
24 # 3) serial, coverage, temp install:
24 # 3) serial, coverage, temp install:
25 # ./run-tests.py -c test-s*
25 # ./run-tests.py -c test-s*
26 # 4) serial, coverage, local hg:
26 # 4) serial, coverage, local hg:
27 # ./run-tests.py -c --local test-s* # unsupported
27 # ./run-tests.py -c --local test-s* # unsupported
28 # 5) parallel, no coverage, temp install:
28 # 5) parallel, no coverage, temp install:
29 # ./run-tests.py -j2 test-s*
29 # ./run-tests.py -j2 test-s*
30 # 6) parallel, no coverage, local hg:
30 # 6) parallel, no coverage, local hg:
31 # ./run-tests.py -j2 --local test-s*
31 # ./run-tests.py -j2 --local test-s*
32 # 7) parallel, coverage, temp install:
32 # 7) parallel, coverage, temp install:
33 # ./run-tests.py -j2 -c test-s* # currently broken
33 # ./run-tests.py -j2 -c test-s* # currently broken
34 # 8) parallel, coverage, local install:
34 # 8) parallel, coverage, local install:
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
35 # ./run-tests.py -j2 -c --local test-s* # unsupported (and broken)
36 # 9) parallel, custom tmp dir:
36 # 9) parallel, custom tmp dir:
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
37 # ./run-tests.py -j2 --tmpdir /tmp/myhgtests
38 #
38 #
39 # (You could use any subset of the tests: test-s* happens to match
39 # (You could use any subset of the tests: test-s* happens to match
40 # enough that it's worth doing parallel runs, few enough that it
40 # enough that it's worth doing parallel runs, few enough that it
41 # completes fairly quickly, includes both shell and Python scripts, and
41 # completes fairly quickly, includes both shell and Python scripts, and
42 # includes some scripts that run daemon processes.)
42 # includes some scripts that run daemon processes.)
43
43
44 from distutils import version
44 from distutils import version
45 import difflib
45 import difflib
46 import errno
46 import errno
47 import optparse
47 import optparse
48 import os
48 import os
49 import shutil
49 import shutil
50 import subprocess
50 import subprocess
51 import signal
51 import signal
52 import sys
52 import sys
53 import tempfile
53 import tempfile
54 import time
54 import time
55 import random
55 import random
56 import re
56 import re
57 import threading
57 import threading
58 import killdaemons as killmod
58 import killdaemons as killmod
59 import Queue as queue
59 import Queue as queue
60 from xml.dom import minidom
60 from xml.dom import minidom
61 import unittest
61 import unittest
62
62
63 processlock = threading.Lock()
63 processlock = threading.Lock()
64
64
65 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
65 # subprocess._cleanup can race with any Popen.wait or Popen.poll on py24
66 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
66 # http://bugs.python.org/issue1731717 for details. We shouldn't be producing
67 # zombies but it's pretty harmless even if we do.
67 # zombies but it's pretty harmless even if we do.
68 if sys.version_info < (2, 5):
68 if sys.version_info < (2, 5):
69 subprocess._cleanup = lambda: None
69 subprocess._cleanup = lambda: None
70
70
71 closefds = os.name == 'posix'
71 closefds = os.name == 'posix'
72 def Popen4(cmd, wd, timeout, env=None):
72 def Popen4(cmd, wd, timeout, env=None):
73 processlock.acquire()
73 processlock.acquire()
74 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
74 p = subprocess.Popen(cmd, shell=True, bufsize=-1, cwd=wd, env=env,
75 close_fds=closefds,
75 close_fds=closefds,
76 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
76 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
77 stderr=subprocess.STDOUT)
77 stderr=subprocess.STDOUT)
78 processlock.release()
78 processlock.release()
79
79
80 p.fromchild = p.stdout
80 p.fromchild = p.stdout
81 p.tochild = p.stdin
81 p.tochild = p.stdin
82 p.childerr = p.stderr
82 p.childerr = p.stderr
83
83
84 p.timeout = False
84 p.timeout = False
85 if timeout:
85 if timeout:
86 def t():
86 def t():
87 start = time.time()
87 start = time.time()
88 while time.time() - start < timeout and p.returncode is None:
88 while time.time() - start < timeout and p.returncode is None:
89 time.sleep(.1)
89 time.sleep(.1)
90 p.timeout = True
90 p.timeout = True
91 if p.returncode is None:
91 if p.returncode is None:
92 terminate(p)
92 terminate(p)
93 threading.Thread(target=t).start()
93 threading.Thread(target=t).start()
94
94
95 return p
95 return p
96
96
97 PYTHON = sys.executable.replace('\\', '/')
97 PYTHON = sys.executable.replace('\\', '/')
98 IMPL_PATH = 'PYTHONPATH'
98 IMPL_PATH = 'PYTHONPATH'
99 if 'java' in sys.platform:
99 if 'java' in sys.platform:
100 IMPL_PATH = 'JYTHONPATH'
100 IMPL_PATH = 'JYTHONPATH'
101
101
102 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
102 TESTDIR = HGTMP = INST = BINDIR = TMPBINDIR = PYTHONDIR = None
103
103
104 defaults = {
104 defaults = {
105 'jobs': ('HGTEST_JOBS', 1),
105 'jobs': ('HGTEST_JOBS', 1),
106 'timeout': ('HGTEST_TIMEOUT', 180),
106 'timeout': ('HGTEST_TIMEOUT', 180),
107 'port': ('HGTEST_PORT', 20059),
107 'port': ('HGTEST_PORT', 20059),
108 'shell': ('HGTEST_SHELL', 'sh'),
108 'shell': ('HGTEST_SHELL', 'sh'),
109 }
109 }
110
110
111 def parselistfiles(files, listtype, warn=True):
111 def parselistfiles(files, listtype, warn=True):
112 entries = dict()
112 entries = dict()
113 for filename in files:
113 for filename in files:
114 try:
114 try:
115 path = os.path.expanduser(os.path.expandvars(filename))
115 path = os.path.expanduser(os.path.expandvars(filename))
116 f = open(path, "rb")
116 f = open(path, "rb")
117 except IOError, err:
117 except IOError, err:
118 if err.errno != errno.ENOENT:
118 if err.errno != errno.ENOENT:
119 raise
119 raise
120 if warn:
120 if warn:
121 print "warning: no such %s file: %s" % (listtype, filename)
121 print "warning: no such %s file: %s" % (listtype, filename)
122 continue
122 continue
123
123
124 for line in f.readlines():
124 for line in f.readlines():
125 line = line.split('#', 1)[0].strip()
125 line = line.split('#', 1)[0].strip()
126 if line:
126 if line:
127 entries[line] = filename
127 entries[line] = filename
128
128
129 f.close()
129 f.close()
130 return entries
130 return entries
131
131
132 def getparser():
132 def getparser():
133 """Obtain the OptionParser used by the CLI."""
133 """Obtain the OptionParser used by the CLI."""
134 parser = optparse.OptionParser("%prog [options] [tests]")
134 parser = optparse.OptionParser("%prog [options] [tests]")
135
135
136 # keep these sorted
136 # keep these sorted
137 parser.add_option("--blacklist", action="append",
137 parser.add_option("--blacklist", action="append",
138 help="skip tests listed in the specified blacklist file")
138 help="skip tests listed in the specified blacklist file")
139 parser.add_option("--whitelist", action="append",
139 parser.add_option("--whitelist", action="append",
140 help="always run tests listed in the specified whitelist file")
140 help="always run tests listed in the specified whitelist file")
141 parser.add_option("--changed", type="string",
141 parser.add_option("--changed", type="string",
142 help="run tests that are changed in parent rev or working directory")
142 help="run tests that are changed in parent rev or working directory")
143 parser.add_option("-C", "--annotate", action="store_true",
143 parser.add_option("-C", "--annotate", action="store_true",
144 help="output files annotated with coverage")
144 help="output files annotated with coverage")
145 parser.add_option("-c", "--cover", action="store_true",
145 parser.add_option("-c", "--cover", action="store_true",
146 help="print a test coverage report")
146 help="print a test coverage report")
147 parser.add_option("-d", "--debug", action="store_true",
147 parser.add_option("-d", "--debug", action="store_true",
148 help="debug mode: write output of test scripts to console"
148 help="debug mode: write output of test scripts to console"
149 " rather than capturing and diffing it (disables timeout)")
149 " rather than capturing and diffing it (disables timeout)")
150 parser.add_option("-f", "--first", action="store_true",
150 parser.add_option("-f", "--first", action="store_true",
151 help="exit on the first test failure")
151 help="exit on the first test failure")
152 parser.add_option("-H", "--htmlcov", action="store_true",
152 parser.add_option("-H", "--htmlcov", action="store_true",
153 help="create an HTML report of the coverage of the files")
153 help="create an HTML report of the coverage of the files")
154 parser.add_option("-i", "--interactive", action="store_true",
154 parser.add_option("-i", "--interactive", action="store_true",
155 help="prompt to accept changed output")
155 help="prompt to accept changed output")
156 parser.add_option("-j", "--jobs", type="int",
156 parser.add_option("-j", "--jobs", type="int",
157 help="number of jobs to run in parallel"
157 help="number of jobs to run in parallel"
158 " (default: $%s or %d)" % defaults['jobs'])
158 " (default: $%s or %d)" % defaults['jobs'])
159 parser.add_option("--keep-tmpdir", action="store_true",
159 parser.add_option("--keep-tmpdir", action="store_true",
160 help="keep temporary directory after running tests")
160 help="keep temporary directory after running tests")
161 parser.add_option("-k", "--keywords",
161 parser.add_option("-k", "--keywords",
162 help="run tests matching keywords")
162 help="run tests matching keywords")
163 parser.add_option("-l", "--local", action="store_true",
163 parser.add_option("-l", "--local", action="store_true",
164 help="shortcut for --with-hg=<testdir>/../hg")
164 help="shortcut for --with-hg=<testdir>/../hg")
165 parser.add_option("--loop", action="store_true",
165 parser.add_option("--loop", action="store_true",
166 help="loop tests repeatedly")
166 help="loop tests repeatedly")
167 parser.add_option("-n", "--nodiff", action="store_true",
167 parser.add_option("-n", "--nodiff", action="store_true",
168 help="skip showing test changes")
168 help="skip showing test changes")
169 parser.add_option("-p", "--port", type="int",
169 parser.add_option("-p", "--port", type="int",
170 help="port on which servers should listen"
170 help="port on which servers should listen"
171 " (default: $%s or %d)" % defaults['port'])
171 " (default: $%s or %d)" % defaults['port'])
172 parser.add_option("--compiler", type="string",
172 parser.add_option("--compiler", type="string",
173 help="compiler to build with")
173 help="compiler to build with")
174 parser.add_option("--pure", action="store_true",
174 parser.add_option("--pure", action="store_true",
175 help="use pure Python code instead of C extensions")
175 help="use pure Python code instead of C extensions")
176 parser.add_option("-R", "--restart", action="store_true",
176 parser.add_option("-R", "--restart", action="store_true",
177 help="restart at last error")
177 help="restart at last error")
178 parser.add_option("-r", "--retest", action="store_true",
178 parser.add_option("-r", "--retest", action="store_true",
179 help="retest failed tests")
179 help="retest failed tests")
180 parser.add_option("-S", "--noskips", action="store_true",
180 parser.add_option("-S", "--noskips", action="store_true",
181 help="don't report skip tests verbosely")
181 help="don't report skip tests verbosely")
182 parser.add_option("--shell", type="string",
182 parser.add_option("--shell", type="string",
183 help="shell to use (default: $%s or %s)" % defaults['shell'])
183 help="shell to use (default: $%s or %s)" % defaults['shell'])
184 parser.add_option("-t", "--timeout", type="int",
184 parser.add_option("-t", "--timeout", type="int",
185 help="kill errant tests after TIMEOUT seconds"
185 help="kill errant tests after TIMEOUT seconds"
186 " (default: $%s or %d)" % defaults['timeout'])
186 " (default: $%s or %d)" % defaults['timeout'])
187 parser.add_option("--time", action="store_true",
187 parser.add_option("--time", action="store_true",
188 help="time how long each test takes")
188 help="time how long each test takes")
189 parser.add_option("--tmpdir", type="string",
189 parser.add_option("--tmpdir", type="string",
190 help="run tests in the given temporary directory"
190 help="run tests in the given temporary directory"
191 " (implies --keep-tmpdir)")
191 " (implies --keep-tmpdir)")
192 parser.add_option("-v", "--verbose", action="store_true",
192 parser.add_option("-v", "--verbose", action="store_true",
193 help="output verbose messages")
193 help="output verbose messages")
194 parser.add_option("--xunit", type="string",
194 parser.add_option("--xunit", type="string",
195 help="record xunit results at specified path")
195 help="record xunit results at specified path")
196 parser.add_option("--view", type="string",
196 parser.add_option("--view", type="string",
197 help="external diff viewer")
197 help="external diff viewer")
198 parser.add_option("--with-hg", type="string",
198 parser.add_option("--with-hg", type="string",
199 metavar="HG",
199 metavar="HG",
200 help="test using specified hg script rather than a "
200 help="test using specified hg script rather than a "
201 "temporary installation")
201 "temporary installation")
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
202 parser.add_option("-3", "--py3k-warnings", action="store_true",
203 help="enable Py3k warnings on Python 2.6+")
203 help="enable Py3k warnings on Python 2.6+")
204 parser.add_option('--extra-config-opt', action="append",
204 parser.add_option('--extra-config-opt', action="append",
205 help='set the given config opt in the test hgrc')
205 help='set the given config opt in the test hgrc')
206 parser.add_option('--random', action="store_true",
206 parser.add_option('--random', action="store_true",
207 help='run tests in random order')
207 help='run tests in random order')
208
208
209 for option, (envvar, default) in defaults.items():
209 for option, (envvar, default) in defaults.items():
210 defaults[option] = type(default)(os.environ.get(envvar, default))
210 defaults[option] = type(default)(os.environ.get(envvar, default))
211 parser.set_defaults(**defaults)
211 parser.set_defaults(**defaults)
212
212
213 return parser
213 return parser
214
214
215 def parseargs(args, parser):
215 def parseargs(args, parser):
216 """Parse arguments with our OptionParser and validate results."""
216 """Parse arguments with our OptionParser and validate results."""
217 (options, args) = parser.parse_args(args)
217 (options, args) = parser.parse_args(args)
218
218
219 # jython is always pure
219 # jython is always pure
220 if 'java' in sys.platform or '__pypy__' in sys.modules:
220 if 'java' in sys.platform or '__pypy__' in sys.modules:
221 options.pure = True
221 options.pure = True
222
222
223 if options.with_hg:
223 if options.with_hg:
224 options.with_hg = os.path.expanduser(options.with_hg)
224 options.with_hg = os.path.expanduser(options.with_hg)
225 if not (os.path.isfile(options.with_hg) and
225 if not (os.path.isfile(options.with_hg) and
226 os.access(options.with_hg, os.X_OK)):
226 os.access(options.with_hg, os.X_OK)):
227 parser.error('--with-hg must specify an executable hg script')
227 parser.error('--with-hg must specify an executable hg script')
228 if not os.path.basename(options.with_hg) == 'hg':
228 if not os.path.basename(options.with_hg) == 'hg':
229 sys.stderr.write('warning: --with-hg should specify an hg script\n')
229 sys.stderr.write('warning: --with-hg should specify an hg script\n')
230 if options.local:
230 if options.local:
231 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
231 testdir = os.path.dirname(os.path.realpath(sys.argv[0]))
232 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
232 hgbin = os.path.join(os.path.dirname(testdir), 'hg')
233 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
233 if os.name != 'nt' and not os.access(hgbin, os.X_OK):
234 parser.error('--local specified, but %r not found or not executable'
234 parser.error('--local specified, but %r not found or not executable'
235 % hgbin)
235 % hgbin)
236 options.with_hg = hgbin
236 options.with_hg = hgbin
237
237
238 options.anycoverage = options.cover or options.annotate or options.htmlcov
238 options.anycoverage = options.cover or options.annotate or options.htmlcov
239 if options.anycoverage:
239 if options.anycoverage:
240 try:
240 try:
241 import coverage
241 import coverage
242 covver = version.StrictVersion(coverage.__version__).version
242 covver = version.StrictVersion(coverage.__version__).version
243 if covver < (3, 3):
243 if covver < (3, 3):
244 parser.error('coverage options require coverage 3.3 or later')
244 parser.error('coverage options require coverage 3.3 or later')
245 except ImportError:
245 except ImportError:
246 parser.error('coverage options now require the coverage package')
246 parser.error('coverage options now require the coverage package')
247
247
248 if options.anycoverage and options.local:
248 if options.anycoverage and options.local:
249 # this needs some path mangling somewhere, I guess
249 # this needs some path mangling somewhere, I guess
250 parser.error("sorry, coverage options do not work when --local "
250 parser.error("sorry, coverage options do not work when --local "
251 "is specified")
251 "is specified")
252
252
253 global verbose
253 global verbose
254 if options.verbose:
254 if options.verbose:
255 verbose = ''
255 verbose = ''
256
256
257 if options.tmpdir:
257 if options.tmpdir:
258 options.tmpdir = os.path.expanduser(options.tmpdir)
258 options.tmpdir = os.path.expanduser(options.tmpdir)
259
259
260 if options.jobs < 1:
260 if options.jobs < 1:
261 parser.error('--jobs must be positive')
261 parser.error('--jobs must be positive')
262 if options.interactive and options.debug:
262 if options.interactive and options.debug:
263 parser.error("-i/--interactive and -d/--debug are incompatible")
263 parser.error("-i/--interactive and -d/--debug are incompatible")
264 if options.debug:
264 if options.debug:
265 if options.timeout != defaults['timeout']:
265 if options.timeout != defaults['timeout']:
266 sys.stderr.write(
266 sys.stderr.write(
267 'warning: --timeout option ignored with --debug\n')
267 'warning: --timeout option ignored with --debug\n')
268 options.timeout = 0
268 options.timeout = 0
269 if options.py3k_warnings:
269 if options.py3k_warnings:
270 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
270 if sys.version_info[:2] < (2, 6) or sys.version_info[:2] >= (3, 0):
271 parser.error('--py3k-warnings can only be used on Python 2.6+')
271 parser.error('--py3k-warnings can only be used on Python 2.6+')
272 if options.blacklist:
272 if options.blacklist:
273 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
273 options.blacklist = parselistfiles(options.blacklist, 'blacklist')
274 if options.whitelist:
274 if options.whitelist:
275 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
275 options.whitelisted = parselistfiles(options.whitelist, 'whitelist')
276 else:
276 else:
277 options.whitelisted = {}
277 options.whitelisted = {}
278
278
279 return (options, args)
279 return (options, args)
280
280
281 def rename(src, dst):
281 def rename(src, dst):
282 """Like os.rename(), trade atomicity and opened files friendliness
282 """Like os.rename(), trade atomicity and opened files friendliness
283 for existing destination support.
283 for existing destination support.
284 """
284 """
285 shutil.copy(src, dst)
285 shutil.copy(src, dst)
286 os.remove(src)
286 os.remove(src)
287
287
288 def getdiff(expected, output, ref, err):
288 def getdiff(expected, output, ref, err):
289 servefail = False
289 servefail = False
290 lines = []
290 lines = []
291 for line in difflib.unified_diff(expected, output, ref, err):
291 for line in difflib.unified_diff(expected, output, ref, err):
292 if line.startswith('+++') or line.startswith('---'):
292 if line.startswith('+++') or line.startswith('---'):
293 if line.endswith(' \n'):
293 if line.endswith(' \n'):
294 line = line[:-2] + '\n'
294 line = line[:-2] + '\n'
295 lines.append(line)
295 lines.append(line)
296 if not servefail and line.startswith(
296 if not servefail and line.startswith(
297 '+ abort: child process failed to start'):
297 '+ abort: child process failed to start'):
298 servefail = True
298 servefail = True
299
299
300 return servefail, lines
300 return servefail, lines
301
301
302 verbose = False
302 verbose = False
303 def vlog(*msg):
303 def vlog(*msg):
304 """Log only when in verbose mode."""
304 """Log only when in verbose mode."""
305 if verbose is False:
305 if verbose is False:
306 return
306 return
307
307
308 return log(*msg)
308 return log(*msg)
309
309
310 # Bytes that break XML even in a CDATA block: control characters 0-31
310 # Bytes that break XML even in a CDATA block: control characters 0-31
311 # sans \t, \n and \r
311 # sans \t, \n and \r
312 CDATA_EVIL = re.compile(r"[\000-\010\013\014\016-\037]")
312 CDATA_EVIL = re.compile(r"[\000-\010\013\014\016-\037]")
313
313
314 def cdatasafe(data):
314 def cdatasafe(data):
315 """Make a string safe to include in a CDATA block.
315 """Make a string safe to include in a CDATA block.
316
316
317 Certain control characters are illegal in a CDATA block, and
317 Certain control characters are illegal in a CDATA block, and
318 there's no way to include a ]]> in a CDATA either. This function
318 there's no way to include a ]]> in a CDATA either. This function
319 replaces illegal bytes with ? and adds a space between the ]] so
319 replaces illegal bytes with ? and adds a space between the ]] so
320 that it won't break the CDATA block.
320 that it won't break the CDATA block.
321 """
321 """
322 return CDATA_EVIL.sub('?', data).replace(']]>', '] ]>')
322 return CDATA_EVIL.sub('?', data).replace(']]>', '] ]>')
323
323
324 def log(*msg):
324 def log(*msg):
325 """Log something to stdout.
325 """Log something to stdout.
326
326
327 Arguments are strings to print.
327 Arguments are strings to print.
328 """
328 """
329 iolock.acquire()
329 iolock.acquire()
330 if verbose:
330 if verbose:
331 print verbose,
331 print verbose,
332 for m in msg:
332 for m in msg:
333 print m,
333 print m,
334 print
334 print
335 sys.stdout.flush()
335 sys.stdout.flush()
336 iolock.release()
336 iolock.release()
337
337
338 def terminate(proc):
338 def terminate(proc):
339 """Terminate subprocess (with fallback for Python versions < 2.6)"""
339 """Terminate subprocess (with fallback for Python versions < 2.6)"""
340 vlog('# Terminating process %d' % proc.pid)
340 vlog('# Terminating process %d' % proc.pid)
341 try:
341 try:
342 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
342 getattr(proc, 'terminate', lambda : os.kill(proc.pid, signal.SIGTERM))()
343 except OSError:
343 except OSError:
344 pass
344 pass
345
345
346 def killdaemons(pidfile):
346 def killdaemons(pidfile):
347 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
347 return killmod.killdaemons(pidfile, tryhard=False, remove=True,
348 logfn=vlog)
348 logfn=vlog)
349
349
350 class Test(unittest.TestCase):
350 class Test(unittest.TestCase):
351 """Encapsulates a single, runnable test.
351 """Encapsulates a single, runnable test.
352
352
353 While this class conforms to the unittest.TestCase API, it differs in that
353 While this class conforms to the unittest.TestCase API, it differs in that
354 instances need to be instantiated manually. (Typically, unittest.TestCase
354 instances need to be instantiated manually. (Typically, unittest.TestCase
355 classes are instantiated automatically by scanning modules.)
355 classes are instantiated automatically by scanning modules.)
356 """
356 """
357
357
358 # Status code reserved for skipped tests (used by hghave).
358 # Status code reserved for skipped tests (used by hghave).
359 SKIPPED_STATUS = 80
359 SKIPPED_STATUS = 80
360
360
361 def __init__(self, path, tmpdir, keeptmpdir=False,
361 def __init__(self, path, tmpdir, keeptmpdir=False,
362 debug=False,
362 debug=False,
363 timeout=defaults['timeout'],
363 timeout=defaults['timeout'],
364 startport=defaults['port'], extraconfigopts=None,
364 startport=defaults['port'], extraconfigopts=None,
365 py3kwarnings=False, shell=None):
365 py3kwarnings=False, shell=None):
366 """Create a test from parameters.
366 """Create a test from parameters.
367
367
368 path is the full path to the file defining the test.
368 path is the full path to the file defining the test.
369
369
370 tmpdir is the main temporary directory to use for this test.
370 tmpdir is the main temporary directory to use for this test.
371
371
372 keeptmpdir determines whether to keep the test's temporary directory
372 keeptmpdir determines whether to keep the test's temporary directory
373 after execution. It defaults to removal (False).
373 after execution. It defaults to removal (False).
374
374
375 debug mode will make the test execute verbosely, with unfiltered
375 debug mode will make the test execute verbosely, with unfiltered
376 output.
376 output.
377
377
378 timeout controls the maximum run time of the test. It is ignored when
378 timeout controls the maximum run time of the test. It is ignored when
379 debug is True.
379 debug is True.
380
380
381 startport controls the starting port number to use for this test. Each
381 startport controls the starting port number to use for this test. Each
382 test will reserve 3 port numbers for execution. It is the caller's
382 test will reserve 3 port numbers for execution. It is the caller's
383 responsibility to allocate a non-overlapping port range to Test
383 responsibility to allocate a non-overlapping port range to Test
384 instances.
384 instances.
385
385
386 extraconfigopts is an iterable of extra hgrc config options. Values
386 extraconfigopts is an iterable of extra hgrc config options. Values
387 must have the form "key=value" (something understood by hgrc). Values
387 must have the form "key=value" (something understood by hgrc). Values
388 of the form "foo.key=value" will result in "[foo] key=value".
388 of the form "foo.key=value" will result in "[foo] key=value".
389
389
390 py3kwarnings enables Py3k warnings.
390 py3kwarnings enables Py3k warnings.
391
391
392 shell is the shell to execute tests in.
392 shell is the shell to execute tests in.
393 """
393 """
394
394
395 self.path = path
395 self.path = path
396 self.name = os.path.basename(path)
396 self.name = os.path.basename(path)
397 self._testdir = os.path.dirname(path)
397 self._testdir = os.path.dirname(path)
398 self.errpath = os.path.join(self._testdir, '%s.err' % self.name)
398 self.errpath = os.path.join(self._testdir, '%s.err' % self.name)
399
399
400 self._threadtmp = tmpdir
400 self._threadtmp = tmpdir
401 self._keeptmpdir = keeptmpdir
401 self._keeptmpdir = keeptmpdir
402 self._debug = debug
402 self._debug = debug
403 self._timeout = timeout
403 self._timeout = timeout
404 self._startport = startport
404 self._startport = startport
405 self._extraconfigopts = extraconfigopts or []
405 self._extraconfigopts = extraconfigopts or []
406 self._py3kwarnings = py3kwarnings
406 self._py3kwarnings = py3kwarnings
407 self._shell = shell
407 self._shell = shell
408
408
409 self._aborted = False
409 self._aborted = False
410 self._daemonpids = []
410 self._daemonpids = []
411 self._finished = None
411 self._finished = None
412 self._ret = None
412 self._ret = None
413 self._out = None
413 self._out = None
414 self._skipped = None
414 self._skipped = None
415 self._testtmp = None
415 self._testtmp = None
416
416
417 # If we're not in --debug mode and reference output file exists,
417 # If we're not in --debug mode and reference output file exists,
418 # check test output against it.
418 # check test output against it.
419 if debug:
419 if debug:
420 self._refout = None # to match "out is None"
420 self._refout = None # to match "out is None"
421 elif os.path.exists(self.refpath):
421 elif os.path.exists(self.refpath):
422 f = open(self.refpath, 'rb')
422 f = open(self.refpath, 'rb')
423 self._refout = f.read().splitlines(True)
423 self._refout = f.read().splitlines(True)
424 f.close()
424 f.close()
425 else:
425 else:
426 self._refout = []
426 self._refout = []
427
427
428 def __str__(self):
428 def __str__(self):
429 return self.name
429 return self.name
430
430
431 def shortDescription(self):
431 def shortDescription(self):
432 return self.name
432 return self.name
433
433
434 def setUp(self):
434 def setUp(self):
435 """Tasks to perform before run()."""
435 """Tasks to perform before run()."""
436 self._finished = False
436 self._finished = False
437 self._ret = None
437 self._ret = None
438 self._out = None
438 self._out = None
439 self._skipped = None
439 self._skipped = None
440
440
441 try:
441 try:
442 os.mkdir(self._threadtmp)
442 os.mkdir(self._threadtmp)
443 except OSError, e:
443 except OSError, e:
444 if e.errno != errno.EEXIST:
444 if e.errno != errno.EEXIST:
445 raise
445 raise
446
446
447 self._testtmp = os.path.join(self._threadtmp,
447 self._testtmp = os.path.join(self._threadtmp,
448 os.path.basename(self.path))
448 os.path.basename(self.path))
449 os.mkdir(self._testtmp)
449 os.mkdir(self._testtmp)
450
450
451 # Remove any previous output files.
451 # Remove any previous output files.
452 if os.path.exists(self.errpath):
452 if os.path.exists(self.errpath):
453 os.remove(self.errpath)
453 os.remove(self.errpath)
454
454
455 def run(self, result):
455 def run(self, result):
456 """Run this test and report results against a TestResult instance."""
456 """Run this test and report results against a TestResult instance."""
457 # This function is extremely similar to unittest.TestCase.run(). Once
457 # This function is extremely similar to unittest.TestCase.run(). Once
458 # we require Python 2.7 (or at least its version of unittest), this
458 # we require Python 2.7 (or at least its version of unittest), this
459 # function can largely go away.
459 # function can largely go away.
460 self._result = result
460 self._result = result
461 result.startTest(self)
461 result.startTest(self)
462 try:
462 try:
463 try:
463 try:
464 self.setUp()
464 self.setUp()
465 except (KeyboardInterrupt, SystemExit):
465 except (KeyboardInterrupt, SystemExit):
466 self._aborted = True
466 self._aborted = True
467 raise
467 raise
468 except Exception:
468 except Exception:
469 result.addError(self, sys.exc_info())
469 result.addError(self, sys.exc_info())
470 return
470 return
471
471
472 success = False
472 success = False
473 try:
473 try:
474 self.runTest()
474 self.runTest()
475 except KeyboardInterrupt:
475 except KeyboardInterrupt:
476 self._aborted = True
476 self._aborted = True
477 raise
477 raise
478 except SkipTest, e:
478 except SkipTest, e:
479 result.addSkip(self, str(e))
479 result.addSkip(self, str(e))
480 # The base class will have already counted this as a
480 # The base class will have already counted this as a
481 # test we "ran", but we want to exclude skipped tests
481 # test we "ran", but we want to exclude skipped tests
482 # from those we count towards those run.
482 # from those we count towards those run.
483 result.testsRun -= 1
483 result.testsRun -= 1
484 except IgnoreTest, e:
484 except IgnoreTest, e:
485 result.addIgnore(self, str(e))
485 result.addIgnore(self, str(e))
486 # As with skips, ignores also should be excluded from
486 # As with skips, ignores also should be excluded from
487 # the number of tests executed.
487 # the number of tests executed.
488 result.testsRun -= 1
488 result.testsRun -= 1
489 except WarnTest, e:
489 except WarnTest, e:
490 result.addWarn(self, str(e))
490 result.addWarn(self, str(e))
491 except self.failureException, e:
491 except self.failureException, e:
492 # This differs from unittest in that we don't capture
492 # This differs from unittest in that we don't capture
493 # the stack trace. This is for historical reasons and
493 # the stack trace. This is for historical reasons and
494 # this decision could be revisted in the future,
494 # this decision could be revisted in the future,
495 # especially for PythonTest instances.
495 # especially for PythonTest instances.
496 if result.addFailure(self, str(e)):
496 if result.addFailure(self, str(e)):
497 success = True
497 success = True
498 except Exception:
498 except Exception:
499 result.addError(self, sys.exc_info())
499 result.addError(self, sys.exc_info())
500 else:
500 else:
501 success = True
501 success = True
502
502
503 try:
503 try:
504 self.tearDown()
504 self.tearDown()
505 except (KeyboardInterrupt, SystemExit):
505 except (KeyboardInterrupt, SystemExit):
506 self._aborted = True
506 self._aborted = True
507 raise
507 raise
508 except Exception:
508 except Exception:
509 result.addError(self, sys.exc_info())
509 result.addError(self, sys.exc_info())
510 success = False
510 success = False
511
511
512 if success:
512 if success:
513 result.addSuccess(self)
513 result.addSuccess(self)
514 finally:
514 finally:
515 result.stopTest(self, interrupted=self._aborted)
515 result.stopTest(self, interrupted=self._aborted)
516
516
517 def runTest(self):
517 def runTest(self):
518 """Run this test instance.
518 """Run this test instance.
519
519
520 This will return a tuple describing the result of the test.
520 This will return a tuple describing the result of the test.
521 """
521 """
522 replacements = self._getreplacements()
522 replacements = self._getreplacements()
523 env = self._getenv()
523 env = self._getenv()
524 self._daemonpids.append(env['DAEMON_PIDS'])
524 self._daemonpids.append(env['DAEMON_PIDS'])
525 self._createhgrc(env['HGRCPATH'])
525 self._createhgrc(env['HGRCPATH'])
526
526
527 vlog('# Test', self.name)
527 vlog('# Test', self.name)
528
528
529 ret, out = self._run(replacements, env)
529 ret, out = self._run(replacements, env)
530 self._finished = True
530 self._finished = True
531 self._ret = ret
531 self._ret = ret
532 self._out = out
532 self._out = out
533
533
534 def describe(ret):
534 def describe(ret):
535 if ret < 0:
535 if ret < 0:
536 return 'killed by signal: %d' % -ret
536 return 'killed by signal: %d' % -ret
537 return 'returned error code %d' % ret
537 return 'returned error code %d' % ret
538
538
539 self._skipped = False
539 self._skipped = False
540
540
541 if ret == self.SKIPPED_STATUS:
541 if ret == self.SKIPPED_STATUS:
542 if out is None: # Debug mode, nothing to parse.
542 if out is None: # Debug mode, nothing to parse.
543 missing = ['unknown']
543 missing = ['unknown']
544 failed = None
544 failed = None
545 else:
545 else:
546 missing, failed = TTest.parsehghaveoutput(out)
546 missing, failed = TTest.parsehghaveoutput(out)
547
547
548 if not missing:
548 if not missing:
549 missing = ['irrelevant']
549 missing = ['irrelevant']
550
550
551 if failed:
551 if failed:
552 self.fail('hg have failed checking for %s' % failed[-1])
552 self.fail('hg have failed checking for %s' % failed[-1])
553 else:
553 else:
554 self._skipped = True
554 self._skipped = True
555 raise SkipTest(missing[-1])
555 raise SkipTest(missing[-1])
556 elif ret == 'timeout':
556 elif ret == 'timeout':
557 self.fail('timed out')
557 self.fail('timed out')
558 elif ret is False:
558 elif ret is False:
559 raise WarnTest('no result code from test')
559 raise WarnTest('no result code from test')
560 elif out != self._refout:
560 elif out != self._refout:
561 # Diff generation may rely on written .err file.
561 # Diff generation may rely on written .err file.
562 if (ret != 0 or out != self._refout) and not self._skipped \
562 if (ret != 0 or out != self._refout) and not self._skipped \
563 and not self._debug:
563 and not self._debug:
564 f = open(self.errpath, 'wb')
564 f = open(self.errpath, 'wb')
565 for line in out:
565 for line in out:
566 f.write(line)
566 f.write(line)
567 f.close()
567 f.close()
568
568
569 # The result object handles diff calculation for us.
569 # The result object handles diff calculation for us.
570 if self._result.addOutputMismatch(self, ret, out, self._refout):
570 if self._result.addOutputMismatch(self, ret, out, self._refout):
571 # change was accepted, skip failing
571 # change was accepted, skip failing
572 return
572 return
573
573
574 if ret:
574 if ret:
575 msg = 'output changed and ' + describe(ret)
575 msg = 'output changed and ' + describe(ret)
576 else:
576 else:
577 msg = 'output changed'
577 msg = 'output changed'
578
578
579 self.fail(msg)
579 self.fail(msg)
580 elif ret:
580 elif ret:
581 self.fail(describe(ret))
581 self.fail(describe(ret))
582
582
583 def tearDown(self):
583 def tearDown(self):
584 """Tasks to perform after run()."""
584 """Tasks to perform after run()."""
585 for entry in self._daemonpids:
585 for entry in self._daemonpids:
586 killdaemons(entry)
586 killdaemons(entry)
587 self._daemonpids = []
587 self._daemonpids = []
588
588
589 if not self._keeptmpdir:
589 if not self._keeptmpdir:
590 shutil.rmtree(self._testtmp, True)
590 shutil.rmtree(self._testtmp, True)
591 shutil.rmtree(self._threadtmp, True)
591 shutil.rmtree(self._threadtmp, True)
592
592
593 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
593 if (self._ret != 0 or self._out != self._refout) and not self._skipped \
594 and not self._debug and self._out:
594 and not self._debug and self._out:
595 f = open(self.errpath, 'wb')
595 f = open(self.errpath, 'wb')
596 for line in self._out:
596 for line in self._out:
597 f.write(line)
597 f.write(line)
598 f.close()
598 f.close()
599
599
600 vlog("# Ret was:", self._ret)
600 vlog("# Ret was:", self._ret)
601
601
602 def _run(self, replacements, env):
602 def _run(self, replacements, env):
603 # This should be implemented in child classes to run tests.
603 # This should be implemented in child classes to run tests.
604 raise SkipTest('unknown test type')
604 raise SkipTest('unknown test type')
605
605
606 def abort(self):
606 def abort(self):
607 """Terminate execution of this test."""
607 """Terminate execution of this test."""
608 self._aborted = True
608 self._aborted = True
609
609
610 def _getreplacements(self):
610 def _getreplacements(self):
611 """Obtain a mapping of text replacements to apply to test output.
611 """Obtain a mapping of text replacements to apply to test output.
612
612
613 Test output needs to be normalized so it can be compared to expected
613 Test output needs to be normalized so it can be compared to expected
614 output. This function defines how some of that normalization will
614 output. This function defines how some of that normalization will
615 occur.
615 occur.
616 """
616 """
617 r = [
617 r = [
618 (r':%s\b' % self._startport, ':$HGPORT'),
618 (r':%s\b' % self._startport, ':$HGPORT'),
619 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
619 (r':%s\b' % (self._startport + 1), ':$HGPORT1'),
620 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
620 (r':%s\b' % (self._startport + 2), ':$HGPORT2'),
621 ]
621 ]
622
622
623 if os.name == 'nt':
623 if os.name == 'nt':
624 r.append(
624 r.append(
625 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
625 (''.join(c.isalpha() and '[%s%s]' % (c.lower(), c.upper()) or
626 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
626 c in '/\\' and r'[/\\]' or c.isdigit() and c or '\\' + c
627 for c in self._testtmp), '$TESTTMP'))
627 for c in self._testtmp), '$TESTTMP'))
628 else:
628 else:
629 r.append((re.escape(self._testtmp), '$TESTTMP'))
629 r.append((re.escape(self._testtmp), '$TESTTMP'))
630
630
631 return r
631 return r
632
632
633 def _getenv(self):
633 def _getenv(self):
634 """Obtain environment variables to use during test execution."""
634 """Obtain environment variables to use during test execution."""
635 env = os.environ.copy()
635 env = os.environ.copy()
636 env['TESTTMP'] = self._testtmp
636 env['TESTTMP'] = self._testtmp
637 env['HOME'] = self._testtmp
637 env['HOME'] = self._testtmp
638 env["HGPORT"] = str(self._startport)
638 env["HGPORT"] = str(self._startport)
639 env["HGPORT1"] = str(self._startport + 1)
639 env["HGPORT1"] = str(self._startport + 1)
640 env["HGPORT2"] = str(self._startport + 2)
640 env["HGPORT2"] = str(self._startport + 2)
641 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
641 env["HGRCPATH"] = os.path.join(self._threadtmp, '.hgrc')
642 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
642 env["DAEMON_PIDS"] = os.path.join(self._threadtmp, 'daemon.pids')
643 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
643 env["HGEDITOR"] = sys.executable + ' -c "import sys; sys.exit(0)"'
644 env["HGMERGE"] = "internal:merge"
644 env["HGMERGE"] = "internal:merge"
645 env["HGUSER"] = "test"
645 env["HGUSER"] = "test"
646 env["HGENCODING"] = "ascii"
646 env["HGENCODING"] = "ascii"
647 env["HGENCODINGMODE"] = "strict"
647 env["HGENCODINGMODE"] = "strict"
648
648
649 # Reset some environment variables to well-known values so that
649 # Reset some environment variables to well-known values so that
650 # the tests produce repeatable output.
650 # the tests produce repeatable output.
651 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
651 env['LANG'] = env['LC_ALL'] = env['LANGUAGE'] = 'C'
652 env['TZ'] = 'GMT'
652 env['TZ'] = 'GMT'
653 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
653 env["EMAIL"] = "Foo Bar <foo.bar@example.com>"
654 env['COLUMNS'] = '80'
654 env['COLUMNS'] = '80'
655 env['TERM'] = 'xterm'
655 env['TERM'] = 'xterm'
656
656
657 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
657 for k in ('HG HGPROF CDPATH GREP_OPTIONS http_proxy no_proxy ' +
658 'NO_PROXY').split():
658 'NO_PROXY').split():
659 if k in env:
659 if k in env:
660 del env[k]
660 del env[k]
661
661
662 # unset env related to hooks
662 # unset env related to hooks
663 for k in env.keys():
663 for k in env.keys():
664 if k.startswith('HG_'):
664 if k.startswith('HG_'):
665 del env[k]
665 del env[k]
666
666
667 return env
667 return env
668
668
669 def _createhgrc(self, path):
669 def _createhgrc(self, path):
670 """Create an hgrc file for this test."""
670 """Create an hgrc file for this test."""
671 hgrc = open(path, 'wb')
671 hgrc = open(path, 'wb')
672 hgrc.write('[ui]\n')
672 hgrc.write('[ui]\n')
673 hgrc.write('slash = True\n')
673 hgrc.write('slash = True\n')
674 hgrc.write('interactive = False\n')
674 hgrc.write('interactive = False\n')
675 hgrc.write('mergemarkers = detailed\n')
675 hgrc.write('mergemarkers = detailed\n')
676 hgrc.write('[defaults]\n')
676 hgrc.write('[defaults]\n')
677 hgrc.write('backout = -d "0 0"\n')
677 hgrc.write('backout = -d "0 0"\n')
678 hgrc.write('commit = -d "0 0"\n')
678 hgrc.write('commit = -d "0 0"\n')
679 hgrc.write('shelve = --date "0 0"\n')
679 hgrc.write('shelve = --date "0 0"\n')
680 hgrc.write('tag = -d "0 0"\n')
680 hgrc.write('tag = -d "0 0"\n')
681 for opt in self._extraconfigopts:
681 for opt in self._extraconfigopts:
682 section, key = opt.split('.', 1)
682 section, key = opt.split('.', 1)
683 assert '=' in key, ('extra config opt %s must '
683 assert '=' in key, ('extra config opt %s must '
684 'have an = for assignment' % opt)
684 'have an = for assignment' % opt)
685 hgrc.write('[%s]\n%s\n' % (section, key))
685 hgrc.write('[%s]\n%s\n' % (section, key))
686 hgrc.close()
686 hgrc.close()
687
687
688 def fail(self, msg):
688 def fail(self, msg):
689 # unittest differentiates between errored and failed.
689 # unittest differentiates between errored and failed.
690 # Failed is denoted by AssertionError (by default at least).
690 # Failed is denoted by AssertionError (by default at least).
691 raise AssertionError(msg)
691 raise AssertionError(msg)
692
692
693 class PythonTest(Test):
693 class PythonTest(Test):
694 """A Python-based test."""
694 """A Python-based test."""
695
695
696 @property
696 @property
697 def refpath(self):
697 def refpath(self):
698 return os.path.join(self._testdir, '%s.out' % self.name)
698 return os.path.join(self._testdir, '%s.out' % self.name)
699
699
700 def _run(self, replacements, env):
700 def _run(self, replacements, env):
701 py3kswitch = self._py3kwarnings and ' -3' or ''
701 py3kswitch = self._py3kwarnings and ' -3' or ''
702 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
702 cmd = '%s%s "%s"' % (PYTHON, py3kswitch, self.path)
703 vlog("# Running", cmd)
703 vlog("# Running", cmd)
704 if os.name == 'nt':
704 if os.name == 'nt':
705 replacements.append((r'\r\n', '\n'))
705 replacements.append((r'\r\n', '\n'))
706 result = run(cmd, self._testtmp, replacements, env,
706 result = run(cmd, self._testtmp, replacements, env,
707 debug=self._debug, timeout=self._timeout)
707 debug=self._debug, timeout=self._timeout)
708 if self._aborted:
708 if self._aborted:
709 raise KeyboardInterrupt()
709 raise KeyboardInterrupt()
710
710
711 return result
711 return result
712
712
713 class TTest(Test):
713 class TTest(Test):
714 """A "t test" is a test backed by a .t file."""
714 """A "t test" is a test backed by a .t file."""
715
715
716 SKIPPED_PREFIX = 'skipped: '
716 SKIPPED_PREFIX = 'skipped: '
717 FAILED_PREFIX = 'hghave check failed: '
717 FAILED_PREFIX = 'hghave check failed: '
718 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
718 NEEDESCAPE = re.compile(r'[\x00-\x08\x0b-\x1f\x7f-\xff]').search
719
719
720 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
720 ESCAPESUB = re.compile(r'[\x00-\x08\x0b-\x1f\\\x7f-\xff]').sub
721 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
721 ESCAPEMAP = dict((chr(i), r'\x%02x' % i) for i in range(256))
722 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
722 ESCAPEMAP.update({'\\': '\\\\', '\r': r'\r'})
723
723
724 @property
724 @property
725 def refpath(self):
725 def refpath(self):
726 return os.path.join(self._testdir, self.name)
726 return os.path.join(self._testdir, self.name)
727
727
728 def _run(self, replacements, env):
728 def _run(self, replacements, env):
729 f = open(self.path, 'rb')
729 f = open(self.path, 'rb')
730 lines = f.readlines()
730 lines = f.readlines()
731 f.close()
731 f.close()
732
732
733 salt, script, after, expected = self._parsetest(lines)
733 salt, script, after, expected = self._parsetest(lines)
734
734
735 # Write out the generated script.
735 # Write out the generated script.
736 fname = '%s.sh' % self._testtmp
736 fname = '%s.sh' % self._testtmp
737 f = open(fname, 'wb')
737 f = open(fname, 'wb')
738 for l in script:
738 for l in script:
739 f.write(l)
739 f.write(l)
740 f.close()
740 f.close()
741
741
742 cmd = '%s "%s"' % (self._shell, fname)
742 cmd = '%s "%s"' % (self._shell, fname)
743 vlog("# Running", cmd)
743 vlog("# Running", cmd)
744
744
745 exitcode, output = run(cmd, self._testtmp, replacements, env,
745 exitcode, output = run(cmd, self._testtmp, replacements, env,
746 debug=self._debug, timeout=self._timeout)
746 debug=self._debug, timeout=self._timeout)
747
747
748 if self._aborted:
748 if self._aborted:
749 raise KeyboardInterrupt()
749 raise KeyboardInterrupt()
750
750
751 # Do not merge output if skipped. Return hghave message instead.
751 # Do not merge output if skipped. Return hghave message instead.
752 # Similarly, with --debug, output is None.
752 # Similarly, with --debug, output is None.
753 if exitcode == self.SKIPPED_STATUS or output is None:
753 if exitcode == self.SKIPPED_STATUS or output is None:
754 return exitcode, output
754 return exitcode, output
755
755
756 return self._processoutput(exitcode, output, salt, after, expected)
756 return self._processoutput(exitcode, output, salt, after, expected)
757
757
758 def _hghave(self, reqs):
758 def _hghave(self, reqs):
759 # TODO do something smarter when all other uses of hghave are gone.
759 # TODO do something smarter when all other uses of hghave are gone.
760 tdir = self._testdir.replace('\\', '/')
760 tdir = self._testdir.replace('\\', '/')
761 proc = Popen4('%s -c "%s/hghave %s"' %
761 proc = Popen4('%s -c "%s/hghave %s"' %
762 (self._shell, tdir, ' '.join(reqs)),
762 (self._shell, tdir, ' '.join(reqs)),
763 self._testtmp, 0)
763 self._testtmp, 0)
764 stdout, stderr = proc.communicate()
764 stdout, stderr = proc.communicate()
765 ret = proc.wait()
765 ret = proc.wait()
766 if wifexited(ret):
766 if wifexited(ret):
767 ret = os.WEXITSTATUS(ret)
767 ret = os.WEXITSTATUS(ret)
768 if ret == 2:
768 if ret == 2:
769 print stdout
769 print stdout
770 sys.exit(1)
770 sys.exit(1)
771
771
772 return ret == 0
772 return ret == 0
773
773
774 def _parsetest(self, lines):
774 def _parsetest(self, lines):
775 # We generate a shell script which outputs unique markers to line
775 # We generate a shell script which outputs unique markers to line
776 # up script results with our source. These markers include input
776 # up script results with our source. These markers include input
777 # line number and the last return code.
777 # line number and the last return code.
778 salt = "SALT" + str(time.time())
778 salt = "SALT" + str(time.time())
779 def addsalt(line, inpython):
779 def addsalt(line, inpython):
780 if inpython:
780 if inpython:
781 script.append('%s %d 0\n' % (salt, line))
781 script.append('%s %d 0\n' % (salt, line))
782 else:
782 else:
783 script.append('echo %s %s $?\n' % (salt, line))
783 script.append('echo %s %s $?\n' % (salt, line))
784
784
785 script = []
785 script = []
786
786
787 # After we run the shell script, we re-unify the script output
787 # After we run the shell script, we re-unify the script output
788 # with non-active parts of the source, with synchronization by our
788 # with non-active parts of the source, with synchronization by our
789 # SALT line number markers. The after table contains the non-active
789 # SALT line number markers. The after table contains the non-active
790 # components, ordered by line number.
790 # components, ordered by line number.
791 after = {}
791 after = {}
792
792
793 # Expected shell script output.
793 # Expected shell script output.
794 expected = {}
794 expected = {}
795
795
796 pos = prepos = -1
796 pos = prepos = -1
797
797
798 # True or False when in a true or false conditional section
798 # True or False when in a true or false conditional section
799 skipping = None
799 skipping = None
800
800
801 # We keep track of whether or not we're in a Python block so we
801 # We keep track of whether or not we're in a Python block so we
802 # can generate the surrounding doctest magic.
802 # can generate the surrounding doctest magic.
803 inpython = False
803 inpython = False
804
804
805 if self._debug:
805 if self._debug:
806 script.append('set -x\n')
806 script.append('set -x\n')
807 if os.getenv('MSYSTEM'):
807 if os.getenv('MSYSTEM'):
808 script.append('alias pwd="pwd -W"\n')
808 script.append('alias pwd="pwd -W"\n')
809
809
810 for n, l in enumerate(lines):
810 for n, l in enumerate(lines):
811 if not l.endswith('\n'):
811 if not l.endswith('\n'):
812 l += '\n'
812 l += '\n'
813 if l.startswith('#require'):
813 if l.startswith('#require'):
814 lsplit = l.split()
814 lsplit = l.split()
815 if len(lsplit) < 2 or lsplit[0] != '#require':
815 if len(lsplit) < 2 or lsplit[0] != '#require':
816 after.setdefault(pos, []).append(' !!! invalid #require\n')
816 after.setdefault(pos, []).append(' !!! invalid #require\n')
817 if not self._hghave(lsplit[1:]):
817 if not self._hghave(lsplit[1:]):
818 script = ["exit 80\n"]
818 script = ["exit 80\n"]
819 break
819 break
820 after.setdefault(pos, []).append(l)
820 after.setdefault(pos, []).append(l)
821 elif l.startswith('#if'):
821 elif l.startswith('#if'):
822 lsplit = l.split()
822 lsplit = l.split()
823 if len(lsplit) < 2 or lsplit[0] != '#if':
823 if len(lsplit) < 2 or lsplit[0] != '#if':
824 after.setdefault(pos, []).append(' !!! invalid #if\n')
824 after.setdefault(pos, []).append(' !!! invalid #if\n')
825 if skipping is not None:
825 if skipping is not None:
826 after.setdefault(pos, []).append(' !!! nested #if\n')
826 after.setdefault(pos, []).append(' !!! nested #if\n')
827 skipping = not self._hghave(lsplit[1:])
827 skipping = not self._hghave(lsplit[1:])
828 after.setdefault(pos, []).append(l)
828 after.setdefault(pos, []).append(l)
829 elif l.startswith('#else'):
829 elif l.startswith('#else'):
830 if skipping is None:
830 if skipping is None:
831 after.setdefault(pos, []).append(' !!! missing #if\n')
831 after.setdefault(pos, []).append(' !!! missing #if\n')
832 skipping = not skipping
832 skipping = not skipping
833 after.setdefault(pos, []).append(l)
833 after.setdefault(pos, []).append(l)
834 elif l.startswith('#endif'):
834 elif l.startswith('#endif'):
835 if skipping is None:
835 if skipping is None:
836 after.setdefault(pos, []).append(' !!! missing #if\n')
836 after.setdefault(pos, []).append(' !!! missing #if\n')
837 skipping = None
837 skipping = None
838 after.setdefault(pos, []).append(l)
838 after.setdefault(pos, []).append(l)
839 elif skipping:
839 elif skipping:
840 after.setdefault(pos, []).append(l)
840 after.setdefault(pos, []).append(l)
841 elif l.startswith(' >>> '): # python inlines
841 elif l.startswith(' >>> '): # python inlines
842 after.setdefault(pos, []).append(l)
842 after.setdefault(pos, []).append(l)
843 prepos = pos
843 prepos = pos
844 pos = n
844 pos = n
845 if not inpython:
845 if not inpython:
846 # We've just entered a Python block. Add the header.
846 # We've just entered a Python block. Add the header.
847 inpython = True
847 inpython = True
848 addsalt(prepos, False) # Make sure we report the exit code.
848 addsalt(prepos, False) # Make sure we report the exit code.
849 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
849 script.append('%s -m heredoctest <<EOF\n' % PYTHON)
850 addsalt(n, True)
850 addsalt(n, True)
851 script.append(l[2:])
851 script.append(l[2:])
852 elif l.startswith(' ... '): # python inlines
852 elif l.startswith(' ... '): # python inlines
853 after.setdefault(prepos, []).append(l)
853 after.setdefault(prepos, []).append(l)
854 script.append(l[2:])
854 script.append(l[2:])
855 elif l.startswith(' $ '): # commands
855 elif l.startswith(' $ '): # commands
856 if inpython:
856 if inpython:
857 script.append('EOF\n')
857 script.append('EOF\n')
858 inpython = False
858 inpython = False
859 after.setdefault(pos, []).append(l)
859 after.setdefault(pos, []).append(l)
860 prepos = pos
860 prepos = pos
861 pos = n
861 pos = n
862 addsalt(n, False)
862 addsalt(n, False)
863 cmd = l[4:].split()
863 cmd = l[4:].split()
864 if len(cmd) == 2 and cmd[0] == 'cd':
864 if len(cmd) == 2 and cmd[0] == 'cd':
865 l = ' $ cd %s || exit 1\n' % cmd[1]
865 l = ' $ cd %s || exit 1\n' % cmd[1]
866 script.append(l[4:])
866 script.append(l[4:])
867 elif l.startswith(' > '): # continuations
867 elif l.startswith(' > '): # continuations
868 after.setdefault(prepos, []).append(l)
868 after.setdefault(prepos, []).append(l)
869 script.append(l[4:])
869 script.append(l[4:])
870 elif l.startswith(' '): # results
870 elif l.startswith(' '): # results
871 # Queue up a list of expected results.
871 # Queue up a list of expected results.
872 expected.setdefault(pos, []).append(l[2:])
872 expected.setdefault(pos, []).append(l[2:])
873 else:
873 else:
874 if inpython:
874 if inpython:
875 script.append('EOF\n')
875 script.append('EOF\n')
876 inpython = False
876 inpython = False
877 # Non-command/result. Queue up for merged output.
877 # Non-command/result. Queue up for merged output.
878 after.setdefault(pos, []).append(l)
878 after.setdefault(pos, []).append(l)
879
879
880 if inpython:
880 if inpython:
881 script.append('EOF\n')
881 script.append('EOF\n')
882 if skipping is not None:
882 if skipping is not None:
883 after.setdefault(pos, []).append(' !!! missing #endif\n')
883 after.setdefault(pos, []).append(' !!! missing #endif\n')
884 addsalt(n + 1, False)
884 addsalt(n + 1, False)
885
885
886 return salt, script, after, expected
886 return salt, script, after, expected
887
887
888 def _processoutput(self, exitcode, output, salt, after, expected):
888 def _processoutput(self, exitcode, output, salt, after, expected):
889 # Merge the script output back into a unified test.
889 # Merge the script output back into a unified test.
890 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
890 warnonly = 1 # 1: not yet; 2: yes; 3: for sure not
891 if exitcode != 0:
891 if exitcode != 0:
892 warnonly = 3
892 warnonly = 3
893
893
894 pos = -1
894 pos = -1
895 postout = []
895 postout = []
896 for l in output:
896 for l in output:
897 lout, lcmd = l, None
897 lout, lcmd = l, None
898 if salt in l:
898 if salt in l:
899 lout, lcmd = l.split(salt, 1)
899 lout, lcmd = l.split(salt, 1)
900
900
901 if lout:
901 if lout:
902 if not lout.endswith('\n'):
902 if not lout.endswith('\n'):
903 lout += ' (no-eol)\n'
903 lout += ' (no-eol)\n'
904
904
905 # Find the expected output at the current position.
905 # Find the expected output at the current position.
906 el = None
906 el = None
907 if expected.get(pos, None):
907 if expected.get(pos, None):
908 el = expected[pos].pop(0)
908 el = expected[pos].pop(0)
909
909
910 r = TTest.linematch(el, lout)
910 r = TTest.linematch(el, lout)
911 if isinstance(r, str):
911 if isinstance(r, str):
912 if r == '+glob':
912 if r == '+glob':
913 lout = el[:-1] + ' (glob)\n'
913 lout = el[:-1] + ' (glob)\n'
914 r = '' # Warn only this line.
914 r = '' # Warn only this line.
915 elif r == '-glob':
915 elif r == '-glob':
916 lout = ''.join(el.rsplit(' (glob)', 1))
916 lout = ''.join(el.rsplit(' (glob)', 1))
917 r = '' # Warn only this line.
917 r = '' # Warn only this line.
918 else:
918 else:
919 log('\ninfo, unknown linematch result: %r\n' % r)
919 log('\ninfo, unknown linematch result: %r\n' % r)
920 r = False
920 r = False
921 if r:
921 if r:
922 postout.append(' ' + el)
922 postout.append(' ' + el)
923 else:
923 else:
924 if self.NEEDESCAPE(lout):
924 if self.NEEDESCAPE(lout):
925 lout = TTest._stringescape('%s (esc)\n' %
925 lout = TTest._stringescape('%s (esc)\n' %
926 lout.rstrip('\n'))
926 lout.rstrip('\n'))
927 postout.append(' ' + lout) # Let diff deal with it.
927 postout.append(' ' + lout) # Let diff deal with it.
928 if r != '': # If line failed.
928 if r != '': # If line failed.
929 warnonly = 3 # for sure not
929 warnonly = 3 # for sure not
930 elif warnonly == 1: # Is "not yet" and line is warn only.
930 elif warnonly == 1: # Is "not yet" and line is warn only.
931 warnonly = 2 # Yes do warn.
931 warnonly = 2 # Yes do warn.
932
932
933 if lcmd:
933 if lcmd:
934 # Add on last return code.
934 # Add on last return code.
935 ret = int(lcmd.split()[1])
935 ret = int(lcmd.split()[1])
936 if ret != 0:
936 if ret != 0:
937 postout.append(' [%s]\n' % ret)
937 postout.append(' [%s]\n' % ret)
938 if pos in after:
938 if pos in after:
939 # Merge in non-active test bits.
939 # Merge in non-active test bits.
940 postout += after.pop(pos)
940 postout += after.pop(pos)
941 pos = int(lcmd.split()[0])
941 pos = int(lcmd.split()[0])
942
942
943 if pos in after:
943 if pos in after:
944 postout += after.pop(pos)
944 postout += after.pop(pos)
945
945
946 if warnonly == 2:
946 if warnonly == 2:
947 exitcode = False # Set exitcode to warned.
947 exitcode = False # Set exitcode to warned.
948
948
949 return exitcode, postout
949 return exitcode, postout
950
950
951 @staticmethod
951 @staticmethod
952 def rematch(el, l):
952 def rematch(el, l):
953 try:
953 try:
954 # use \Z to ensure that the regex matches to the end of the string
954 # use \Z to ensure that the regex matches to the end of the string
955 if os.name == 'nt':
955 if os.name == 'nt':
956 return re.match(el + r'\r?\n\Z', l)
956 return re.match(el + r'\r?\n\Z', l)
957 return re.match(el + r'\n\Z', l)
957 return re.match(el + r'\n\Z', l)
958 except re.error:
958 except re.error:
959 # el is an invalid regex
959 # el is an invalid regex
960 return False
960 return False
961
961
962 @staticmethod
962 @staticmethod
963 def globmatch(el, l):
963 def globmatch(el, l):
964 # The only supported special characters are * and ? plus / which also
964 # The only supported special characters are * and ? plus / which also
965 # matches \ on windows. Escaping of these characters is supported.
965 # matches \ on windows. Escaping of these characters is supported.
966 if el + '\n' == l:
966 if el + '\n' == l:
967 if os.altsep:
967 if os.altsep:
968 # matching on "/" is not needed for this line
968 # matching on "/" is not needed for this line
969 return '-glob'
969 return '-glob'
970 return True
970 return True
971 i, n = 0, len(el)
971 i, n = 0, len(el)
972 res = ''
972 res = ''
973 while i < n:
973 while i < n:
974 c = el[i]
974 c = el[i]
975 i += 1
975 i += 1
976 if c == '\\' and el[i] in '*?\\/':
976 if c == '\\' and el[i] in '*?\\/':
977 res += el[i - 1:i + 1]
977 res += el[i - 1:i + 1]
978 i += 1
978 i += 1
979 elif c == '*':
979 elif c == '*':
980 res += '.*'
980 res += '.*'
981 elif c == '?':
981 elif c == '?':
982 res += '.'
982 res += '.'
983 elif c == '/' and os.altsep:
983 elif c == '/' and os.altsep:
984 res += '[/\\\\]'
984 res += '[/\\\\]'
985 else:
985 else:
986 res += re.escape(c)
986 res += re.escape(c)
987 return TTest.rematch(res, l)
987 return TTest.rematch(res, l)
988
988
989 @staticmethod
989 @staticmethod
990 def linematch(el, l):
990 def linematch(el, l):
991 if el == l: # perfect match (fast)
991 if el == l: # perfect match (fast)
992 return True
992 return True
993 if el:
993 if el:
994 if el.endswith(" (esc)\n"):
994 if el.endswith(" (esc)\n"):
995 el = el[:-7].decode('string-escape') + '\n'
995 el = el[:-7].decode('string-escape') + '\n'
996 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
996 if el == l or os.name == 'nt' and el[:-1] + '\r\n' == l:
997 return True
997 return True
998 if el.endswith(" (re)\n"):
998 if el.endswith(" (re)\n"):
999 return TTest.rematch(el[:-6], l)
999 return TTest.rematch(el[:-6], l)
1000 if el.endswith(" (glob)\n"):
1000 if el.endswith(" (glob)\n"):
1001 return TTest.globmatch(el[:-8], l)
1001 return TTest.globmatch(el[:-8], l)
1002 if os.altsep and l.replace('\\', '/') == el:
1002 if os.altsep and l.replace('\\', '/') == el:
1003 return '+glob'
1003 return '+glob'
1004 return False
1004 return False
1005
1005
1006 @staticmethod
1006 @staticmethod
1007 def parsehghaveoutput(lines):
1007 def parsehghaveoutput(lines):
1008 '''Parse hghave log lines.
1008 '''Parse hghave log lines.
1009
1009
1010 Return tuple of lists (missing, failed):
1010 Return tuple of lists (missing, failed):
1011 * the missing/unknown features
1011 * the missing/unknown features
1012 * the features for which existence check failed'''
1012 * the features for which existence check failed'''
1013 missing = []
1013 missing = []
1014 failed = []
1014 failed = []
1015 for line in lines:
1015 for line in lines:
1016 if line.startswith(TTest.SKIPPED_PREFIX):
1016 if line.startswith(TTest.SKIPPED_PREFIX):
1017 line = line.splitlines()[0]
1017 line = line.splitlines()[0]
1018 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1018 missing.append(line[len(TTest.SKIPPED_PREFIX):])
1019 elif line.startswith(TTest.FAILED_PREFIX):
1019 elif line.startswith(TTest.FAILED_PREFIX):
1020 line = line.splitlines()[0]
1020 line = line.splitlines()[0]
1021 failed.append(line[len(TTest.FAILED_PREFIX):])
1021 failed.append(line[len(TTest.FAILED_PREFIX):])
1022
1022
1023 return missing, failed
1023 return missing, failed
1024
1024
1025 @staticmethod
1025 @staticmethod
1026 def _escapef(m):
1026 def _escapef(m):
1027 return TTest.ESCAPEMAP[m.group(0)]
1027 return TTest.ESCAPEMAP[m.group(0)]
1028
1028
1029 @staticmethod
1029 @staticmethod
1030 def _stringescape(s):
1030 def _stringescape(s):
1031 return TTest.ESCAPESUB(TTest._escapef, s)
1031 return TTest.ESCAPESUB(TTest._escapef, s)
1032
1032
1033
1033
1034 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1034 wifexited = getattr(os, "WIFEXITED", lambda x: False)
1035 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1035 def run(cmd, wd, replacements, env, debug=False, timeout=None):
1036 """Run command in a sub-process, capturing the output (stdout and stderr).
1036 """Run command in a sub-process, capturing the output (stdout and stderr).
1037 Return a tuple (exitcode, output). output is None in debug mode."""
1037 Return a tuple (exitcode, output). output is None in debug mode."""
1038 if debug:
1038 if debug:
1039 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1039 proc = subprocess.Popen(cmd, shell=True, cwd=wd, env=env)
1040 ret = proc.wait()
1040 ret = proc.wait()
1041 return (ret, None)
1041 return (ret, None)
1042
1042
1043 proc = Popen4(cmd, wd, timeout, env)
1043 proc = Popen4(cmd, wd, timeout, env)
1044 def cleanup():
1044 def cleanup():
1045 terminate(proc)
1045 terminate(proc)
1046 ret = proc.wait()
1046 ret = proc.wait()
1047 if ret == 0:
1047 if ret == 0:
1048 ret = signal.SIGTERM << 8
1048 ret = signal.SIGTERM << 8
1049 killdaemons(env['DAEMON_PIDS'])
1049 killdaemons(env['DAEMON_PIDS'])
1050 return ret
1050 return ret
1051
1051
1052 output = ''
1052 output = ''
1053 proc.tochild.close()
1053 proc.tochild.close()
1054
1054
1055 try:
1055 try:
1056 output = proc.fromchild.read()
1056 output = proc.fromchild.read()
1057 except KeyboardInterrupt:
1057 except KeyboardInterrupt:
1058 vlog('# Handling keyboard interrupt')
1058 vlog('# Handling keyboard interrupt')
1059 cleanup()
1059 cleanup()
1060 raise
1060 raise
1061
1061
1062 ret = proc.wait()
1062 ret = proc.wait()
1063 if wifexited(ret):
1063 if wifexited(ret):
1064 ret = os.WEXITSTATUS(ret)
1064 ret = os.WEXITSTATUS(ret)
1065
1065
1066 if proc.timeout:
1066 if proc.timeout:
1067 ret = 'timeout'
1067 ret = 'timeout'
1068
1068
1069 if ret:
1069 if ret:
1070 killdaemons(env['DAEMON_PIDS'])
1070 killdaemons(env['DAEMON_PIDS'])
1071
1071
1072 for s, r in replacements:
1072 for s, r in replacements:
1073 output = re.sub(s, r, output)
1073 output = re.sub(s, r, output)
1074 return ret, output.splitlines(True)
1074 return ret, output.splitlines(True)
1075
1075
1076 iolock = threading.RLock()
1076 iolock = threading.RLock()
1077
1077
1078 class SkipTest(Exception):
1078 class SkipTest(Exception):
1079 """Raised to indicate that a test is to be skipped."""
1079 """Raised to indicate that a test is to be skipped."""
1080
1080
1081 class IgnoreTest(Exception):
1081 class IgnoreTest(Exception):
1082 """Raised to indicate that a test is to be ignored."""
1082 """Raised to indicate that a test is to be ignored."""
1083
1083
1084 class WarnTest(Exception):
1084 class WarnTest(Exception):
1085 """Raised to indicate that a test warned."""
1085 """Raised to indicate that a test warned."""
1086
1086
1087 class TestResult(unittest._TextTestResult):
1087 class TestResult(unittest._TextTestResult):
1088 """Holds results when executing via unittest."""
1088 """Holds results when executing via unittest."""
1089 # Don't worry too much about accessing the non-public _TextTestResult.
1089 # Don't worry too much about accessing the non-public _TextTestResult.
1090 # It is relatively common in Python testing tools.
1090 # It is relatively common in Python testing tools.
1091 def __init__(self, options, *args, **kwargs):
1091 def __init__(self, options, *args, **kwargs):
1092 super(TestResult, self).__init__(*args, **kwargs)
1092 super(TestResult, self).__init__(*args, **kwargs)
1093
1093
1094 self._options = options
1094 self._options = options
1095
1095
1096 # unittest.TestResult didn't have skipped until 2.7. We need to
1096 # unittest.TestResult didn't have skipped until 2.7. We need to
1097 # polyfill it.
1097 # polyfill it.
1098 self.skipped = []
1098 self.skipped = []
1099
1099
1100 # We have a custom "ignored" result that isn't present in any Python
1100 # We have a custom "ignored" result that isn't present in any Python
1101 # unittest implementation. It is very similar to skipped. It may make
1101 # unittest implementation. It is very similar to skipped. It may make
1102 # sense to map it into skip some day.
1102 # sense to map it into skip some day.
1103 self.ignored = []
1103 self.ignored = []
1104
1104
1105 # We have a custom "warned" result that isn't present in any Python
1105 # We have a custom "warned" result that isn't present in any Python
1106 # unittest implementation. It is very similar to failed. It may make
1106 # unittest implementation. It is very similar to failed. It may make
1107 # sense to map it into fail some day.
1107 # sense to map it into fail some day.
1108 self.warned = []
1108 self.warned = []
1109
1109
1110 self.times = []
1110 self.times = []
1111 self._started = {}
1111 self._started = {}
1112 self._stopped = {}
1112 self._stopped = {}
1113 # Data stored for the benefit of generating xunit reports.
1113 # Data stored for the benefit of generating xunit reports.
1114 self.successes = []
1114 self.successes = []
1115 self.faildata = {}
1115 self.faildata = {}
1116
1116
1117 def addFailure(self, test, reason):
1117 def addFailure(self, test, reason):
1118 self.failures.append((test, reason))
1118 self.failures.append((test, reason))
1119
1119
1120 if self._options.first:
1120 if self._options.first:
1121 self.stop()
1121 self.stop()
1122 else:
1122 else:
1123 iolock.acquire()
1123 iolock.acquire()
1124 if not self._options.nodiff:
1124 if not self._options.nodiff:
1125 self.stream.write('\nERROR: %s output changed\n' % test)
1125 self.stream.write('\nERROR: %s output changed\n' % test)
1126
1126
1127 self.stream.write('!')
1127 self.stream.write('!')
1128 self.stream.flush()
1128 iolock.release()
1129 iolock.release()
1129
1130
1130 def addSuccess(self, test):
1131 def addSuccess(self, test):
1131 iolock.acquire()
1132 iolock.acquire()
1132 super(TestResult, self).addSuccess(test)
1133 super(TestResult, self).addSuccess(test)
1133 iolock.release()
1134 iolock.release()
1134 self.successes.append(test)
1135 self.successes.append(test)
1135
1136
1136 def addError(self, test, err):
1137 def addError(self, test, err):
1137 super(TestResult, self).addError(test, err)
1138 super(TestResult, self).addError(test, err)
1138 if self._options.first:
1139 if self._options.first:
1139 self.stop()
1140 self.stop()
1140
1141
1141 # Polyfill.
1142 # Polyfill.
1142 def addSkip(self, test, reason):
1143 def addSkip(self, test, reason):
1143 self.skipped.append((test, reason))
1144 self.skipped.append((test, reason))
1144 iolock.acquire()
1145 iolock.acquire()
1145 if self.showAll:
1146 if self.showAll:
1146 self.stream.writeln('skipped %s' % reason)
1147 self.stream.writeln('skipped %s' % reason)
1147 else:
1148 else:
1148 self.stream.write('s')
1149 self.stream.write('s')
1149 self.stream.flush()
1150 self.stream.flush()
1150 iolock.release()
1151 iolock.release()
1151
1152
1152 def addIgnore(self, test, reason):
1153 def addIgnore(self, test, reason):
1153 self.ignored.append((test, reason))
1154 self.ignored.append((test, reason))
1154 iolock.acquire()
1155 iolock.acquire()
1155 if self.showAll:
1156 if self.showAll:
1156 self.stream.writeln('ignored %s' % reason)
1157 self.stream.writeln('ignored %s' % reason)
1157 else:
1158 else:
1158 if reason != 'not retesting' and reason != "doesn't match keyword":
1159 if reason != 'not retesting' and reason != "doesn't match keyword":
1159 self.stream.write('i')
1160 self.stream.write('i')
1160 else:
1161 else:
1161 self.testsRun += 1
1162 self.testsRun += 1
1162 self.stream.flush()
1163 self.stream.flush()
1163 iolock.release()
1164 iolock.release()
1164
1165
1165 def addWarn(self, test, reason):
1166 def addWarn(self, test, reason):
1166 self.warned.append((test, reason))
1167 self.warned.append((test, reason))
1167
1168
1168 if self._options.first:
1169 if self._options.first:
1169 self.stop()
1170 self.stop()
1170
1171
1171 iolock.acquire()
1172 iolock.acquire()
1172 if self.showAll:
1173 if self.showAll:
1173 self.stream.writeln('warned %s' % reason)
1174 self.stream.writeln('warned %s' % reason)
1174 else:
1175 else:
1175 self.stream.write('~')
1176 self.stream.write('~')
1176 self.stream.flush()
1177 self.stream.flush()
1177 iolock.release()
1178 iolock.release()
1178
1179
1179 def addOutputMismatch(self, test, ret, got, expected):
1180 def addOutputMismatch(self, test, ret, got, expected):
1180 """Record a mismatch in test output for a particular test."""
1181 """Record a mismatch in test output for a particular test."""
1181
1182
1182 accepted = False
1183 accepted = False
1183 failed = False
1184 failed = False
1184 lines = []
1185 lines = []
1185
1186
1186 iolock.acquire()
1187 iolock.acquire()
1187 if self._options.nodiff:
1188 if self._options.nodiff:
1188 pass
1189 pass
1189 elif self._options.view:
1190 elif self._options.view:
1190 os.system("%s %s %s" %
1191 os.system("%s %s %s" %
1191 (self._options.view, test.refpath, test.errpath))
1192 (self._options.view, test.refpath, test.errpath))
1192 else:
1193 else:
1193 failed, lines = getdiff(expected, got,
1194 failed, lines = getdiff(expected, got,
1194 test.refpath, test.errpath)
1195 test.refpath, test.errpath)
1195 if failed:
1196 if failed:
1196 self.addFailure(test, 'diff generation failed')
1197 self.addFailure(test, 'diff generation failed')
1197 else:
1198 else:
1198 self.stream.write('\n')
1199 self.stream.write('\n')
1199 for line in lines:
1200 for line in lines:
1200 self.stream.write(line)
1201 self.stream.write(line)
1201 self.stream.flush()
1202 self.stream.flush()
1202
1203
1203 # handle interactive prompt without releasing iolock
1204 # handle interactive prompt without releasing iolock
1204 if self._options.interactive:
1205 if self._options.interactive:
1205 self.stream.write('Accept this change? [n] ')
1206 self.stream.write('Accept this change? [n] ')
1206 answer = sys.stdin.readline().strip()
1207 answer = sys.stdin.readline().strip()
1207 if answer.lower() in ('y', 'yes'):
1208 if answer.lower() in ('y', 'yes'):
1208 if test.name.endswith('.t'):
1209 if test.name.endswith('.t'):
1209 rename(test.errpath, test.path)
1210 rename(test.errpath, test.path)
1210 else:
1211 else:
1211 rename(test.errpath, '%s.out' % test.path)
1212 rename(test.errpath, '%s.out' % test.path)
1212 accepted = True
1213 accepted = True
1213 if not accepted and not failed:
1214 if not accepted and not failed:
1214 self.faildata[test.name] = ''.join(lines)
1215 self.faildata[test.name] = ''.join(lines)
1215 iolock.release()
1216 iolock.release()
1216
1217
1217 return accepted
1218 return accepted
1218
1219
1219 def startTest(self, test):
1220 def startTest(self, test):
1220 super(TestResult, self).startTest(test)
1221 super(TestResult, self).startTest(test)
1221
1222
1222 # os.times module computes the user time and system time spent by
1223 # os.times module computes the user time and system time spent by
1223 # child's processes along with real elapsed time taken by a process.
1224 # child's processes along with real elapsed time taken by a process.
1224 # This module has one limitation. It can only work for Linux user
1225 # This module has one limitation. It can only work for Linux user
1225 # and not for Windows.
1226 # and not for Windows.
1226 self._started[test.name] = os.times()
1227 self._started[test.name] = os.times()
1227
1228
1228 def stopTest(self, test, interrupted=False):
1229 def stopTest(self, test, interrupted=False):
1229 super(TestResult, self).stopTest(test)
1230 super(TestResult, self).stopTest(test)
1230
1231
1231 self._stopped[test.name] = os.times()
1232 self._stopped[test.name] = os.times()
1232
1233
1233 starttime = self._started[test.name]
1234 starttime = self._started[test.name]
1234 endtime = self._stopped[test.name]
1235 endtime = self._stopped[test.name]
1235 self.times.append((test.name, endtime[2] - starttime[2],
1236 self.times.append((test.name, endtime[2] - starttime[2],
1236 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1237 endtime[3] - starttime[3], endtime[4] - starttime[4]))
1237
1238
1238 del self._started[test.name]
1239 del self._started[test.name]
1239 del self._stopped[test.name]
1240 del self._stopped[test.name]
1240
1241
1241 if interrupted:
1242 if interrupted:
1242 iolock.acquire()
1243 iolock.acquire()
1243 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1244 self.stream.writeln('INTERRUPTED: %s (after %d seconds)' % (
1244 test.name, self.times[-1][3]))
1245 test.name, self.times[-1][3]))
1245 iolock.release()
1246 iolock.release()
1246
1247
1247 class TestSuite(unittest.TestSuite):
1248 class TestSuite(unittest.TestSuite):
1248 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1249 """Custom unitest TestSuite that knows how to execute Mercurial tests."""
1249
1250
1250 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1251 def __init__(self, testdir, jobs=1, whitelist=None, blacklist=None,
1251 retest=False, keywords=None, loop=False,
1252 retest=False, keywords=None, loop=False,
1252 *args, **kwargs):
1253 *args, **kwargs):
1253 """Create a new instance that can run tests with a configuration.
1254 """Create a new instance that can run tests with a configuration.
1254
1255
1255 testdir specifies the directory where tests are executed from. This
1256 testdir specifies the directory where tests are executed from. This
1256 is typically the ``tests`` directory from Mercurial's source
1257 is typically the ``tests`` directory from Mercurial's source
1257 repository.
1258 repository.
1258
1259
1259 jobs specifies the number of jobs to run concurrently. Each test
1260 jobs specifies the number of jobs to run concurrently. Each test
1260 executes on its own thread. Tests actually spawn new processes, so
1261 executes on its own thread. Tests actually spawn new processes, so
1261 state mutation should not be an issue.
1262 state mutation should not be an issue.
1262
1263
1263 whitelist and blacklist denote tests that have been whitelisted and
1264 whitelist and blacklist denote tests that have been whitelisted and
1264 blacklisted, respectively. These arguments don't belong in TestSuite.
1265 blacklisted, respectively. These arguments don't belong in TestSuite.
1265 Instead, whitelist and blacklist should be handled by the thing that
1266 Instead, whitelist and blacklist should be handled by the thing that
1266 populates the TestSuite with tests. They are present to preserve
1267 populates the TestSuite with tests. They are present to preserve
1267 backwards compatible behavior which reports skipped tests as part
1268 backwards compatible behavior which reports skipped tests as part
1268 of the results.
1269 of the results.
1269
1270
1270 retest denotes whether to retest failed tests. This arguably belongs
1271 retest denotes whether to retest failed tests. This arguably belongs
1271 outside of TestSuite.
1272 outside of TestSuite.
1272
1273
1273 keywords denotes key words that will be used to filter which tests
1274 keywords denotes key words that will be used to filter which tests
1274 to execute. This arguably belongs outside of TestSuite.
1275 to execute. This arguably belongs outside of TestSuite.
1275
1276
1276 loop denotes whether to loop over tests forever.
1277 loop denotes whether to loop over tests forever.
1277 """
1278 """
1278 super(TestSuite, self).__init__(*args, **kwargs)
1279 super(TestSuite, self).__init__(*args, **kwargs)
1279
1280
1280 self._jobs = jobs
1281 self._jobs = jobs
1281 self._whitelist = whitelist
1282 self._whitelist = whitelist
1282 self._blacklist = blacklist
1283 self._blacklist = blacklist
1283 self._retest = retest
1284 self._retest = retest
1284 self._keywords = keywords
1285 self._keywords = keywords
1285 self._loop = loop
1286 self._loop = loop
1286
1287
1287 def run(self, result):
1288 def run(self, result):
1288 # We have a number of filters that need to be applied. We do this
1289 # We have a number of filters that need to be applied. We do this
1289 # here instead of inside Test because it makes the running logic for
1290 # here instead of inside Test because it makes the running logic for
1290 # Test simpler.
1291 # Test simpler.
1291 tests = []
1292 tests = []
1292 for test in self._tests:
1293 for test in self._tests:
1293 if not os.path.exists(test.path):
1294 if not os.path.exists(test.path):
1294 result.addSkip(test, "Doesn't exist")
1295 result.addSkip(test, "Doesn't exist")
1295 continue
1296 continue
1296
1297
1297 if not (self._whitelist and test.name in self._whitelist):
1298 if not (self._whitelist and test.name in self._whitelist):
1298 if self._blacklist and test.name in self._blacklist:
1299 if self._blacklist and test.name in self._blacklist:
1299 result.addSkip(test, 'blacklisted')
1300 result.addSkip(test, 'blacklisted')
1300 continue
1301 continue
1301
1302
1302 if self._retest and not os.path.exists(test.errpath):
1303 if self._retest and not os.path.exists(test.errpath):
1303 result.addIgnore(test, 'not retesting')
1304 result.addIgnore(test, 'not retesting')
1304 continue
1305 continue
1305
1306
1306 if self._keywords:
1307 if self._keywords:
1307 f = open(test.path, 'rb')
1308 f = open(test.path, 'rb')
1308 t = f.read().lower() + test.name.lower()
1309 t = f.read().lower() + test.name.lower()
1309 f.close()
1310 f.close()
1310 ignored = False
1311 ignored = False
1311 for k in self._keywords.lower().split():
1312 for k in self._keywords.lower().split():
1312 if k not in t:
1313 if k not in t:
1313 result.addIgnore(test, "doesn't match keyword")
1314 result.addIgnore(test, "doesn't match keyword")
1314 ignored = True
1315 ignored = True
1315 break
1316 break
1316
1317
1317 if ignored:
1318 if ignored:
1318 continue
1319 continue
1319
1320
1320 tests.append(test)
1321 tests.append(test)
1321
1322
1322 runtests = list(tests)
1323 runtests = list(tests)
1323 done = queue.Queue()
1324 done = queue.Queue()
1324 running = 0
1325 running = 0
1325
1326
1326 def job(test, result):
1327 def job(test, result):
1327 try:
1328 try:
1328 test(result)
1329 test(result)
1329 done.put(None)
1330 done.put(None)
1330 except KeyboardInterrupt:
1331 except KeyboardInterrupt:
1331 pass
1332 pass
1332 except: # re-raises
1333 except: # re-raises
1333 done.put(('!', test, 'run-test raised an error, see traceback'))
1334 done.put(('!', test, 'run-test raised an error, see traceback'))
1334 raise
1335 raise
1335
1336
1336 try:
1337 try:
1337 while tests or running:
1338 while tests or running:
1338 if not done.empty() or running == self._jobs or not tests:
1339 if not done.empty() or running == self._jobs or not tests:
1339 try:
1340 try:
1340 done.get(True, 1)
1341 done.get(True, 1)
1341 if result and result.shouldStop:
1342 if result and result.shouldStop:
1342 break
1343 break
1343 except queue.Empty:
1344 except queue.Empty:
1344 continue
1345 continue
1345 running -= 1
1346 running -= 1
1346 if tests and not running == self._jobs:
1347 if tests and not running == self._jobs:
1347 test = tests.pop(0)
1348 test = tests.pop(0)
1348 if self._loop:
1349 if self._loop:
1349 tests.append(test)
1350 tests.append(test)
1350 t = threading.Thread(target=job, name=test.name,
1351 t = threading.Thread(target=job, name=test.name,
1351 args=(test, result))
1352 args=(test, result))
1352 t.start()
1353 t.start()
1353 running += 1
1354 running += 1
1354 except KeyboardInterrupt:
1355 except KeyboardInterrupt:
1355 for test in runtests:
1356 for test in runtests:
1356 test.abort()
1357 test.abort()
1357
1358
1358 return result
1359 return result
1359
1360
1360 class TextTestRunner(unittest.TextTestRunner):
1361 class TextTestRunner(unittest.TextTestRunner):
1361 """Custom unittest test runner that uses appropriate settings."""
1362 """Custom unittest test runner that uses appropriate settings."""
1362
1363
1363 def __init__(self, runner, *args, **kwargs):
1364 def __init__(self, runner, *args, **kwargs):
1364 super(TextTestRunner, self).__init__(*args, **kwargs)
1365 super(TextTestRunner, self).__init__(*args, **kwargs)
1365
1366
1366 self._runner = runner
1367 self._runner = runner
1367
1368
1368 def run(self, test):
1369 def run(self, test):
1369 result = TestResult(self._runner.options, self.stream,
1370 result = TestResult(self._runner.options, self.stream,
1370 self.descriptions, self.verbosity)
1371 self.descriptions, self.verbosity)
1371
1372
1372 test(result)
1373 test(result)
1373
1374
1374 failed = len(result.failures)
1375 failed = len(result.failures)
1375 warned = len(result.warned)
1376 warned = len(result.warned)
1376 skipped = len(result.skipped)
1377 skipped = len(result.skipped)
1377 ignored = len(result.ignored)
1378 ignored = len(result.ignored)
1378
1379
1379 iolock.acquire()
1380 iolock.acquire()
1380 self.stream.writeln('')
1381 self.stream.writeln('')
1381
1382
1382 if not self._runner.options.noskips:
1383 if not self._runner.options.noskips:
1383 for test, msg in result.skipped:
1384 for test, msg in result.skipped:
1384 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1385 self.stream.writeln('Skipped %s: %s' % (test.name, msg))
1385 for test, msg in result.warned:
1386 for test, msg in result.warned:
1386 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1387 self.stream.writeln('Warned %s: %s' % (test.name, msg))
1387 for test, msg in result.failures:
1388 for test, msg in result.failures:
1388 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1389 self.stream.writeln('Failed %s: %s' % (test.name, msg))
1389 for test, msg in result.errors:
1390 for test, msg in result.errors:
1390 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1391 self.stream.writeln('Errored %s: %s' % (test.name, msg))
1391
1392
1392 if self._runner.options.xunit:
1393 if self._runner.options.xunit:
1393 xuf = open(self._runner.options.xunit, 'wb')
1394 xuf = open(self._runner.options.xunit, 'wb')
1394 try:
1395 try:
1395 timesd = dict(
1396 timesd = dict(
1396 (test, real) for test, cuser, csys, real in result.times)
1397 (test, real) for test, cuser, csys, real in result.times)
1397 doc = minidom.Document()
1398 doc = minidom.Document()
1398 s = doc.createElement('testsuite')
1399 s = doc.createElement('testsuite')
1399 s.setAttribute('name', 'run-tests')
1400 s.setAttribute('name', 'run-tests')
1400 s.setAttribute('tests', str(result.testsRun))
1401 s.setAttribute('tests', str(result.testsRun))
1401 s.setAttribute('errors', "0") # TODO
1402 s.setAttribute('errors', "0") # TODO
1402 s.setAttribute('failures', str(failed))
1403 s.setAttribute('failures', str(failed))
1403 s.setAttribute('skipped', str(skipped + ignored))
1404 s.setAttribute('skipped', str(skipped + ignored))
1404 doc.appendChild(s)
1405 doc.appendChild(s)
1405 for tc in result.successes:
1406 for tc in result.successes:
1406 t = doc.createElement('testcase')
1407 t = doc.createElement('testcase')
1407 t.setAttribute('name', tc.name)
1408 t.setAttribute('name', tc.name)
1408 t.setAttribute('time', '%.3f' % timesd[tc.name])
1409 t.setAttribute('time', '%.3f' % timesd[tc.name])
1409 s.appendChild(t)
1410 s.appendChild(t)
1410 for tc, err in sorted(result.faildata.iteritems()):
1411 for tc, err in sorted(result.faildata.iteritems()):
1411 t = doc.createElement('testcase')
1412 t = doc.createElement('testcase')
1412 t.setAttribute('name', tc)
1413 t.setAttribute('name', tc)
1413 t.setAttribute('time', '%.3f' % timesd[tc])
1414 t.setAttribute('time', '%.3f' % timesd[tc])
1414 cd = doc.createCDATASection(cdatasafe(err))
1415 cd = doc.createCDATASection(cdatasafe(err))
1415 t.appendChild(cd)
1416 t.appendChild(cd)
1416 s.appendChild(t)
1417 s.appendChild(t)
1417 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1418 xuf.write(doc.toprettyxml(indent=' ', encoding='utf-8'))
1418 finally:
1419 finally:
1419 xuf.close()
1420 xuf.close()
1420
1421
1421 self._runner._checkhglib('Tested')
1422 self._runner._checkhglib('Tested')
1422
1423
1423 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1424 self.stream.writeln('# Ran %d tests, %d skipped, %d warned, %d failed.'
1424 % (result.testsRun,
1425 % (result.testsRun,
1425 skipped + ignored, warned, failed))
1426 skipped + ignored, warned, failed))
1426 if failed:
1427 if failed:
1427 self.stream.writeln('python hash seed: %s' %
1428 self.stream.writeln('python hash seed: %s' %
1428 os.environ['PYTHONHASHSEED'])
1429 os.environ['PYTHONHASHSEED'])
1429 if self._runner.options.time:
1430 if self._runner.options.time:
1430 self.printtimes(result.times)
1431 self.printtimes(result.times)
1431
1432
1432 iolock.release()
1433 iolock.release()
1433
1434
1434 return result
1435 return result
1435
1436
1436 def printtimes(self, times):
1437 def printtimes(self, times):
1437 # iolock held by run
1438 # iolock held by run
1438 self.stream.writeln('# Producing time report')
1439 self.stream.writeln('# Producing time report')
1439 times.sort(key=lambda t: (t[3]))
1440 times.sort(key=lambda t: (t[3]))
1440 cols = '%7.3f %7.3f %7.3f %s'
1441 cols = '%7.3f %7.3f %7.3f %s'
1441 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1442 self.stream.writeln('%-7s %-7s %-7s %s' % ('cuser', 'csys', 'real',
1442 'Test'))
1443 'Test'))
1443 for test, cuser, csys, real in times:
1444 for test, cuser, csys, real in times:
1444 self.stream.writeln(cols % (cuser, csys, real, test))
1445 self.stream.writeln(cols % (cuser, csys, real, test))
1445
1446
1446 class TestRunner(object):
1447 class TestRunner(object):
1447 """Holds context for executing tests.
1448 """Holds context for executing tests.
1448
1449
1449 Tests rely on a lot of state. This object holds it for them.
1450 Tests rely on a lot of state. This object holds it for them.
1450 """
1451 """
1451
1452
1452 # Programs required to run tests.
1453 # Programs required to run tests.
1453 REQUIREDTOOLS = [
1454 REQUIREDTOOLS = [
1454 os.path.basename(sys.executable),
1455 os.path.basename(sys.executable),
1455 'diff',
1456 'diff',
1456 'grep',
1457 'grep',
1457 'unzip',
1458 'unzip',
1458 'gunzip',
1459 'gunzip',
1459 'bunzip2',
1460 'bunzip2',
1460 'sed',
1461 'sed',
1461 ]
1462 ]
1462
1463
1463 # Maps file extensions to test class.
1464 # Maps file extensions to test class.
1464 TESTTYPES = [
1465 TESTTYPES = [
1465 ('.py', PythonTest),
1466 ('.py', PythonTest),
1466 ('.t', TTest),
1467 ('.t', TTest),
1467 ]
1468 ]
1468
1469
1469 def __init__(self):
1470 def __init__(self):
1470 self.options = None
1471 self.options = None
1471 self._testdir = None
1472 self._testdir = None
1472 self._hgtmp = None
1473 self._hgtmp = None
1473 self._installdir = None
1474 self._installdir = None
1474 self._bindir = None
1475 self._bindir = None
1475 self._tmpbinddir = None
1476 self._tmpbinddir = None
1476 self._pythondir = None
1477 self._pythondir = None
1477 self._coveragefile = None
1478 self._coveragefile = None
1478 self._createdfiles = []
1479 self._createdfiles = []
1479 self._hgpath = None
1480 self._hgpath = None
1480
1481
1481 def run(self, args, parser=None):
1482 def run(self, args, parser=None):
1482 """Run the test suite."""
1483 """Run the test suite."""
1483 oldmask = os.umask(022)
1484 oldmask = os.umask(022)
1484 try:
1485 try:
1485 parser = parser or getparser()
1486 parser = parser or getparser()
1486 options, args = parseargs(args, parser)
1487 options, args = parseargs(args, parser)
1487 self.options = options
1488 self.options = options
1488
1489
1489 self._checktools()
1490 self._checktools()
1490 tests = self.findtests(args)
1491 tests = self.findtests(args)
1491 return self._run(tests)
1492 return self._run(tests)
1492 finally:
1493 finally:
1493 os.umask(oldmask)
1494 os.umask(oldmask)
1494
1495
1495 def _run(self, tests):
1496 def _run(self, tests):
1496 if self.options.random:
1497 if self.options.random:
1497 random.shuffle(tests)
1498 random.shuffle(tests)
1498 else:
1499 else:
1499 # keywords for slow tests
1500 # keywords for slow tests
1500 slow = 'svn gendoc check-code-hg'.split()
1501 slow = 'svn gendoc check-code-hg'.split()
1501 def sortkey(f):
1502 def sortkey(f):
1502 # run largest tests first, as they tend to take the longest
1503 # run largest tests first, as they tend to take the longest
1503 try:
1504 try:
1504 val = -os.stat(f).st_size
1505 val = -os.stat(f).st_size
1505 except OSError, e:
1506 except OSError, e:
1506 if e.errno != errno.ENOENT:
1507 if e.errno != errno.ENOENT:
1507 raise
1508 raise
1508 return -1e9 # file does not exist, tell early
1509 return -1e9 # file does not exist, tell early
1509 for kw in slow:
1510 for kw in slow:
1510 if kw in f:
1511 if kw in f:
1511 val *= 10
1512 val *= 10
1512 return val
1513 return val
1513 tests.sort(key=sortkey)
1514 tests.sort(key=sortkey)
1514
1515
1515 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1516 self._testdir = os.environ['TESTDIR'] = os.getcwd()
1516
1517
1517 if 'PYTHONHASHSEED' not in os.environ:
1518 if 'PYTHONHASHSEED' not in os.environ:
1518 # use a random python hash seed all the time
1519 # use a random python hash seed all the time
1519 # we do the randomness ourself to know what seed is used
1520 # we do the randomness ourself to know what seed is used
1520 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1521 os.environ['PYTHONHASHSEED'] = str(random.getrandbits(32))
1521
1522
1522 if self.options.tmpdir:
1523 if self.options.tmpdir:
1523 self.options.keep_tmpdir = True
1524 self.options.keep_tmpdir = True
1524 tmpdir = self.options.tmpdir
1525 tmpdir = self.options.tmpdir
1525 if os.path.exists(tmpdir):
1526 if os.path.exists(tmpdir):
1526 # Meaning of tmpdir has changed since 1.3: we used to create
1527 # Meaning of tmpdir has changed since 1.3: we used to create
1527 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1528 # HGTMP inside tmpdir; now HGTMP is tmpdir. So fail if
1528 # tmpdir already exists.
1529 # tmpdir already exists.
1529 print "error: temp dir %r already exists" % tmpdir
1530 print "error: temp dir %r already exists" % tmpdir
1530 return 1
1531 return 1
1531
1532
1532 # Automatically removing tmpdir sounds convenient, but could
1533 # Automatically removing tmpdir sounds convenient, but could
1533 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1534 # really annoy anyone in the habit of using "--tmpdir=/tmp"
1534 # or "--tmpdir=$HOME".
1535 # or "--tmpdir=$HOME".
1535 #vlog("# Removing temp dir", tmpdir)
1536 #vlog("# Removing temp dir", tmpdir)
1536 #shutil.rmtree(tmpdir)
1537 #shutil.rmtree(tmpdir)
1537 os.makedirs(tmpdir)
1538 os.makedirs(tmpdir)
1538 else:
1539 else:
1539 d = None
1540 d = None
1540 if os.name == 'nt':
1541 if os.name == 'nt':
1541 # without this, we get the default temp dir location, but
1542 # without this, we get the default temp dir location, but
1542 # in all lowercase, which causes troubles with paths (issue3490)
1543 # in all lowercase, which causes troubles with paths (issue3490)
1543 d = os.getenv('TMP')
1544 d = os.getenv('TMP')
1544 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1545 tmpdir = tempfile.mkdtemp('', 'hgtests.', d)
1545 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1546 self._hgtmp = os.environ['HGTMP'] = os.path.realpath(tmpdir)
1546
1547
1547 if self.options.with_hg:
1548 if self.options.with_hg:
1548 self._installdir = None
1549 self._installdir = None
1549 self._bindir = os.path.dirname(os.path.realpath(
1550 self._bindir = os.path.dirname(os.path.realpath(
1550 self.options.with_hg))
1551 self.options.with_hg))
1551 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1552 self._tmpbindir = os.path.join(self._hgtmp, 'install', 'bin')
1552 os.makedirs(self._tmpbindir)
1553 os.makedirs(self._tmpbindir)
1553
1554
1554 # This looks redundant with how Python initializes sys.path from
1555 # This looks redundant with how Python initializes sys.path from
1555 # the location of the script being executed. Needed because the
1556 # the location of the script being executed. Needed because the
1556 # "hg" specified by --with-hg is not the only Python script
1557 # "hg" specified by --with-hg is not the only Python script
1557 # executed in the test suite that needs to import 'mercurial'
1558 # executed in the test suite that needs to import 'mercurial'
1558 # ... which means it's not really redundant at all.
1559 # ... which means it's not really redundant at all.
1559 self._pythondir = self._bindir
1560 self._pythondir = self._bindir
1560 else:
1561 else:
1561 self._installdir = os.path.join(self._hgtmp, "install")
1562 self._installdir = os.path.join(self._hgtmp, "install")
1562 self._bindir = os.environ["BINDIR"] = \
1563 self._bindir = os.environ["BINDIR"] = \
1563 os.path.join(self._installdir, "bin")
1564 os.path.join(self._installdir, "bin")
1564 self._tmpbindir = self._bindir
1565 self._tmpbindir = self._bindir
1565 self._pythondir = os.path.join(self._installdir, "lib", "python")
1566 self._pythondir = os.path.join(self._installdir, "lib", "python")
1566
1567
1567 os.environ["BINDIR"] = self._bindir
1568 os.environ["BINDIR"] = self._bindir
1568 os.environ["PYTHON"] = PYTHON
1569 os.environ["PYTHON"] = PYTHON
1569
1570
1570 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1571 path = [self._bindir] + os.environ["PATH"].split(os.pathsep)
1571 if self._tmpbindir != self._bindir:
1572 if self._tmpbindir != self._bindir:
1572 path = [self._tmpbindir] + path
1573 path = [self._tmpbindir] + path
1573 os.environ["PATH"] = os.pathsep.join(path)
1574 os.environ["PATH"] = os.pathsep.join(path)
1574
1575
1575 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1576 # Include TESTDIR in PYTHONPATH so that out-of-tree extensions
1576 # can run .../tests/run-tests.py test-foo where test-foo
1577 # can run .../tests/run-tests.py test-foo where test-foo
1577 # adds an extension to HGRC. Also include run-test.py directory to
1578 # adds an extension to HGRC. Also include run-test.py directory to
1578 # import modules like heredoctest.
1579 # import modules like heredoctest.
1579 pypath = [self._pythondir, self._testdir,
1580 pypath = [self._pythondir, self._testdir,
1580 os.path.abspath(os.path.dirname(__file__))]
1581 os.path.abspath(os.path.dirname(__file__))]
1581 # We have to augment PYTHONPATH, rather than simply replacing
1582 # We have to augment PYTHONPATH, rather than simply replacing
1582 # it, in case external libraries are only available via current
1583 # it, in case external libraries are only available via current
1583 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1584 # PYTHONPATH. (In particular, the Subversion bindings on OS X
1584 # are in /opt/subversion.)
1585 # are in /opt/subversion.)
1585 oldpypath = os.environ.get(IMPL_PATH)
1586 oldpypath = os.environ.get(IMPL_PATH)
1586 if oldpypath:
1587 if oldpypath:
1587 pypath.append(oldpypath)
1588 pypath.append(oldpypath)
1588 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1589 os.environ[IMPL_PATH] = os.pathsep.join(pypath)
1589
1590
1590 self._coveragefile = os.path.join(self._testdir, '.coverage')
1591 self._coveragefile = os.path.join(self._testdir, '.coverage')
1591
1592
1592 vlog("# Using TESTDIR", self._testdir)
1593 vlog("# Using TESTDIR", self._testdir)
1593 vlog("# Using HGTMP", self._hgtmp)
1594 vlog("# Using HGTMP", self._hgtmp)
1594 vlog("# Using PATH", os.environ["PATH"])
1595 vlog("# Using PATH", os.environ["PATH"])
1595 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1596 vlog("# Using", IMPL_PATH, os.environ[IMPL_PATH])
1596
1597
1597 try:
1598 try:
1598 return self._runtests(tests) or 0
1599 return self._runtests(tests) or 0
1599 finally:
1600 finally:
1600 time.sleep(.1)
1601 time.sleep(.1)
1601 self._cleanup()
1602 self._cleanup()
1602
1603
1603 def findtests(self, args):
1604 def findtests(self, args):
1604 """Finds possible test files from arguments.
1605 """Finds possible test files from arguments.
1605
1606
1606 If you wish to inject custom tests into the test harness, this would
1607 If you wish to inject custom tests into the test harness, this would
1607 be a good function to monkeypatch or override in a derived class.
1608 be a good function to monkeypatch or override in a derived class.
1608 """
1609 """
1609 if not args:
1610 if not args:
1610 if self.options.changed:
1611 if self.options.changed:
1611 proc = Popen4('hg st --rev "%s" -man0 .' %
1612 proc = Popen4('hg st --rev "%s" -man0 .' %
1612 self.options.changed, None, 0)
1613 self.options.changed, None, 0)
1613 stdout, stderr = proc.communicate()
1614 stdout, stderr = proc.communicate()
1614 args = stdout.strip('\0').split('\0')
1615 args = stdout.strip('\0').split('\0')
1615 else:
1616 else:
1616 args = os.listdir('.')
1617 args = os.listdir('.')
1617
1618
1618 return [t for t in args
1619 return [t for t in args
1619 if os.path.basename(t).startswith('test-')
1620 if os.path.basename(t).startswith('test-')
1620 and (t.endswith('.py') or t.endswith('.t'))]
1621 and (t.endswith('.py') or t.endswith('.t'))]
1621
1622
1622 def _runtests(self, tests):
1623 def _runtests(self, tests):
1623 try:
1624 try:
1624 if self._installdir:
1625 if self._installdir:
1625 self._installhg()
1626 self._installhg()
1626 self._checkhglib("Testing")
1627 self._checkhglib("Testing")
1627 else:
1628 else:
1628 self._usecorrectpython()
1629 self._usecorrectpython()
1629
1630
1630 if self.options.restart:
1631 if self.options.restart:
1631 orig = list(tests)
1632 orig = list(tests)
1632 while tests:
1633 while tests:
1633 if os.path.exists(tests[0] + ".err"):
1634 if os.path.exists(tests[0] + ".err"):
1634 break
1635 break
1635 tests.pop(0)
1636 tests.pop(0)
1636 if not tests:
1637 if not tests:
1637 print "running all tests"
1638 print "running all tests"
1638 tests = orig
1639 tests = orig
1639
1640
1640 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1641 tests = [self._gettest(t, i) for i, t in enumerate(tests)]
1641
1642
1642 failed = False
1643 failed = False
1643 warned = False
1644 warned = False
1644
1645
1645 suite = TestSuite(self._testdir,
1646 suite = TestSuite(self._testdir,
1646 jobs=self.options.jobs,
1647 jobs=self.options.jobs,
1647 whitelist=self.options.whitelisted,
1648 whitelist=self.options.whitelisted,
1648 blacklist=self.options.blacklist,
1649 blacklist=self.options.blacklist,
1649 retest=self.options.retest,
1650 retest=self.options.retest,
1650 keywords=self.options.keywords,
1651 keywords=self.options.keywords,
1651 loop=self.options.loop,
1652 loop=self.options.loop,
1652 tests=tests)
1653 tests=tests)
1653 verbosity = 1
1654 verbosity = 1
1654 if self.options.verbose:
1655 if self.options.verbose:
1655 verbosity = 2
1656 verbosity = 2
1656 runner = TextTestRunner(self, verbosity=verbosity)
1657 runner = TextTestRunner(self, verbosity=verbosity)
1657 result = runner.run(suite)
1658 result = runner.run(suite)
1658
1659
1659 if result.failures:
1660 if result.failures:
1660 failed = True
1661 failed = True
1661 if result.warned:
1662 if result.warned:
1662 warned = True
1663 warned = True
1663
1664
1664 if self.options.anycoverage:
1665 if self.options.anycoverage:
1665 self._outputcoverage()
1666 self._outputcoverage()
1666 except KeyboardInterrupt:
1667 except KeyboardInterrupt:
1667 failed = True
1668 failed = True
1668 print "\ninterrupted!"
1669 print "\ninterrupted!"
1669
1670
1670 if failed:
1671 if failed:
1671 return 1
1672 return 1
1672 if warned:
1673 if warned:
1673 return 80
1674 return 80
1674
1675
1675 def _gettest(self, test, count):
1676 def _gettest(self, test, count):
1676 """Obtain a Test by looking at its filename.
1677 """Obtain a Test by looking at its filename.
1677
1678
1678 Returns a Test instance. The Test may not be runnable if it doesn't
1679 Returns a Test instance. The Test may not be runnable if it doesn't
1679 map to a known type.
1680 map to a known type.
1680 """
1681 """
1681 lctest = test.lower()
1682 lctest = test.lower()
1682 testcls = Test
1683 testcls = Test
1683
1684
1684 for ext, cls in self.TESTTYPES:
1685 for ext, cls in self.TESTTYPES:
1685 if lctest.endswith(ext):
1686 if lctest.endswith(ext):
1686 testcls = cls
1687 testcls = cls
1687 break
1688 break
1688
1689
1689 refpath = os.path.join(self._testdir, test)
1690 refpath = os.path.join(self._testdir, test)
1690 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1691 tmpdir = os.path.join(self._hgtmp, 'child%d' % count)
1691
1692
1692 return testcls(refpath, tmpdir,
1693 return testcls(refpath, tmpdir,
1693 keeptmpdir=self.options.keep_tmpdir,
1694 keeptmpdir=self.options.keep_tmpdir,
1694 debug=self.options.debug,
1695 debug=self.options.debug,
1695 timeout=self.options.timeout,
1696 timeout=self.options.timeout,
1696 startport=self.options.port + count * 3,
1697 startport=self.options.port + count * 3,
1697 extraconfigopts=self.options.extra_config_opt,
1698 extraconfigopts=self.options.extra_config_opt,
1698 py3kwarnings=self.options.py3k_warnings,
1699 py3kwarnings=self.options.py3k_warnings,
1699 shell=self.options.shell)
1700 shell=self.options.shell)
1700
1701
1701 def _cleanup(self):
1702 def _cleanup(self):
1702 """Clean up state from this test invocation."""
1703 """Clean up state from this test invocation."""
1703
1704
1704 if self.options.keep_tmpdir:
1705 if self.options.keep_tmpdir:
1705 return
1706 return
1706
1707
1707 vlog("# Cleaning up HGTMP", self._hgtmp)
1708 vlog("# Cleaning up HGTMP", self._hgtmp)
1708 shutil.rmtree(self._hgtmp, True)
1709 shutil.rmtree(self._hgtmp, True)
1709 for f in self._createdfiles:
1710 for f in self._createdfiles:
1710 try:
1711 try:
1711 os.remove(f)
1712 os.remove(f)
1712 except OSError:
1713 except OSError:
1713 pass
1714 pass
1714
1715
1715 def _usecorrectpython(self):
1716 def _usecorrectpython(self):
1716 """Configure the environment to use the appropriate Python in tests."""
1717 """Configure the environment to use the appropriate Python in tests."""
1717 # Tests must use the same interpreter as us or bad things will happen.
1718 # Tests must use the same interpreter as us or bad things will happen.
1718 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1719 pyexename = sys.platform == 'win32' and 'python.exe' or 'python'
1719 if getattr(os, 'symlink', None):
1720 if getattr(os, 'symlink', None):
1720 vlog("# Making python executable in test path a symlink to '%s'" %
1721 vlog("# Making python executable in test path a symlink to '%s'" %
1721 sys.executable)
1722 sys.executable)
1722 mypython = os.path.join(self._tmpbindir, pyexename)
1723 mypython = os.path.join(self._tmpbindir, pyexename)
1723 try:
1724 try:
1724 if os.readlink(mypython) == sys.executable:
1725 if os.readlink(mypython) == sys.executable:
1725 return
1726 return
1726 os.unlink(mypython)
1727 os.unlink(mypython)
1727 except OSError, err:
1728 except OSError, err:
1728 if err.errno != errno.ENOENT:
1729 if err.errno != errno.ENOENT:
1729 raise
1730 raise
1730 if self._findprogram(pyexename) != sys.executable:
1731 if self._findprogram(pyexename) != sys.executable:
1731 try:
1732 try:
1732 os.symlink(sys.executable, mypython)
1733 os.symlink(sys.executable, mypython)
1733 self._createdfiles.append(mypython)
1734 self._createdfiles.append(mypython)
1734 except OSError, err:
1735 except OSError, err:
1735 # child processes may race, which is harmless
1736 # child processes may race, which is harmless
1736 if err.errno != errno.EEXIST:
1737 if err.errno != errno.EEXIST:
1737 raise
1738 raise
1738 else:
1739 else:
1739 exedir, exename = os.path.split(sys.executable)
1740 exedir, exename = os.path.split(sys.executable)
1740 vlog("# Modifying search path to find %s as %s in '%s'" %
1741 vlog("# Modifying search path to find %s as %s in '%s'" %
1741 (exename, pyexename, exedir))
1742 (exename, pyexename, exedir))
1742 path = os.environ['PATH'].split(os.pathsep)
1743 path = os.environ['PATH'].split(os.pathsep)
1743 while exedir in path:
1744 while exedir in path:
1744 path.remove(exedir)
1745 path.remove(exedir)
1745 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1746 os.environ['PATH'] = os.pathsep.join([exedir] + path)
1746 if not self._findprogram(pyexename):
1747 if not self._findprogram(pyexename):
1747 print "WARNING: Cannot find %s in search path" % pyexename
1748 print "WARNING: Cannot find %s in search path" % pyexename
1748
1749
1749 def _installhg(self):
1750 def _installhg(self):
1750 """Install hg into the test environment.
1751 """Install hg into the test environment.
1751
1752
1752 This will also configure hg with the appropriate testing settings.
1753 This will also configure hg with the appropriate testing settings.
1753 """
1754 """
1754 vlog("# Performing temporary installation of HG")
1755 vlog("# Performing temporary installation of HG")
1755 installerrs = os.path.join("tests", "install.err")
1756 installerrs = os.path.join("tests", "install.err")
1756 compiler = ''
1757 compiler = ''
1757 if self.options.compiler:
1758 if self.options.compiler:
1758 compiler = '--compiler ' + self.options.compiler
1759 compiler = '--compiler ' + self.options.compiler
1759 pure = self.options.pure and "--pure" or ""
1760 pure = self.options.pure and "--pure" or ""
1760 py3 = ''
1761 py3 = ''
1761 if sys.version_info[0] == 3:
1762 if sys.version_info[0] == 3:
1762 py3 = '--c2to3'
1763 py3 = '--c2to3'
1763
1764
1764 # Run installer in hg root
1765 # Run installer in hg root
1765 script = os.path.realpath(sys.argv[0])
1766 script = os.path.realpath(sys.argv[0])
1766 hgroot = os.path.dirname(os.path.dirname(script))
1767 hgroot = os.path.dirname(os.path.dirname(script))
1767 os.chdir(hgroot)
1768 os.chdir(hgroot)
1768 nohome = '--home=""'
1769 nohome = '--home=""'
1769 if os.name == 'nt':
1770 if os.name == 'nt':
1770 # The --home="" trick works only on OS where os.sep == '/'
1771 # The --home="" trick works only on OS where os.sep == '/'
1771 # because of a distutils convert_path() fast-path. Avoid it at
1772 # because of a distutils convert_path() fast-path. Avoid it at
1772 # least on Windows for now, deal with .pydistutils.cfg bugs
1773 # least on Windows for now, deal with .pydistutils.cfg bugs
1773 # when they happen.
1774 # when they happen.
1774 nohome = ''
1775 nohome = ''
1775 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1776 cmd = ('%(exe)s setup.py %(py3)s %(pure)s clean --all'
1776 ' build %(compiler)s --build-base="%(base)s"'
1777 ' build %(compiler)s --build-base="%(base)s"'
1777 ' install --force --prefix="%(prefix)s"'
1778 ' install --force --prefix="%(prefix)s"'
1778 ' --install-lib="%(libdir)s"'
1779 ' --install-lib="%(libdir)s"'
1779 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1780 ' --install-scripts="%(bindir)s" %(nohome)s >%(logfile)s 2>&1'
1780 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1781 % {'exe': sys.executable, 'py3': py3, 'pure': pure,
1781 'compiler': compiler,
1782 'compiler': compiler,
1782 'base': os.path.join(self._hgtmp, "build"),
1783 'base': os.path.join(self._hgtmp, "build"),
1783 'prefix': self._installdir, 'libdir': self._pythondir,
1784 'prefix': self._installdir, 'libdir': self._pythondir,
1784 'bindir': self._bindir,
1785 'bindir': self._bindir,
1785 'nohome': nohome, 'logfile': installerrs})
1786 'nohome': nohome, 'logfile': installerrs})
1786 vlog("# Running", cmd)
1787 vlog("# Running", cmd)
1787 if os.system(cmd) == 0:
1788 if os.system(cmd) == 0:
1788 if not self.options.verbose:
1789 if not self.options.verbose:
1789 os.remove(installerrs)
1790 os.remove(installerrs)
1790 else:
1791 else:
1791 f = open(installerrs, 'rb')
1792 f = open(installerrs, 'rb')
1792 for line in f:
1793 for line in f:
1793 print line
1794 print line
1794 f.close()
1795 f.close()
1795 sys.exit(1)
1796 sys.exit(1)
1796 os.chdir(self._testdir)
1797 os.chdir(self._testdir)
1797
1798
1798 self._usecorrectpython()
1799 self._usecorrectpython()
1799
1800
1800 if self.options.py3k_warnings and not self.options.anycoverage:
1801 if self.options.py3k_warnings and not self.options.anycoverage:
1801 vlog("# Updating hg command to enable Py3k Warnings switch")
1802 vlog("# Updating hg command to enable Py3k Warnings switch")
1802 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1803 f = open(os.path.join(self._bindir, 'hg'), 'rb')
1803 lines = [line.rstrip() for line in f]
1804 lines = [line.rstrip() for line in f]
1804 lines[0] += ' -3'
1805 lines[0] += ' -3'
1805 f.close()
1806 f.close()
1806 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1807 f = open(os.path.join(self._bindir, 'hg'), 'wb')
1807 for line in lines:
1808 for line in lines:
1808 f.write(line + '\n')
1809 f.write(line + '\n')
1809 f.close()
1810 f.close()
1810
1811
1811 hgbat = os.path.join(self._bindir, 'hg.bat')
1812 hgbat = os.path.join(self._bindir, 'hg.bat')
1812 if os.path.isfile(hgbat):
1813 if os.path.isfile(hgbat):
1813 # hg.bat expects to be put in bin/scripts while run-tests.py
1814 # hg.bat expects to be put in bin/scripts while run-tests.py
1814 # installation layout put it in bin/ directly. Fix it
1815 # installation layout put it in bin/ directly. Fix it
1815 f = open(hgbat, 'rb')
1816 f = open(hgbat, 'rb')
1816 data = f.read()
1817 data = f.read()
1817 f.close()
1818 f.close()
1818 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1819 if '"%~dp0..\python" "%~dp0hg" %*' in data:
1819 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1820 data = data.replace('"%~dp0..\python" "%~dp0hg" %*',
1820 '"%~dp0python" "%~dp0hg" %*')
1821 '"%~dp0python" "%~dp0hg" %*')
1821 f = open(hgbat, 'wb')
1822 f = open(hgbat, 'wb')
1822 f.write(data)
1823 f.write(data)
1823 f.close()
1824 f.close()
1824 else:
1825 else:
1825 print 'WARNING: cannot fix hg.bat reference to python.exe'
1826 print 'WARNING: cannot fix hg.bat reference to python.exe'
1826
1827
1827 if self.options.anycoverage:
1828 if self.options.anycoverage:
1828 custom = os.path.join(self._testdir, 'sitecustomize.py')
1829 custom = os.path.join(self._testdir, 'sitecustomize.py')
1829 target = os.path.join(self._pythondir, 'sitecustomize.py')
1830 target = os.path.join(self._pythondir, 'sitecustomize.py')
1830 vlog('# Installing coverage trigger to %s' % target)
1831 vlog('# Installing coverage trigger to %s' % target)
1831 shutil.copyfile(custom, target)
1832 shutil.copyfile(custom, target)
1832 rc = os.path.join(self._testdir, '.coveragerc')
1833 rc = os.path.join(self._testdir, '.coveragerc')
1833 vlog('# Installing coverage rc to %s' % rc)
1834 vlog('# Installing coverage rc to %s' % rc)
1834 os.environ['COVERAGE_PROCESS_START'] = rc
1835 os.environ['COVERAGE_PROCESS_START'] = rc
1835 fn = os.path.join(self._installdir, '..', '.coverage')
1836 fn = os.path.join(self._installdir, '..', '.coverage')
1836 os.environ['COVERAGE_FILE'] = fn
1837 os.environ['COVERAGE_FILE'] = fn
1837
1838
1838 def _checkhglib(self, verb):
1839 def _checkhglib(self, verb):
1839 """Ensure that the 'mercurial' package imported by python is
1840 """Ensure that the 'mercurial' package imported by python is
1840 the one we expect it to be. If not, print a warning to stderr."""
1841 the one we expect it to be. If not, print a warning to stderr."""
1841 if ((self._bindir == self._pythondir) and
1842 if ((self._bindir == self._pythondir) and
1842 (self._bindir != self._tmpbindir)):
1843 (self._bindir != self._tmpbindir)):
1843 # The pythondir has been infered from --with-hg flag.
1844 # The pythondir has been infered from --with-hg flag.
1844 # We cannot expect anything sensible here
1845 # We cannot expect anything sensible here
1845 return
1846 return
1846 expecthg = os.path.join(self._pythondir, 'mercurial')
1847 expecthg = os.path.join(self._pythondir, 'mercurial')
1847 actualhg = self._gethgpath()
1848 actualhg = self._gethgpath()
1848 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1849 if os.path.abspath(actualhg) != os.path.abspath(expecthg):
1849 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1850 sys.stderr.write('warning: %s with unexpected mercurial lib: %s\n'
1850 ' (expected %s)\n'
1851 ' (expected %s)\n'
1851 % (verb, actualhg, expecthg))
1852 % (verb, actualhg, expecthg))
1852 def _gethgpath(self):
1853 def _gethgpath(self):
1853 """Return the path to the mercurial package that is actually found by
1854 """Return the path to the mercurial package that is actually found by
1854 the current Python interpreter."""
1855 the current Python interpreter."""
1855 if self._hgpath is not None:
1856 if self._hgpath is not None:
1856 return self._hgpath
1857 return self._hgpath
1857
1858
1858 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1859 cmd = '%s -c "import mercurial; print (mercurial.__path__[0])"'
1859 pipe = os.popen(cmd % PYTHON)
1860 pipe = os.popen(cmd % PYTHON)
1860 try:
1861 try:
1861 self._hgpath = pipe.read().strip()
1862 self._hgpath = pipe.read().strip()
1862 finally:
1863 finally:
1863 pipe.close()
1864 pipe.close()
1864
1865
1865 return self._hgpath
1866 return self._hgpath
1866
1867
1867 def _outputcoverage(self):
1868 def _outputcoverage(self):
1868 """Produce code coverage output."""
1869 """Produce code coverage output."""
1869 vlog('# Producing coverage report')
1870 vlog('# Producing coverage report')
1870 os.chdir(self._pythondir)
1871 os.chdir(self._pythondir)
1871
1872
1872 def covrun(*args):
1873 def covrun(*args):
1873 cmd = 'coverage %s' % ' '.join(args)
1874 cmd = 'coverage %s' % ' '.join(args)
1874 vlog('# Running: %s' % cmd)
1875 vlog('# Running: %s' % cmd)
1875 os.system(cmd)
1876 os.system(cmd)
1876
1877
1877 covrun('-c')
1878 covrun('-c')
1878 omit = ','.join(os.path.join(x, '*') for x in
1879 omit = ','.join(os.path.join(x, '*') for x in
1879 [self._bindir, self._testdir])
1880 [self._bindir, self._testdir])
1880 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1881 covrun('-i', '-r', '"--omit=%s"' % omit) # report
1881 if self.options.htmlcov:
1882 if self.options.htmlcov:
1882 htmldir = os.path.join(self._testdir, 'htmlcov')
1883 htmldir = os.path.join(self._testdir, 'htmlcov')
1883 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1884 covrun('-i', '-b', '"--directory=%s"' % htmldir,
1884 '"--omit=%s"' % omit)
1885 '"--omit=%s"' % omit)
1885 if self.options.annotate:
1886 if self.options.annotate:
1886 adir = os.path.join(self._testdir, 'annotated')
1887 adir = os.path.join(self._testdir, 'annotated')
1887 if not os.path.isdir(adir):
1888 if not os.path.isdir(adir):
1888 os.mkdir(adir)
1889 os.mkdir(adir)
1889 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1890 covrun('-i', '-a', '"--directory=%s"' % adir, '"--omit=%s"' % omit)
1890
1891
1891 def _findprogram(self, program):
1892 def _findprogram(self, program):
1892 """Search PATH for a executable program"""
1893 """Search PATH for a executable program"""
1893 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1894 for p in os.environ.get('PATH', os.defpath).split(os.pathsep):
1894 name = os.path.join(p, program)
1895 name = os.path.join(p, program)
1895 if os.name == 'nt' or os.access(name, os.X_OK):
1896 if os.name == 'nt' or os.access(name, os.X_OK):
1896 return name
1897 return name
1897 return None
1898 return None
1898
1899
1899 def _checktools(self):
1900 def _checktools(self):
1900 """Ensure tools required to run tests are present."""
1901 """Ensure tools required to run tests are present."""
1901 for p in self.REQUIREDTOOLS:
1902 for p in self.REQUIREDTOOLS:
1902 if os.name == 'nt' and not p.endswith('.exe'):
1903 if os.name == 'nt' and not p.endswith('.exe'):
1903 p += '.exe'
1904 p += '.exe'
1904 found = self._findprogram(p)
1905 found = self._findprogram(p)
1905 if found:
1906 if found:
1906 vlog("# Found prerequisite", p, "at", found)
1907 vlog("# Found prerequisite", p, "at", found)
1907 else:
1908 else:
1908 print "WARNING: Did not find prerequisite tool: %s " % p
1909 print "WARNING: Did not find prerequisite tool: %s " % p
1909
1910
1910 if __name__ == '__main__':
1911 if __name__ == '__main__':
1911 runner = TestRunner()
1912 runner = TestRunner()
1912
1913
1913 try:
1914 try:
1914 import msvcrt
1915 import msvcrt
1915 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
1916 msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
1916 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1917 msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
1917 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
1918 msvcrt.setmode(sys.stderr.fileno(), os.O_BINARY)
1918 except ImportError:
1919 except ImportError:
1919 pass
1920 pass
1920
1921
1921 sys.exit(runner.run(sys.argv[1:]))
1922 sys.exit(runner.run(sys.argv[1:]))
@@ -1,410 +1,448 b''
1 Test histedit extension: Fold commands
1 Test histedit extension: Fold commands
2 ======================================
2 ======================================
3
3
4 This test file is dedicated to testing the fold command in non conflicting
4 This test file is dedicated to testing the fold command in non conflicting
5 case.
5 case.
6
6
7 Initialization
7 Initialization
8 ---------------
8 ---------------
9
9
10
10
11 $ . "$TESTDIR/histedit-helpers.sh"
11 $ . "$TESTDIR/histedit-helpers.sh"
12
12
13 $ cat >> $HGRCPATH <<EOF
13 $ cat >> $HGRCPATH <<EOF
14 > [alias]
14 > [alias]
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
15 > logt = log --template '{rev}:{node|short} {desc|firstline}\n'
16 > [extensions]
16 > [extensions]
17 > histedit=
17 > histedit=
18 > EOF
18 > EOF
19
19
20
20
21 Simple folding
21 Simple folding
22 --------------------
22 --------------------
23 $ initrepo ()
23 $ initrepo ()
24 > {
24 > {
25 > hg init r
25 > hg init r
26 > cd r
26 > cd r
27 > for x in a b c d e f ; do
27 > for x in a b c d e f ; do
28 > echo $x > $x
28 > echo $x > $x
29 > hg add $x
29 > hg add $x
30 > hg ci -m $x
30 > hg ci -m $x
31 > done
31 > done
32 > }
32 > }
33
33
34 $ initrepo
34 $ initrepo
35
35
36 log before edit
36 log before edit
37 $ hg logt --graph
37 $ hg logt --graph
38 @ 5:652413bf663e f
38 @ 5:652413bf663e f
39 |
39 |
40 o 4:e860deea161a e
40 o 4:e860deea161a e
41 |
41 |
42 o 3:055a42cdd887 d
42 o 3:055a42cdd887 d
43 |
43 |
44 o 2:177f92b77385 c
44 o 2:177f92b77385 c
45 |
45 |
46 o 1:d2ae7f538514 b
46 o 1:d2ae7f538514 b
47 |
47 |
48 o 0:cb9a9f314b8b a
48 o 0:cb9a9f314b8b a
49
49
50
50
51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
51 $ hg histedit 177f92b77385 --commands - 2>&1 <<EOF | fixbundle
52 > pick e860deea161a e
52 > pick e860deea161a e
53 > pick 652413bf663e f
53 > pick 652413bf663e f
54 > fold 177f92b77385 c
54 > fold 177f92b77385 c
55 > pick 055a42cdd887 d
55 > pick 055a42cdd887 d
56 > EOF
56 > EOF
57 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
57 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
58 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
59 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
60 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
62 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
63 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
64
64
65 log after edit
65 log after edit
66 $ hg logt --graph
66 $ hg logt --graph
67 @ 4:9c277da72c9b d
67 @ 4:9c277da72c9b d
68 |
68 |
69 o 3:6de59d13424a f
69 o 3:6de59d13424a f
70 |
70 |
71 o 2:ee283cb5f2d5 e
71 o 2:ee283cb5f2d5 e
72 |
72 |
73 o 1:d2ae7f538514 b
73 o 1:d2ae7f538514 b
74 |
74 |
75 o 0:cb9a9f314b8b a
75 o 0:cb9a9f314b8b a
76
76
77
77
78 post-fold manifest
78 post-fold manifest
79 $ hg manifest
79 $ hg manifest
80 a
80 a
81 b
81 b
82 c
82 c
83 d
83 d
84 e
84 e
85 f
85 f
86
86
87
87
88 check histedit_source
88 check histedit_source
89
89
90 $ hg log --debug --rev 3
90 $ hg log --debug --rev 3
91 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
91 changeset: 3:6de59d13424a8a13acd3e975514aed29dd0d9b2d
92 phase: draft
92 phase: draft
93 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
93 parent: 2:ee283cb5f2d5955443f23a27b697a04339e9a39a
94 parent: -1:0000000000000000000000000000000000000000
94 parent: -1:0000000000000000000000000000000000000000
95 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
95 manifest: 3:81eede616954057198ead0b2c73b41d1f392829a
96 user: test
96 user: test
97 date: Thu Jan 01 00:00:00 1970 +0000
97 date: Thu Jan 01 00:00:00 1970 +0000
98 files+: c f
98 files+: c f
99 extra: branch=default
99 extra: branch=default
100 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
100 extra: histedit_source=a4f7421b80f79fcc59fff01bcbf4a53d127dd6d3,177f92b773850b59254aa5e923436f921b55483b
101 description:
101 description:
102 f
102 f
103 ***
103 ***
104 c
104 c
105
105
106
106
107
107
108 rollup will fold without preserving the folded commit's message
108 rollup will fold without preserving the folded commit's message
109
109
110 $ hg histedit d2ae7f538514 --commands - 2>&1 <<EOF | fixbundle
110 $ hg histedit d2ae7f538514 --commands - 2>&1 <<EOF | fixbundle
111 > pick d2ae7f538514 b
111 > pick d2ae7f538514 b
112 > roll ee283cb5f2d5 e
112 > roll ee283cb5f2d5 e
113 > pick 6de59d13424a f
113 > pick 6de59d13424a f
114 > pick 9c277da72c9b d
114 > pick 9c277da72c9b d
115 > EOF
115 > EOF
116 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
116 0 files updated, 0 files merged, 4 files removed, 0 files unresolved
117 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
117 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
118 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
118 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
121 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
122
122
123 log after edit
123 log after edit
124 $ hg logt --graph
124 $ hg logt --graph
125 @ 3:c4a9eb7989fc d
125 @ 3:c4a9eb7989fc d
126 |
126 |
127 o 2:8e03a72b6f83 f
127 o 2:8e03a72b6f83 f
128 |
128 |
129 o 1:391ee782c689 b
129 o 1:391ee782c689 b
130 |
130 |
131 o 0:cb9a9f314b8b a
131 o 0:cb9a9f314b8b a
132
132
133
133
134 description is taken from rollup target commit
134 description is taken from rollup target commit
135
135
136 $ hg log --debug --rev 1
136 $ hg log --debug --rev 1
137 changeset: 1:391ee782c68930be438ccf4c6a403daedbfbffa5
137 changeset: 1:391ee782c68930be438ccf4c6a403daedbfbffa5
138 phase: draft
138 phase: draft
139 parent: 0:cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
139 parent: 0:cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b
140 parent: -1:0000000000000000000000000000000000000000
140 parent: -1:0000000000000000000000000000000000000000
141 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
141 manifest: 1:b5e112a3a8354e269b1524729f0918662d847c38
142 user: test
142 user: test
143 date: Thu Jan 01 00:00:00 1970 +0000
143 date: Thu Jan 01 00:00:00 1970 +0000
144 files+: b e
144 files+: b e
145 extra: branch=default
145 extra: branch=default
146 extra: histedit_source=d2ae7f538514cd87c17547b0de4cea71fe1af9fb,ee283cb5f2d5955443f23a27b697a04339e9a39a
146 extra: histedit_source=d2ae7f538514cd87c17547b0de4cea71fe1af9fb,ee283cb5f2d5955443f23a27b697a04339e9a39a
147 description:
147 description:
148 b
148 b
149
149
150
150
151
151
152 check saving last-message.txt
152 check saving last-message.txt
153
153
154 $ cat > $TESTTMP/abortfolding.py <<EOF
154 $ cat > $TESTTMP/abortfolding.py <<EOF
155 > from mercurial import util
155 > from mercurial import util
156 > def abortfolding(ui, repo, hooktype, **kwargs):
156 > def abortfolding(ui, repo, hooktype, **kwargs):
157 > ctx = repo[kwargs.get('node')]
157 > ctx = repo[kwargs.get('node')]
158 > if set(ctx.files()) == set(['c', 'd', 'f']):
158 > if set(ctx.files()) == set(['c', 'd', 'f']):
159 > return True # abort folding commit only
159 > return True # abort folding commit only
160 > ui.warn('allow non-folding commit\\n')
160 > ui.warn('allow non-folding commit\\n')
161 > EOF
161 > EOF
162 $ cat > .hg/hgrc <<EOF
162 $ cat > .hg/hgrc <<EOF
163 > [hooks]
163 > [hooks]
164 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
164 > pretxncommit.abortfolding = python:$TESTTMP/abortfolding.py:abortfolding
165 > EOF
165 > EOF
166
166
167 $ cat > $TESTTMP/editor.sh << EOF
167 $ cat > $TESTTMP/editor.sh << EOF
168 > echo "==== before editing"
168 > echo "==== before editing"
169 > cat \$1
169 > cat \$1
170 > echo "===="
170 > echo "===="
171 > echo "check saving last-message.txt" >> \$1
171 > echo "check saving last-message.txt" >> \$1
172 > EOF
172 > EOF
173
173
174 $ rm -f .hg/last-message.txt
174 $ rm -f .hg/last-message.txt
175 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 8e03a72b6f83 --commands - 2>&1 <<EOF | fixbundle
175 $ HGEDITOR="sh $TESTTMP/editor.sh" hg histedit 8e03a72b6f83 --commands - 2>&1 <<EOF | fixbundle
176 > pick 8e03a72b6f83 f
176 > pick 8e03a72b6f83 f
177 > fold c4a9eb7989fc d
177 > fold c4a9eb7989fc d
178 > EOF
178 > EOF
179 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
179 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
180 allow non-folding commit
180 allow non-folding commit
181 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
181 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
182 ==== before editing
182 ==== before editing
183 f
183 f
184 ***
184 ***
185 c
185 c
186 ***
186 ***
187 d
187 d
188
188
189
189
190
190
191 HG: Enter commit message. Lines beginning with 'HG:' are removed.
191 HG: Enter commit message. Lines beginning with 'HG:' are removed.
192 HG: Leave message empty to abort commit.
192 HG: Leave message empty to abort commit.
193 HG: --
193 HG: --
194 HG: user: test
194 HG: user: test
195 HG: branch 'default'
195 HG: branch 'default'
196 HG: changed c
196 HG: changed c
197 HG: changed d
197 HG: changed d
198 HG: changed f
198 HG: changed f
199 ====
199 ====
200 transaction abort!
200 transaction abort!
201 rollback completed
201 rollback completed
202 abort: pretxncommit.abortfolding hook failed
202 abort: pretxncommit.abortfolding hook failed
203
203
204 $ cat .hg/last-message.txt
204 $ cat .hg/last-message.txt
205 f
205 f
206 ***
206 ***
207 c
207 c
208 ***
208 ***
209 d
209 d
210
210
211
211
212
212
213 check saving last-message.txt
213 check saving last-message.txt
214
214
215 $ cd ..
215 $ cd ..
216 $ rm -r r
217
218 folding preserves initial author
219 --------------------------------
220
221 $ initrepo
222
223 $ hg ci --user "someone else" --amend --quiet
224
225 tip before edit
226 $ hg log --rev .
227 changeset: 5:a00ad806cb55
228 tag: tip
229 user: someone else
230 date: Thu Jan 01 00:00:00 1970 +0000
231 summary: f
232
233
234 $ hg histedit e860deea161a --commands - 2>&1 <<EOF | fixbundle
235 > pick e860deea161a e
236 > fold a00ad806cb55 f
237 > EOF
238 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
239 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
240 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
241 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
242
243 tip after edit
244 $ hg log --rev .
245 changeset: 4:698d4e8040a1
246 tag: tip
247 user: test
248 date: Thu Jan 01 00:00:00 1970 +0000
249 summary: e
250
251
252 $ cd ..
253 $ rm -r r
216
254
217 folding and creating no new change doesn't break:
255 folding and creating no new change doesn't break:
218 -------------------------------------------------
256 -------------------------------------------------
219
257
220 folded content is dropped during a merge. The folded commit should properly disappear.
258 folded content is dropped during a merge. The folded commit should properly disappear.
221
259
222 $ mkdir fold-to-empty-test
260 $ mkdir fold-to-empty-test
223 $ cd fold-to-empty-test
261 $ cd fold-to-empty-test
224 $ hg init
262 $ hg init
225 $ printf "1\n2\n3\n" > file
263 $ printf "1\n2\n3\n" > file
226 $ hg add file
264 $ hg add file
227 $ hg commit -m '1+2+3'
265 $ hg commit -m '1+2+3'
228 $ echo 4 >> file
266 $ echo 4 >> file
229 $ hg commit -m '+4'
267 $ hg commit -m '+4'
230 $ echo 5 >> file
268 $ echo 5 >> file
231 $ hg commit -m '+5'
269 $ hg commit -m '+5'
232 $ echo 6 >> file
270 $ echo 6 >> file
233 $ hg commit -m '+6'
271 $ hg commit -m '+6'
234 $ hg logt --graph
272 $ hg logt --graph
235 @ 3:251d831eeec5 +6
273 @ 3:251d831eeec5 +6
236 |
274 |
237 o 2:888f9082bf99 +5
275 o 2:888f9082bf99 +5
238 |
276 |
239 o 1:617f94f13c0f +4
277 o 1:617f94f13c0f +4
240 |
278 |
241 o 0:0189ba417d34 1+2+3
279 o 0:0189ba417d34 1+2+3
242
280
243
281
244 $ hg histedit 1 --commands - << EOF
282 $ hg histedit 1 --commands - << EOF
245 > pick 617f94f13c0f 1 +4
283 > pick 617f94f13c0f 1 +4
246 > drop 888f9082bf99 2 +5
284 > drop 888f9082bf99 2 +5
247 > fold 251d831eeec5 3 +6
285 > fold 251d831eeec5 3 +6
248 > EOF
286 > EOF
249 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
287 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
250 merging file
288 merging file
251 warning: conflicts during merge.
289 warning: conflicts during merge.
252 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
290 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
253 Fix up the change and run hg histedit --continue
291 Fix up the change and run hg histedit --continue
254 [1]
292 [1]
255 There were conflicts, we keep P1 content. This
293 There were conflicts, we keep P1 content. This
256 should effectively drop the changes from +6.
294 should effectively drop the changes from +6.
257 $ hg status
295 $ hg status
258 M file
296 M file
259 ? file.orig
297 ? file.orig
260 $ hg resolve -l
298 $ hg resolve -l
261 U file
299 U file
262 $ hg revert -r 'p1()' file
300 $ hg revert -r 'p1()' file
263 $ hg resolve --mark file
301 $ hg resolve --mark file
264 (no more unresolved files)
302 (no more unresolved files)
265 $ hg histedit --continue
303 $ hg histedit --continue
266 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
304 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
267 saved backup bundle to $TESTTMP/*-backup.hg (glob)
305 saved backup bundle to $TESTTMP/*-backup.hg (glob)
268 $ hg logt --graph
306 $ hg logt --graph
269 @ 1:617f94f13c0f +4
307 @ 1:617f94f13c0f +4
270 |
308 |
271 o 0:0189ba417d34 1+2+3
309 o 0:0189ba417d34 1+2+3
272
310
273
311
274 $ cd ..
312 $ cd ..
275
313
276
314
277 Test fold through dropped
315 Test fold through dropped
278 -------------------------
316 -------------------------
279
317
280
318
281 Test corner case where folded revision is separated from its parent by a
319 Test corner case where folded revision is separated from its parent by a
282 dropped revision.
320 dropped revision.
283
321
284
322
285 $ hg init fold-with-dropped
323 $ hg init fold-with-dropped
286 $ cd fold-with-dropped
324 $ cd fold-with-dropped
287 $ printf "1\n2\n3\n" > file
325 $ printf "1\n2\n3\n" > file
288 $ hg commit -Am '1+2+3'
326 $ hg commit -Am '1+2+3'
289 adding file
327 adding file
290 $ echo 4 >> file
328 $ echo 4 >> file
291 $ hg commit -m '+4'
329 $ hg commit -m '+4'
292 $ echo 5 >> file
330 $ echo 5 >> file
293 $ hg commit -m '+5'
331 $ hg commit -m '+5'
294 $ echo 6 >> file
332 $ echo 6 >> file
295 $ hg commit -m '+6'
333 $ hg commit -m '+6'
296 $ hg logt -G
334 $ hg logt -G
297 @ 3:251d831eeec5 +6
335 @ 3:251d831eeec5 +6
298 |
336 |
299 o 2:888f9082bf99 +5
337 o 2:888f9082bf99 +5
300 |
338 |
301 o 1:617f94f13c0f +4
339 o 1:617f94f13c0f +4
302 |
340 |
303 o 0:0189ba417d34 1+2+3
341 o 0:0189ba417d34 1+2+3
304
342
305 $ hg histedit 1 --commands - << EOF
343 $ hg histedit 1 --commands - << EOF
306 > pick 617f94f13c0f 1 +4
344 > pick 617f94f13c0f 1 +4
307 > drop 888f9082bf99 2 +5
345 > drop 888f9082bf99 2 +5
308 > fold 251d831eeec5 3 +6
346 > fold 251d831eeec5 3 +6
309 > EOF
347 > EOF
310 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
348 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
311 merging file
349 merging file
312 warning: conflicts during merge.
350 warning: conflicts during merge.
313 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
351 merging file incomplete! (edit conflicts, then use 'hg resolve --mark')
314 Fix up the change and run hg histedit --continue
352 Fix up the change and run hg histedit --continue
315 [1]
353 [1]
316 $ cat > file << EOF
354 $ cat > file << EOF
317 > 1
355 > 1
318 > 2
356 > 2
319 > 3
357 > 3
320 > 4
358 > 4
321 > 5
359 > 5
322 > EOF
360 > EOF
323 $ hg resolve --mark file
361 $ hg resolve --mark file
324 (no more unresolved files)
362 (no more unresolved files)
325 $ hg commit -m '+5.2'
363 $ hg commit -m '+5.2'
326 created new head
364 created new head
327 $ echo 6 >> file
365 $ echo 6 >> file
328 $ HGEDITOR=cat hg histedit --continue
366 $ HGEDITOR=cat hg histedit --continue
329 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
367 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
330 +4
368 +4
331 ***
369 ***
332 +5.2
370 +5.2
333 ***
371 ***
334 +6
372 +6
335
373
336
374
337
375
338 HG: Enter commit message. Lines beginning with 'HG:' are removed.
376 HG: Enter commit message. Lines beginning with 'HG:' are removed.
339 HG: Leave message empty to abort commit.
377 HG: Leave message empty to abort commit.
340 HG: --
378 HG: --
341 HG: user: test
379 HG: user: test
342 HG: branch 'default'
380 HG: branch 'default'
343 HG: changed file
381 HG: changed file
344 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
382 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
345 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
383 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
346 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
384 saved backup bundle to $TESTTMP/fold-with-dropped/.hg/strip-backup/617f94f13c0f-backup.hg (glob)
347 $ hg logt -G
385 $ hg logt -G
348 @ 1:10c647b2cdd5 +4
386 @ 1:10c647b2cdd5 +4
349 |
387 |
350 o 0:0189ba417d34 1+2+3
388 o 0:0189ba417d34 1+2+3
351
389
352 $ hg export tip
390 $ hg export tip
353 # HG changeset patch
391 # HG changeset patch
354 # User test
392 # User test
355 # Date 0 0
393 # Date 0 0
356 # Thu Jan 01 00:00:00 1970 +0000
394 # Thu Jan 01 00:00:00 1970 +0000
357 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
395 # Node ID 10c647b2cdd54db0603ecb99b2ff5ce66d5a5323
358 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
396 # Parent 0189ba417d34df9dda55f88b637dcae9917b5964
359 +4
397 +4
360 ***
398 ***
361 +5.2
399 +5.2
362 ***
400 ***
363 +6
401 +6
364
402
365 diff -r 0189ba417d34 -r 10c647b2cdd5 file
403 diff -r 0189ba417d34 -r 10c647b2cdd5 file
366 --- a/file Thu Jan 01 00:00:00 1970 +0000
404 --- a/file Thu Jan 01 00:00:00 1970 +0000
367 +++ b/file Thu Jan 01 00:00:00 1970 +0000
405 +++ b/file Thu Jan 01 00:00:00 1970 +0000
368 @@ -1,3 +1,6 @@
406 @@ -1,3 +1,6 @@
369 1
407 1
370 2
408 2
371 3
409 3
372 +4
410 +4
373 +5
411 +5
374 +6
412 +6
375 $ cd ..
413 $ cd ..
376
414
377
415
378 Folding with initial rename (issue3729)
416 Folding with initial rename (issue3729)
379 ---------------------------------------
417 ---------------------------------------
380
418
381 $ hg init fold-rename
419 $ hg init fold-rename
382 $ cd fold-rename
420 $ cd fold-rename
383 $ echo a > a.txt
421 $ echo a > a.txt
384 $ hg add a.txt
422 $ hg add a.txt
385 $ hg commit -m a
423 $ hg commit -m a
386 $ hg rename a.txt b.txt
424 $ hg rename a.txt b.txt
387 $ hg commit -m rename
425 $ hg commit -m rename
388 $ echo b >> b.txt
426 $ echo b >> b.txt
389 $ hg commit -m b
427 $ hg commit -m b
390
428
391 $ hg logt --follow b.txt
429 $ hg logt --follow b.txt
392 2:e0371e0426bc b
430 2:e0371e0426bc b
393 1:1c4f440a8085 rename
431 1:1c4f440a8085 rename
394 0:6c795aa153cb a
432 0:6c795aa153cb a
395
433
396 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
434 $ hg histedit 1c4f440a8085 --commands - 2>&1 << EOF | fixbundle
397 > pick 1c4f440a8085 rename
435 > pick 1c4f440a8085 rename
398 > fold e0371e0426bc b
436 > fold e0371e0426bc b
399 > EOF
437 > EOF
400 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
438 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
401 reverting b.txt
439 reverting b.txt
402 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
440 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
403 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
441 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
404 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
442 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
405
443
406 $ hg logt --follow b.txt
444 $ hg logt --follow b.txt
407 1:cf858d235c76 rename
445 1:cf858d235c76 rename
408 0:6c795aa153cb a
446 0:6c795aa153cb a
409
447
410 $ cd ..
448 $ cd ..
General Comments 0
You need to be logged in to leave comments. Login now