##// END OF EJS Templates
drawdag: add a couple of doctests to help with python3 porting
Augie Fackler -
r34203:1e71dddc default
parent child Browse files
Show More
@@ -1,376 +1,433 b''
1 # drawdag.py - convert ASCII revision DAG to actual changesets
1 # drawdag.py - convert ASCII revision DAG to actual changesets
2 #
2 #
3 # Copyright 2016 Facebook, Inc.
3 # Copyright 2016 Facebook, Inc.
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 """
7 """
8 create changesets from an ASCII graph for testing purpose.
8 create changesets from an ASCII graph for testing purpose.
9
9
10 For example, given the following input::
10 For example, given the following input::
11
11
12 c d
12 c d
13 |/
13 |/
14 b
14 b
15 |
15 |
16 a
16 a
17
17
18 4 changesets and 4 local tags will be created.
18 4 changesets and 4 local tags will be created.
19 `hg log -G -T "{rev} {desc} (tag: {tags})"` will output::
19 `hg log -G -T "{rev} {desc} (tag: {tags})"` will output::
20
20
21 o 3 d (tag: d tip)
21 o 3 d (tag: d tip)
22 |
22 |
23 | o 2 c (tag: c)
23 | o 2 c (tag: c)
24 |/
24 |/
25 o 1 b (tag: b)
25 o 1 b (tag: b)
26 |
26 |
27 o 0 a (tag: a)
27 o 0 a (tag: a)
28
28
29 For root nodes (nodes without parents) in the graph, they can be revsets
29 For root nodes (nodes without parents) in the graph, they can be revsets
30 pointing to existing nodes. The ASCII graph could also have disconnected
30 pointing to existing nodes. The ASCII graph could also have disconnected
31 components with same names referring to the same changeset.
31 components with same names referring to the same changeset.
32
32
33 Therefore, given the repo having the 4 changesets (and tags) above, with the
33 Therefore, given the repo having the 4 changesets (and tags) above, with the
34 following ASCII graph as input::
34 following ASCII graph as input::
35
35
36 foo bar bar foo
36 foo bar bar foo
37 | / | |
37 | / | |
38 ancestor(c,d) a baz
38 ancestor(c,d) a baz
39
39
40 The result (`hg log -G -T "{desc}"`) will look like::
40 The result (`hg log -G -T "{desc}"`) will look like::
41
41
42 o foo
42 o foo
43 |\
43 |\
44 +---o bar
44 +---o bar
45 | | |
45 | | |
46 | o | baz
46 | o | baz
47 | /
47 | /
48 +---o d
48 +---o d
49 | |
49 | |
50 +---o c
50 +---o c
51 | |
51 | |
52 o | b
52 o | b
53 |/
53 |/
54 o a
54 o a
55
55
56 Note that if you take the above `hg log` output directly as input. It will work
56 Note that if you take the above `hg log` output directly as input. It will work
57 as expected - the result would be an isomorphic graph::
57 as expected - the result would be an isomorphic graph::
58
58
59 o foo
59 o foo
60 |\
60 |\
61 | | o d
61 | | o d
62 | |/
62 | |/
63 | | o c
63 | | o c
64 | |/
64 | |/
65 | | o bar
65 | | o bar
66 | |/|
66 | |/|
67 | o | b
67 | o | b
68 | |/
68 | |/
69 o / baz
69 o / baz
70 /
70 /
71 o a
71 o a
72
72
73 This is because 'o' is specially handled in the input: instead of using 'o' as
73 This is because 'o' is specially handled in the input: instead of using 'o' as
74 the node name, the word to the right will be used.
74 the node name, the word to the right will be used.
75
75
76 Some special comments could have side effects:
76 Some special comments could have side effects:
77
77
78 - Create obsmarkers
78 - Create obsmarkers
79 # replace: A -> B -> C -> D # chained 1 to 1 replacements
79 # replace: A -> B -> C -> D # chained 1 to 1 replacements
80 # split: A -> B, C # 1 to many
80 # split: A -> B, C # 1 to many
81 # prune: A, B, C # many to nothing
81 # prune: A, B, C # many to nothing
82 """
82 """
83 from __future__ import absolute_import, print_function
83 from __future__ import absolute_import, print_function
84
84
85 import collections
85 import collections
86 import itertools
86 import itertools
87 import re
87 import re
88
88
89 from mercurial.i18n import _
89 from mercurial.i18n import _
90 from mercurial import (
90 from mercurial import (
91 context,
91 context,
92 error,
92 error,
93 node,
93 node,
94 obsolete,
94 obsolete,
95 registrar,
95 registrar,
96 scmutil,
96 scmutil,
97 tags as tagsmod,
97 tags as tagsmod,
98 )
98 )
99
99
100 cmdtable = {}
100 cmdtable = {}
101 command = registrar.command(cmdtable)
101 command = registrar.command(cmdtable)
102
102
103 _pipechars = '\\/+-|'
103 _pipechars = '\\/+-|'
104 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
104 _nonpipechars = ''.join(chr(i) for i in xrange(33, 127)
105 if chr(i) not in _pipechars)
105 if chr(i) not in _pipechars)
106
106
107 def _isname(ch):
107 def _isname(ch):
108 """char -> bool. return True if ch looks like part of a name, False
108 """char -> bool. return True if ch looks like part of a name, False
109 otherwise"""
109 otherwise"""
110 return ch in _nonpipechars
110 return ch in _nonpipechars
111
111
112 def _parseasciigraph(text):
112 def _parseasciigraph(text):
113 """str -> {str : [str]}. convert the ASCII graph to edges"""
113 r"""str -> {str : [str]}. convert the ASCII graph to edges
114
115 >>> import pprint
116 >>> pprint.pprint({k: [vv for vv in v]
117 ... for k, v in _parseasciigraph(br'''
118 ... G
119 ... |
120 ... I D C F # split: B -> E, F, G
121 ... \ \| | # replace: C -> D -> H
122 ... H B E # prune: F, I
123 ... \|/
124 ... A
125 ... ''').items()})
126 {'A': [],
127 'B': ['A'],
128 'C': ['B'],
129 'D': ['B'],
130 'E': ['A'],
131 'F': ['E'],
132 'G': ['F'],
133 'H': ['A'],
134 'I': ['H']}
135 >>> pprint.pprint({k: [vv for vv in v]
136 ... for k, v in _parseasciigraph(br'''
137 ... o foo
138 ... |\
139 ... +---o bar
140 ... | | |
141 ... | o | baz
142 ... | /
143 ... +---o d
144 ... | |
145 ... +---o c
146 ... | |
147 ... o | b
148 ... |/
149 ... o a
150 ... ''').items()})
151 {'a': [],
152 'b': ['a'],
153 'bar': ['b', 'a'],
154 'baz': [],
155 'c': ['b'],
156 'd': ['b'],
157 'foo': ['baz', 'b']}
158 """
114 lines = text.splitlines()
159 lines = text.splitlines()
115 edges = collections.defaultdict(list) # {node: []}
160 edges = collections.defaultdict(list) # {node: []}
116
161
117 def get(y, x):
162 def get(y, x):
118 """(int, int) -> char. give a coordinate, return the char. return a
163 """(int, int) -> char. give a coordinate, return the char. return a
119 space for anything out of range"""
164 space for anything out of range"""
120 if x < 0 or y < 0:
165 if x < 0 or y < 0:
121 return ' '
166 return ' '
122 try:
167 try:
123 return lines[y][x]
168 return lines[y][x]
124 except IndexError:
169 except IndexError:
125 return ' '
170 return ' '
126
171
127 def getname(y, x):
172 def getname(y, x):
128 """(int, int) -> str. like get(y, x) but concatenate left and right
173 """(int, int) -> str. like get(y, x) but concatenate left and right
129 parts. if name is an 'o', try to replace it to the right"""
174 parts. if name is an 'o', try to replace it to the right"""
130 result = ''
175 result = ''
131 for i in itertools.count(0):
176 for i in itertools.count(0):
132 ch = get(y, x - i)
177 ch = get(y, x - i)
133 if not _isname(ch):
178 if not _isname(ch):
134 break
179 break
135 result = ch + result
180 result = ch + result
136 for i in itertools.count(1):
181 for i in itertools.count(1):
137 ch = get(y, x + i)
182 ch = get(y, x + i)
138 if not _isname(ch):
183 if not _isname(ch):
139 break
184 break
140 result += ch
185 result += ch
141 if result == 'o':
186 if result == 'o':
142 # special handling, find the name to the right
187 # special handling, find the name to the right
143 result = ''
188 result = ''
144 for i in itertools.count(2):
189 for i in itertools.count(2):
145 ch = get(y, x + i)
190 ch = get(y, x + i)
146 if ch == ' ' or ch in _pipechars:
191 if ch == ' ' or ch in _pipechars:
147 if result or x + i >= len(lines[y]):
192 if result or x + i >= len(lines[y]):
148 break
193 break
149 else:
194 else:
150 result += ch
195 result += ch
151 return result or 'o'
196 return result or 'o'
152 return result
197 return result
153
198
154 def parents(y, x):
199 def parents(y, x):
155 """(int, int) -> [str]. follow the ASCII edges at given position,
200 """(int, int) -> [str]. follow the ASCII edges at given position,
156 return a list of parents"""
201 return a list of parents"""
157 visited = {(y, x)}
202 visited = {(y, x)}
158 visit = []
203 visit = []
159 result = []
204 result = []
160
205
161 def follow(y, x, expected):
206 def follow(y, x, expected):
162 """conditionally append (y, x) to visit array, if it's a char
207 """conditionally append (y, x) to visit array, if it's a char
163 in excepted. 'o' in expected means an '_isname' test.
208 in excepted. 'o' in expected means an '_isname' test.
164 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
209 if '-' (or '+') is not in excepted, and get(y, x) is '-' (or '+'),
165 the next line (y + 1, x) will be checked instead."""
210 the next line (y + 1, x) will be checked instead."""
166 ch = get(y, x)
211 ch = get(y, x)
167 if any(ch == c and c not in expected for c in '-+'):
212 if any(ch == c and c not in expected for c in '-+'):
168 y += 1
213 y += 1
169 return follow(y + 1, x, expected)
214 return follow(y + 1, x, expected)
170 if ch in expected or ('o' in expected and _isname(ch)):
215 if ch in expected or ('o' in expected and _isname(ch)):
171 visit.append((y, x))
216 visit.append((y, x))
172
217
173 # -o- # starting point:
218 # -o- # starting point:
174 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
219 # /|\ # follow '-' (horizontally), and '/|\' (to the bottom)
175 follow(y + 1, x, '|')
220 follow(y + 1, x, '|')
176 follow(y + 1, x - 1, '/')
221 follow(y + 1, x - 1, '/')
177 follow(y + 1, x + 1, '\\')
222 follow(y + 1, x + 1, '\\')
178 follow(y, x - 1, '-')
223 follow(y, x - 1, '-')
179 follow(y, x + 1, '-')
224 follow(y, x + 1, '-')
180
225
181 while visit:
226 while visit:
182 y, x = visit.pop()
227 y, x = visit.pop()
183 if (y, x) in visited:
228 if (y, x) in visited:
184 continue
229 continue
185 visited.add((y, x))
230 visited.add((y, x))
186 ch = get(y, x)
231 ch = get(y, x)
187 if _isname(ch):
232 if _isname(ch):
188 result.append(getname(y, x))
233 result.append(getname(y, x))
189 continue
234 continue
190 elif ch == '|':
235 elif ch == '|':
191 follow(y + 1, x, '/|o')
236 follow(y + 1, x, '/|o')
192 follow(y + 1, x - 1, '/')
237 follow(y + 1, x - 1, '/')
193 follow(y + 1, x + 1, '\\')
238 follow(y + 1, x + 1, '\\')
194 elif ch == '+':
239 elif ch == '+':
195 follow(y, x - 1, '-')
240 follow(y, x - 1, '-')
196 follow(y, x + 1, '-')
241 follow(y, x + 1, '-')
197 follow(y + 1, x - 1, '/')
242 follow(y + 1, x - 1, '/')
198 follow(y + 1, x + 1, '\\')
243 follow(y + 1, x + 1, '\\')
199 follow(y + 1, x, '|')
244 follow(y + 1, x, '|')
200 elif ch == '\\':
245 elif ch == '\\':
201 follow(y + 1, x + 1, '\\|o')
246 follow(y + 1, x + 1, '\\|o')
202 elif ch == '/':
247 elif ch == '/':
203 follow(y + 1, x - 1, '/|o')
248 follow(y + 1, x - 1, '/|o')
204 elif ch == '-':
249 elif ch == '-':
205 follow(y, x - 1, '-+o')
250 follow(y, x - 1, '-+o')
206 follow(y, x + 1, '-+o')
251 follow(y, x + 1, '-+o')
207 return result
252 return result
208
253
209 for y, line in enumerate(lines):
254 for y, line in enumerate(lines):
210 for x, ch in enumerate(line):
255 for x, ch in enumerate(line):
211 if ch == '#': # comment
256 if ch == '#': # comment
212 break
257 break
213 if _isname(ch):
258 if _isname(ch):
214 edges[getname(y, x)] += parents(y, x)
259 edges[getname(y, x)] += parents(y, x)
215
260
216 return dict(edges)
261 return dict(edges)
217
262
218 class simplefilectx(object):
263 class simplefilectx(object):
219 def __init__(self, path, data):
264 def __init__(self, path, data):
220 self._data = data
265 self._data = data
221 self._path = path
266 self._path = path
222
267
223 def data(self):
268 def data(self):
224 return self._data
269 return self._data
225
270
226 def filenode(self):
271 def filenode(self):
227 return None
272 return None
228
273
229 def path(self):
274 def path(self):
230 return self._path
275 return self._path
231
276
232 def renamed(self):
277 def renamed(self):
233 return None
278 return None
234
279
235 def flags(self):
280 def flags(self):
236 return ''
281 return ''
237
282
238 class simplecommitctx(context.committablectx):
283 class simplecommitctx(context.committablectx):
239 def __init__(self, repo, name, parentctxs, added):
284 def __init__(self, repo, name, parentctxs, added):
240 opts = {
285 opts = {
241 'changes': scmutil.status([], list(added), [], [], [], [], []),
286 'changes': scmutil.status([], list(added), [], [], [], [], []),
242 'date': '0 0',
287 'date': '0 0',
243 'extra': {'branch': 'default'},
288 'extra': {'branch': 'default'},
244 }
289 }
245 super(simplecommitctx, self).__init__(self, name, **opts)
290 super(simplecommitctx, self).__init__(self, name, **opts)
246 self._repo = repo
291 self._repo = repo
247 self._added = added
292 self._added = added
248 self._parents = parentctxs
293 self._parents = parentctxs
249 while len(self._parents) < 2:
294 while len(self._parents) < 2:
250 self._parents.append(repo[node.nullid])
295 self._parents.append(repo[node.nullid])
251
296
252 def filectx(self, key):
297 def filectx(self, key):
253 return simplefilectx(key, self._added[key])
298 return simplefilectx(key, self._added[key])
254
299
255 def commit(self):
300 def commit(self):
256 return self._repo.commitctx(self)
301 return self._repo.commitctx(self)
257
302
258 def _walkgraph(edges):
303 def _walkgraph(edges):
259 """yield node, parents in topologically order"""
304 """yield node, parents in topologically order"""
260 visible = set(edges.keys())
305 visible = set(edges.keys())
261 remaining = {} # {str: [str]}
306 remaining = {} # {str: [str]}
262 for k, vs in edges.iteritems():
307 for k, vs in edges.iteritems():
263 for v in vs:
308 for v in vs:
264 if v not in remaining:
309 if v not in remaining:
265 remaining[v] = []
310 remaining[v] = []
266 remaining[k] = vs[:]
311 remaining[k] = vs[:]
267 while remaining:
312 while remaining:
268 leafs = [k for k, v in remaining.items() if not v]
313 leafs = [k for k, v in remaining.items() if not v]
269 if not leafs:
314 if not leafs:
270 raise error.Abort(_('the graph has cycles'))
315 raise error.Abort(_('the graph has cycles'))
271 for leaf in sorted(leafs):
316 for leaf in sorted(leafs):
272 if leaf in visible:
317 if leaf in visible:
273 yield leaf, edges[leaf]
318 yield leaf, edges[leaf]
274 del remaining[leaf]
319 del remaining[leaf]
275 for k, v in remaining.iteritems():
320 for k, v in remaining.iteritems():
276 if leaf in v:
321 if leaf in v:
277 v.remove(leaf)
322 v.remove(leaf)
278
323
279 def _getcomments(text):
324 def _getcomments(text):
325 """
326 >>> [s for s in _getcomments(br'''
327 ... G
328 ... |
329 ... I D C F # split: B -> E, F, G
330 ... \ \| | # replace: C -> D -> H
331 ... H B E # prune: F, I
332 ... \|/
333 ... A
334 ... ''')]
335 ['split: B -> E, F, G', 'replace: C -> D -> H', 'prune: F, I']
336 """
280 for line in text.splitlines():
337 for line in text.splitlines():
281 if ' # ' not in line:
338 if ' # ' not in line:
282 continue
339 continue
283 yield line.split(' # ', 1)[1].split(' # ')[0].strip()
340 yield line.split(' # ', 1)[1].split(' # ')[0].strip()
284
341
285 @command('debugdrawdag', [])
342 @command('debugdrawdag', [])
286 def debugdrawdag(ui, repo, **opts):
343 def debugdrawdag(ui, repo, **opts):
287 """read an ASCII graph from stdin and create changesets
344 """read an ASCII graph from stdin and create changesets
288
345
289 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
346 The ASCII graph is like what :hg:`log -G` outputs, with each `o` replaced
290 to the name of the node. The command will create dummy changesets and local
347 to the name of the node. The command will create dummy changesets and local
291 tags with those names to make the dummy changesets easier to be referred
348 tags with those names to make the dummy changesets easier to be referred
292 to.
349 to.
293
350
294 If the name of a node is a single character 'o', It will be replaced by the
351 If the name of a node is a single character 'o', It will be replaced by the
295 word to the right. This makes it easier to reuse
352 word to the right. This makes it easier to reuse
296 :hg:`log -G -T '{desc}'` outputs.
353 :hg:`log -G -T '{desc}'` outputs.
297
354
298 For root (no parents) nodes, revset can be used to query existing repo.
355 For root (no parents) nodes, revset can be used to query existing repo.
299 Note that the revset cannot have confusing characters which can be seen as
356 Note that the revset cannot have confusing characters which can be seen as
300 the part of the graph edges, like `|/+-\`.
357 the part of the graph edges, like `|/+-\`.
301 """
358 """
302 text = ui.fin.read()
359 text = ui.fin.read()
303
360
304 # parse the graph and make sure len(parents) <= 2 for each node
361 # parse the graph and make sure len(parents) <= 2 for each node
305 edges = _parseasciigraph(text)
362 edges = _parseasciigraph(text)
306 for k, v in edges.iteritems():
363 for k, v in edges.iteritems():
307 if len(v) > 2:
364 if len(v) > 2:
308 raise error.Abort(_('%s: too many parents: %s')
365 raise error.Abort(_('%s: too many parents: %s')
309 % (k, ' '.join(v)))
366 % (k, ' '.join(v)))
310
367
311 # parse comments to get extra file content instructions
368 # parse comments to get extra file content instructions
312 files = collections.defaultdict(dict) # {(name, path): content}
369 files = collections.defaultdict(dict) # {(name, path): content}
313 comments = list(_getcomments(text))
370 comments = list(_getcomments(text))
314 filere = re.compile(r'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
371 filere = re.compile(r'^(\w+)/([\w/]+)\s*=\s*(.*)$', re.M)
315 for name, path, content in filere.findall('\n'.join(comments)):
372 for name, path, content in filere.findall('\n'.join(comments)):
316 files[name][path] = content.replace(r'\n', '\n')
373 files[name][path] = content.replace(r'\n', '\n')
317
374
318 committed = {None: node.nullid} # {name: node}
375 committed = {None: node.nullid} # {name: node}
319
376
320 # for leaf nodes, try to find existing nodes in repo
377 # for leaf nodes, try to find existing nodes in repo
321 for name, parents in edges.iteritems():
378 for name, parents in edges.iteritems():
322 if len(parents) == 0:
379 if len(parents) == 0:
323 try:
380 try:
324 committed[name] = scmutil.revsingle(repo, name)
381 committed[name] = scmutil.revsingle(repo, name)
325 except error.RepoLookupError:
382 except error.RepoLookupError:
326 pass
383 pass
327
384
328 # commit in topological order
385 # commit in topological order
329 for name, parents in _walkgraph(edges):
386 for name, parents in _walkgraph(edges):
330 if name in committed:
387 if name in committed:
331 continue
388 continue
332 pctxs = [repo[committed[n]] for n in parents]
389 pctxs = [repo[committed[n]] for n in parents]
333 pctxs.sort(key=lambda c: c.node())
390 pctxs.sort(key=lambda c: c.node())
334 added = {}
391 added = {}
335 if len(parents) > 1:
392 if len(parents) > 1:
336 # If it's a merge, take the files and contents from the parents
393 # If it's a merge, take the files and contents from the parents
337 for f in pctxs[1].manifest():
394 for f in pctxs[1].manifest():
338 if f not in pctxs[0].manifest():
395 if f not in pctxs[0].manifest():
339 added[f] = pctxs[1][f].data()
396 added[f] = pctxs[1][f].data()
340 else:
397 else:
341 # If it's not a merge, add a single file
398 # If it's not a merge, add a single file
342 added[name] = name
399 added[name] = name
343 # add extra file contents in comments
400 # add extra file contents in comments
344 for path, content in files.get(name, {}).items():
401 for path, content in files.get(name, {}).items():
345 added[path] = content
402 added[path] = content
346 ctx = simplecommitctx(repo, name, pctxs, added)
403 ctx = simplecommitctx(repo, name, pctxs, added)
347 n = ctx.commit()
404 n = ctx.commit()
348 committed[name] = n
405 committed[name] = n
349 tagsmod.tag(repo, [name], n, message=None, user=None, date=None,
406 tagsmod.tag(repo, [name], n, message=None, user=None, date=None,
350 local=True)
407 local=True)
351
408
352 # handle special comments
409 # handle special comments
353 with repo.wlock(), repo.lock(), repo.transaction('drawdag'):
410 with repo.wlock(), repo.lock(), repo.transaction('drawdag'):
354 getctx = lambda x: repo.unfiltered()[committed[x.strip()]]
411 getctx = lambda x: repo.unfiltered()[committed[x.strip()]]
355 for comment in comments:
412 for comment in comments:
356 rels = [] # obsolete relationships
413 rels = [] # obsolete relationships
357 args = comment.split(':', 1)
414 args = comment.split(':', 1)
358 if len(args) <= 1:
415 if len(args) <= 1:
359 continue
416 continue
360
417
361 cmd = args[0].strip()
418 cmd = args[0].strip()
362 arg = args[1].strip()
419 arg = args[1].strip()
363
420
364 if cmd in ('replace', 'rebase', 'amend'):
421 if cmd in ('replace', 'rebase', 'amend'):
365 nodes = [getctx(m) for m in arg.split('->')]
422 nodes = [getctx(m) for m in arg.split('->')]
366 for i in range(len(nodes) - 1):
423 for i in range(len(nodes) - 1):
367 rels.append((nodes[i], (nodes[i + 1],)))
424 rels.append((nodes[i], (nodes[i + 1],)))
368 elif cmd in ('split',):
425 elif cmd in ('split',):
369 pre, succs = arg.split('->')
426 pre, succs = arg.split('->')
370 succs = succs.split(',')
427 succs = succs.split(',')
371 rels.append((getctx(pre), (getctx(s) for s in succs)))
428 rels.append((getctx(pre), (getctx(s) for s in succs)))
372 elif cmd in ('prune',):
429 elif cmd in ('prune',):
373 for n in arg.split(','):
430 for n in arg.split(','):
374 rels.append((getctx(n), ()))
431 rels.append((getctx(n), ()))
375 if rels:
432 if rels:
376 obsolete.createmarkers(repo, rels, date=(0, 0), operation=cmd)
433 obsolete.createmarkers(repo, rels, date=(0, 0), operation=cmd)
@@ -1,80 +1,82 b''
1 # this is hack to make sure no escape characters are inserted into the output
1 # this is hack to make sure no escape characters are inserted into the output
2
2
3 from __future__ import absolute_import
3 from __future__ import absolute_import
4
4
5 import doctest
5 import doctest
6 import os
6 import os
7 import re
7 import re
8 import sys
8 import sys
9
9
10 ispy3 = (sys.version_info[0] >= 3)
10 ispy3 = (sys.version_info[0] >= 3)
11
11
12 if 'TERM' in os.environ:
12 if 'TERM' in os.environ:
13 del os.environ['TERM']
13 del os.environ['TERM']
14
14
15 class py3docchecker(doctest.OutputChecker):
15 class py3docchecker(doctest.OutputChecker):
16 def check_output(self, want, got, optionflags):
16 def check_output(self, want, got, optionflags):
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
17 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
18 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
19 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
20 # <exc.name>: <others> -> <name>: <others>
20 # <exc.name>: <others> -> <name>: <others>
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
21 got2 = re.sub(r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''', r'\1: \3',
22 got2, re.MULTILINE)
22 got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
23 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
24 return any(doctest.OutputChecker.check_output(self, w, g, optionflags)
25 for w, g in [(want, got), (want2, got2)])
25 for w, g in [(want, got), (want2, got2)])
26
26
27 # TODO: migrate doctests to py3 and enable them on both versions
27 # TODO: migrate doctests to py3 and enable them on both versions
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
28 def testmod(name, optionflags=0, testtarget=None, py2=True, py3=True):
29 if not (not ispy3 and py2 or ispy3 and py3):
29 if not (not ispy3 and py2 or ispy3 and py3):
30 return
30 return
31 __import__(name)
31 __import__(name)
32 mod = sys.modules[name]
32 mod = sys.modules[name]
33 if testtarget is not None:
33 if testtarget is not None:
34 mod = getattr(mod, testtarget)
34 mod = getattr(mod, testtarget)
35
35
36 # minimal copy of doctest.testmod()
36 # minimal copy of doctest.testmod()
37 finder = doctest.DocTestFinder()
37 finder = doctest.DocTestFinder()
38 checker = None
38 checker = None
39 if ispy3:
39 if ispy3:
40 checker = py3docchecker()
40 checker = py3docchecker()
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
41 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
42 for test in finder.find(mod, name):
42 for test in finder.find(mod, name):
43 runner.run(test)
43 runner.run(test)
44 runner.summarize()
44 runner.summarize()
45
45
46 testmod('mercurial.changegroup')
46 testmod('mercurial.changegroup')
47 testmod('mercurial.changelog')
47 testmod('mercurial.changelog')
48 testmod('mercurial.color')
48 testmod('mercurial.color')
49 testmod('mercurial.config')
49 testmod('mercurial.config')
50 testmod('mercurial.context')
50 testmod('mercurial.context')
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE,
51 testmod('mercurial.dagparser', optionflags=doctest.NORMALIZE_WHITESPACE,
52 py3=False) # py3: use of str()
52 py3=False) # py3: use of str()
53 testmod('mercurial.dispatch')
53 testmod('mercurial.dispatch')
54 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
54 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
55 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
55 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
56 testmod('mercurial.hg')
56 testmod('mercurial.hg')
57 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
57 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
58 testmod('mercurial.match')
58 testmod('mercurial.match')
59 testmod('mercurial.mdiff')
59 testmod('mercurial.mdiff')
60 testmod('mercurial.minirst')
60 testmod('mercurial.minirst')
61 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
61 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
62 testmod('mercurial.pathutil', py3=False) # py3: os.sep
62 testmod('mercurial.pathutil', py3=False) # py3: os.sep
63 testmod('mercurial.parser')
63 testmod('mercurial.parser')
64 testmod('mercurial.pycompat')
64 testmod('mercurial.pycompat')
65 testmod('mercurial.revsetlang')
65 testmod('mercurial.revsetlang')
66 testmod('mercurial.smartset')
66 testmod('mercurial.smartset')
67 testmod('mercurial.store', py3=False) # py3: bytes[n]
67 testmod('mercurial.store', py3=False) # py3: bytes[n]
68 testmod('mercurial.subrepo')
68 testmod('mercurial.subrepo')
69 testmod('mercurial.templatefilters')
69 testmod('mercurial.templatefilters')
70 testmod('mercurial.templater')
70 testmod('mercurial.templater')
71 testmod('mercurial.ui', py3=False) # py3: __name__
71 testmod('mercurial.ui', py3=False) # py3: __name__
72 testmod('mercurial.url')
72 testmod('mercurial.url')
73 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
73 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
74 testmod('mercurial.util', testtarget='platform')
74 testmod('mercurial.util', testtarget='platform')
75 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
75 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
76 testmod('hgext.convert.cvsps')
76 testmod('hgext.convert.cvsps')
77 testmod('hgext.convert.filemap')
77 testmod('hgext.convert.filemap')
78 testmod('hgext.convert.p4')
78 testmod('hgext.convert.p4')
79 testmod('hgext.convert.subversion')
79 testmod('hgext.convert.subversion')
80 testmod('hgext.mq')
80 testmod('hgext.mq')
81 # Helper scripts in tests/ that have doctests:
82 testmod('drawdag')
General Comments 0
You need to be logged in to leave comments. Login now