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