##// END OF EJS Templates
py3: iterate bytes as a byte string in store.lowerencode()
Yuya Nishihara -
r34212:b4abc438 default
parent child Browse files
Show More
@@ -1,578 +1,578 b''
1 1 # store.py - repository store handling for Mercurial
2 2 #
3 3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import hashlib
12 12 import os
13 13 import stat
14 14
15 15 from .i18n import _
16 16 from . import (
17 17 error,
18 18 policy,
19 19 pycompat,
20 20 util,
21 21 vfs as vfsmod,
22 22 )
23 23
24 24 parsers = policy.importmod(r'parsers')
25 25
26 26 # This avoids a collision between a file named foo and a dir named
27 27 # foo.i or foo.d
28 28 def _encodedir(path):
29 29 '''
30 30 >>> _encodedir(b'data/foo.i')
31 31 'data/foo.i'
32 32 >>> _encodedir(b'data/foo.i/bla.i')
33 33 'data/foo.i.hg/bla.i'
34 34 >>> _encodedir(b'data/foo.i.hg/bla.i')
35 35 'data/foo.i.hg.hg/bla.i'
36 36 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
37 37 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
38 38 '''
39 39 return (path
40 40 .replace(".hg/", ".hg.hg/")
41 41 .replace(".i/", ".i.hg/")
42 42 .replace(".d/", ".d.hg/"))
43 43
44 44 encodedir = getattr(parsers, 'encodedir', _encodedir)
45 45
46 46 def decodedir(path):
47 47 '''
48 48 >>> decodedir(b'data/foo.i')
49 49 'data/foo.i'
50 50 >>> decodedir(b'data/foo.i.hg/bla.i')
51 51 'data/foo.i/bla.i'
52 52 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
53 53 'data/foo.i.hg/bla.i'
54 54 '''
55 55 if ".hg/" not in path:
56 56 return path
57 57 return (path
58 58 .replace(".d.hg/", ".d/")
59 59 .replace(".i.hg/", ".i/")
60 60 .replace(".hg.hg/", ".hg/"))
61 61
62 62 def _reserved():
63 63 ''' characters that are problematic for filesystems
64 64
65 65 * ascii escapes (0..31)
66 66 * ascii hi (126..255)
67 67 * windows specials
68 68
69 69 these characters will be escaped by encodefunctions
70 70 '''
71 71 winreserved = [ord(x) for x in u'\\:*?"<>|']
72 72 for x in range(32):
73 73 yield x
74 74 for x in range(126, 256):
75 75 yield x
76 76 for x in winreserved:
77 77 yield x
78 78
79 79 def _buildencodefun():
80 80 '''
81 81 >>> enc, dec = _buildencodefun()
82 82
83 83 >>> enc(b'nothing/special.txt')
84 84 'nothing/special.txt'
85 85 >>> dec(b'nothing/special.txt')
86 86 'nothing/special.txt'
87 87
88 88 >>> enc(b'HELLO')
89 89 '_h_e_l_l_o'
90 90 >>> dec(b'_h_e_l_l_o')
91 91 'HELLO'
92 92
93 93 >>> enc(b'hello:world?')
94 94 'hello~3aworld~3f'
95 95 >>> dec(b'hello~3aworld~3f')
96 96 'hello:world?'
97 97
98 98 >>> enc(b'the\\x07quick\\xADshot')
99 99 'the~07quick~adshot'
100 100 >>> dec(b'the~07quick~adshot')
101 101 'the\\x07quick\\xadshot'
102 102 '''
103 103 e = '_'
104 104 xchr = pycompat.bytechr
105 105 asciistr = list(map(xchr, range(127)))
106 106 capitals = list(range(ord("A"), ord("Z") + 1))
107 107
108 108 cmap = dict((x, x) for x in asciistr)
109 109 for x in _reserved():
110 110 cmap[xchr(x)] = "~%02x" % x
111 111 for x in capitals + [ord(e)]:
112 112 cmap[xchr(x)] = e + xchr(x).lower()
113 113
114 114 dmap = {}
115 115 for k, v in cmap.iteritems():
116 116 dmap[v] = k
117 117 def decode(s):
118 118 i = 0
119 119 while i < len(s):
120 120 for l in xrange(1, 4):
121 121 try:
122 122 yield dmap[s[i:i + l]]
123 123 i += l
124 124 break
125 125 except KeyError:
126 126 pass
127 127 else:
128 128 raise KeyError
129 129 return (lambda s: ''.join([cmap[s[c:c + 1]] for c in xrange(len(s))]),
130 130 lambda s: ''.join(list(decode(s))))
131 131
132 132 _encodefname, _decodefname = _buildencodefun()
133 133
134 134 def encodefilename(s):
135 135 '''
136 136 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
137 137 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
138 138 '''
139 139 return _encodefname(encodedir(s))
140 140
141 141 def decodefilename(s):
142 142 '''
143 143 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
144 144 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
145 145 '''
146 146 return decodedir(_decodefname(s))
147 147
148 148 def _buildlowerencodefun():
149 149 '''
150 150 >>> f = _buildlowerencodefun()
151 151 >>> f(b'nothing/special.txt')
152 152 'nothing/special.txt'
153 153 >>> f(b'HELLO')
154 154 'hello'
155 155 >>> f(b'hello:world?')
156 156 'hello~3aworld~3f'
157 157 >>> f(b'the\\x07quick\\xADshot')
158 158 'the~07quick~adshot'
159 159 '''
160 160 xchr = pycompat.bytechr
161 161 cmap = dict([(xchr(x), xchr(x)) for x in xrange(127)])
162 162 for x in _reserved():
163 163 cmap[xchr(x)] = "~%02x" % x
164 164 for x in range(ord("A"), ord("Z") + 1):
165 165 cmap[xchr(x)] = xchr(x).lower()
166 166 def lowerencode(s):
167 return "".join([cmap[c] for c in s])
167 return "".join([cmap[c] for c in pycompat.iterbytestr(s)])
168 168 return lowerencode
169 169
170 170 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
171 171
172 172 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
173 173 _winres3 = ('aux', 'con', 'prn', 'nul') # length 3
174 174 _winres4 = ('com', 'lpt') # length 4 (with trailing 1..9)
175 175 def _auxencode(path, dotencode):
176 176 '''
177 177 Encodes filenames containing names reserved by Windows or which end in
178 178 period or space. Does not touch other single reserved characters c.
179 179 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
180 180 Additionally encodes space or period at the beginning, if dotencode is
181 181 True. Parameter path is assumed to be all lowercase.
182 182 A segment only needs encoding if a reserved name appears as a
183 183 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
184 184 doesn't need encoding.
185 185
186 186 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
187 187 >>> _auxencode(s.split(b'/'), True)
188 188 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
189 189 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
190 190 >>> _auxencode(s.split(b'/'), False)
191 191 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
192 192 >>> _auxencode([b'foo. '], True)
193 193 ['foo.~20']
194 194 >>> _auxencode([b' .foo'], True)
195 195 ['~20.foo']
196 196 '''
197 197 for i, n in enumerate(path):
198 198 if not n:
199 199 continue
200 200 if dotencode and n[0] in '. ':
201 201 n = "~%02x" % ord(n[0:1]) + n[1:]
202 202 path[i] = n
203 203 else:
204 204 l = n.find('.')
205 205 if l == -1:
206 206 l = len(n)
207 207 if ((l == 3 and n[:3] in _winres3) or
208 208 (l == 4 and n[3:4] <= '9' and n[3:4] >= '1'
209 209 and n[:3] in _winres4)):
210 210 # encode third letter ('aux' -> 'au~78')
211 211 ec = "~%02x" % ord(n[2:3])
212 212 n = n[0:2] + ec + n[3:]
213 213 path[i] = n
214 214 if n[-1] in '. ':
215 215 # encode last period or space ('foo...' -> 'foo..~2e')
216 216 path[i] = n[:-1] + "~%02x" % ord(n[-1:])
217 217 return path
218 218
219 219 _maxstorepathlen = 120
220 220 _dirprefixlen = 8
221 221 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
222 222
223 223 def _hashencode(path, dotencode):
224 224 digest = hashlib.sha1(path).hexdigest()
225 225 le = lowerencode(path[5:]).split('/') # skips prefix 'data/' or 'meta/'
226 226 parts = _auxencode(le, dotencode)
227 227 basename = parts[-1]
228 228 _root, ext = os.path.splitext(basename)
229 229 sdirs = []
230 230 sdirslen = 0
231 231 for p in parts[:-1]:
232 232 d = p[:_dirprefixlen]
233 233 if d[-1] in '. ':
234 234 # Windows can't access dirs ending in period or space
235 235 d = d[:-1] + '_'
236 236 if sdirslen == 0:
237 237 t = len(d)
238 238 else:
239 239 t = sdirslen + 1 + len(d)
240 240 if t > _maxshortdirslen:
241 241 break
242 242 sdirs.append(d)
243 243 sdirslen = t
244 244 dirs = '/'.join(sdirs)
245 245 if len(dirs) > 0:
246 246 dirs += '/'
247 247 res = 'dh/' + dirs + digest + ext
248 248 spaceleft = _maxstorepathlen - len(res)
249 249 if spaceleft > 0:
250 250 filler = basename[:spaceleft]
251 251 res = 'dh/' + dirs + filler + digest + ext
252 252 return res
253 253
254 254 def _hybridencode(path, dotencode):
255 255 '''encodes path with a length limit
256 256
257 257 Encodes all paths that begin with 'data/', according to the following.
258 258
259 259 Default encoding (reversible):
260 260
261 261 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
262 262 characters are encoded as '~xx', where xx is the two digit hex code
263 263 of the character (see encodefilename).
264 264 Relevant path components consisting of Windows reserved filenames are
265 265 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
266 266
267 267 Hashed encoding (not reversible):
268 268
269 269 If the default-encoded path is longer than _maxstorepathlen, a
270 270 non-reversible hybrid hashing of the path is done instead.
271 271 This encoding uses up to _dirprefixlen characters of all directory
272 272 levels of the lowerencoded path, but not more levels than can fit into
273 273 _maxshortdirslen.
274 274 Then follows the filler followed by the sha digest of the full path.
275 275 The filler is the beginning of the basename of the lowerencoded path
276 276 (the basename is everything after the last path separator). The filler
277 277 is as long as possible, filling in characters from the basename until
278 278 the encoded path has _maxstorepathlen characters (or all chars of the
279 279 basename have been taken).
280 280 The extension (e.g. '.i' or '.d') is preserved.
281 281
282 282 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
283 283 encoding was used.
284 284 '''
285 285 path = encodedir(path)
286 286 ef = _encodefname(path).split('/')
287 287 res = '/'.join(_auxencode(ef, dotencode))
288 288 if len(res) > _maxstorepathlen:
289 289 res = _hashencode(path, dotencode)
290 290 return res
291 291
292 292 def _pathencode(path):
293 293 de = encodedir(path)
294 294 if len(path) > _maxstorepathlen:
295 295 return _hashencode(de, True)
296 296 ef = _encodefname(de).split('/')
297 297 res = '/'.join(_auxencode(ef, True))
298 298 if len(res) > _maxstorepathlen:
299 299 return _hashencode(de, True)
300 300 return res
301 301
302 302 _pathencode = getattr(parsers, 'pathencode', _pathencode)
303 303
304 304 def _plainhybridencode(f):
305 305 return _hybridencode(f, False)
306 306
307 307 def _calcmode(vfs):
308 308 try:
309 309 # files in .hg/ will be created using this mode
310 310 mode = vfs.stat().st_mode
311 311 # avoid some useless chmods
312 312 if (0o777 & ~util.umask) == (0o777 & mode):
313 313 mode = None
314 314 except OSError:
315 315 mode = None
316 316 return mode
317 317
318 318 _data = ('data meta 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
319 319 ' phaseroots obsstore')
320 320
321 321 class basicstore(object):
322 322 '''base class for local repository stores'''
323 323 def __init__(self, path, vfstype):
324 324 vfs = vfstype(path)
325 325 self.path = vfs.base
326 326 self.createmode = _calcmode(vfs)
327 327 vfs.createmode = self.createmode
328 328 self.rawvfs = vfs
329 329 self.vfs = vfsmod.filtervfs(vfs, encodedir)
330 330 self.opener = self.vfs
331 331
332 332 def join(self, f):
333 333 return self.path + '/' + encodedir(f)
334 334
335 335 def _walk(self, relpath, recurse):
336 336 '''yields (unencoded, encoded, size)'''
337 337 path = self.path
338 338 if relpath:
339 339 path += '/' + relpath
340 340 striplen = len(self.path) + 1
341 341 l = []
342 342 if self.rawvfs.isdir(path):
343 343 visit = [path]
344 344 readdir = self.rawvfs.readdir
345 345 while visit:
346 346 p = visit.pop()
347 347 for f, kind, st in readdir(p, stat=True):
348 348 fp = p + '/' + f
349 349 if kind == stat.S_IFREG and f[-2:] in ('.d', '.i'):
350 350 n = util.pconvert(fp[striplen:])
351 351 l.append((decodedir(n), n, st.st_size))
352 352 elif kind == stat.S_IFDIR and recurse:
353 353 visit.append(fp)
354 354 l.sort()
355 355 return l
356 356
357 357 def datafiles(self):
358 358 return self._walk('data', True) + self._walk('meta', True)
359 359
360 360 def topfiles(self):
361 361 # yield manifest before changelog
362 362 return reversed(self._walk('', False))
363 363
364 364 def walk(self):
365 365 '''yields (unencoded, encoded, size)'''
366 366 # yield data files first
367 367 for x in self.datafiles():
368 368 yield x
369 369 for x in self.topfiles():
370 370 yield x
371 371
372 372 def copylist(self):
373 373 return ['requires'] + _data.split()
374 374
375 375 def write(self, tr):
376 376 pass
377 377
378 378 def invalidatecaches(self):
379 379 pass
380 380
381 381 def markremoved(self, fn):
382 382 pass
383 383
384 384 def __contains__(self, path):
385 385 '''Checks if the store contains path'''
386 386 path = "/".join(("data", path))
387 387 # file?
388 388 if self.vfs.exists(path + ".i"):
389 389 return True
390 390 # dir?
391 391 if not path.endswith("/"):
392 392 path = path + "/"
393 393 return self.vfs.exists(path)
394 394
395 395 class encodedstore(basicstore):
396 396 def __init__(self, path, vfstype):
397 397 vfs = vfstype(path + '/store')
398 398 self.path = vfs.base
399 399 self.createmode = _calcmode(vfs)
400 400 vfs.createmode = self.createmode
401 401 self.rawvfs = vfs
402 402 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
403 403 self.opener = self.vfs
404 404
405 405 def datafiles(self):
406 406 for a, b, size in super(encodedstore, self).datafiles():
407 407 try:
408 408 a = decodefilename(a)
409 409 except KeyError:
410 410 a = None
411 411 yield a, b, size
412 412
413 413 def join(self, f):
414 414 return self.path + '/' + encodefilename(f)
415 415
416 416 def copylist(self):
417 417 return (['requires', '00changelog.i'] +
418 418 ['store/' + f for f in _data.split()])
419 419
420 420 class fncache(object):
421 421 # the filename used to be partially encoded
422 422 # hence the encodedir/decodedir dance
423 423 def __init__(self, vfs):
424 424 self.vfs = vfs
425 425 self.entries = None
426 426 self._dirty = False
427 427
428 428 def _load(self):
429 429 '''fill the entries from the fncache file'''
430 430 self._dirty = False
431 431 try:
432 432 fp = self.vfs('fncache', mode='rb')
433 433 except IOError:
434 434 # skip nonexistent file
435 435 self.entries = set()
436 436 return
437 437 self.entries = set(decodedir(fp.read()).splitlines())
438 438 if '' in self.entries:
439 439 fp.seek(0)
440 440 for n, line in enumerate(util.iterfile(fp)):
441 441 if not line.rstrip('\n'):
442 442 t = _('invalid entry in fncache, line %d') % (n + 1)
443 443 raise error.Abort(t)
444 444 fp.close()
445 445
446 446 def write(self, tr):
447 447 if self._dirty:
448 448 tr.addbackup('fncache')
449 449 fp = self.vfs('fncache', mode='wb', atomictemp=True)
450 450 if self.entries:
451 451 fp.write(encodedir('\n'.join(self.entries) + '\n'))
452 452 fp.close()
453 453 self._dirty = False
454 454
455 455 def add(self, fn):
456 456 if self.entries is None:
457 457 self._load()
458 458 if fn not in self.entries:
459 459 self._dirty = True
460 460 self.entries.add(fn)
461 461
462 462 def remove(self, fn):
463 463 if self.entries is None:
464 464 self._load()
465 465 try:
466 466 self.entries.remove(fn)
467 467 self._dirty = True
468 468 except KeyError:
469 469 pass
470 470
471 471 def __contains__(self, fn):
472 472 if self.entries is None:
473 473 self._load()
474 474 return fn in self.entries
475 475
476 476 def __iter__(self):
477 477 if self.entries is None:
478 478 self._load()
479 479 return iter(self.entries)
480 480
481 481 class _fncachevfs(vfsmod.abstractvfs, vfsmod.proxyvfs):
482 482 def __init__(self, vfs, fnc, encode):
483 483 vfsmod.proxyvfs.__init__(self, vfs)
484 484 self.fncache = fnc
485 485 self.encode = encode
486 486
487 487 def __call__(self, path, mode='r', *args, **kw):
488 488 if mode not in ('r', 'rb') and (path.startswith('data/') or
489 489 path.startswith('meta/')):
490 490 self.fncache.add(path)
491 491 return self.vfs(self.encode(path), mode, *args, **kw)
492 492
493 493 def join(self, path):
494 494 if path:
495 495 return self.vfs.join(self.encode(path))
496 496 else:
497 497 return self.vfs.join(path)
498 498
499 499 class fncachestore(basicstore):
500 500 def __init__(self, path, vfstype, dotencode):
501 501 if dotencode:
502 502 encode = _pathencode
503 503 else:
504 504 encode = _plainhybridencode
505 505 self.encode = encode
506 506 vfs = vfstype(path + '/store')
507 507 self.path = vfs.base
508 508 self.pathsep = self.path + '/'
509 509 self.createmode = _calcmode(vfs)
510 510 vfs.createmode = self.createmode
511 511 self.rawvfs = vfs
512 512 fnc = fncache(vfs)
513 513 self.fncache = fnc
514 514 self.vfs = _fncachevfs(vfs, fnc, encode)
515 515 self.opener = self.vfs
516 516
517 517 def join(self, f):
518 518 return self.pathsep + self.encode(f)
519 519
520 520 def getsize(self, path):
521 521 return self.rawvfs.stat(path).st_size
522 522
523 523 def datafiles(self):
524 524 for f in sorted(self.fncache):
525 525 ef = self.encode(f)
526 526 try:
527 527 yield f, ef, self.getsize(ef)
528 528 except OSError as err:
529 529 if err.errno != errno.ENOENT:
530 530 raise
531 531
532 532 def copylist(self):
533 533 d = ('data meta dh fncache phaseroots obsstore'
534 534 ' 00manifest.d 00manifest.i 00changelog.d 00changelog.i')
535 535 return (['requires', '00changelog.i'] +
536 536 ['store/' + f for f in d.split()])
537 537
538 538 def write(self, tr):
539 539 self.fncache.write(tr)
540 540
541 541 def invalidatecaches(self):
542 542 self.fncache.entries = None
543 543
544 544 def markremoved(self, fn):
545 545 self.fncache.remove(fn)
546 546
547 547 def _exists(self, f):
548 548 ef = self.encode(f)
549 549 try:
550 550 self.getsize(ef)
551 551 return True
552 552 except OSError as err:
553 553 if err.errno != errno.ENOENT:
554 554 raise
555 555 # nonexistent entry
556 556 return False
557 557
558 558 def __contains__(self, path):
559 559 '''Checks if the store contains path'''
560 560 path = "/".join(("data", path))
561 561 # check for files (exact match)
562 562 e = path + '.i'
563 563 if e in self.fncache and self._exists(e):
564 564 return True
565 565 # now check for directories (prefix match)
566 566 if not path.endswith('/'):
567 567 path += '/'
568 568 for e in self.fncache:
569 569 if e.startswith(path) and self._exists(e):
570 570 return True
571 571 return False
572 572
573 573 def store(requirements, path, vfstype):
574 574 if 'store' in requirements:
575 575 if 'fncache' in requirements:
576 576 return fncachestore(path, vfstype, 'dotencode' in requirements)
577 577 return encodedstore(path, vfstype)
578 578 return basicstore(path, vfstype)
@@ -1,81 +1,81 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 testmod('mercurial.dispatch')
53 53 testmod('mercurial.encoding', py3=False) # py3: multiple encoding issues
54 54 testmod('mercurial.formatter', py3=False) # py3: write bytes to stdout
55 55 testmod('mercurial.hg')
56 56 testmod('mercurial.hgweb.hgwebdir_mod', py3=False) # py3: repr(bytes) ?
57 57 testmod('mercurial.match')
58 58 testmod('mercurial.mdiff')
59 59 testmod('mercurial.minirst')
60 60 testmod('mercurial.patch', py3=False) # py3: bytes[n], etc. ?
61 61 testmod('mercurial.pathutil', py3=False) # py3: os.sep
62 62 testmod('mercurial.parser')
63 63 testmod('mercurial.pycompat')
64 64 testmod('mercurial.revsetlang')
65 65 testmod('mercurial.smartset')
66 testmod('mercurial.store', py3=False) # py3: bytes[n]
66 testmod('mercurial.store')
67 67 testmod('mercurial.subrepo')
68 68 testmod('mercurial.templatefilters')
69 69 testmod('mercurial.templater')
70 70 testmod('mercurial.ui')
71 71 testmod('mercurial.url')
72 72 testmod('mercurial.util', py3=False) # py3: multiple bytes/unicode issues
73 73 testmod('mercurial.util', testtarget='platform')
74 74 testmod('hgext.convert.convcmd', py3=False) # py3: use of str() ?
75 75 testmod('hgext.convert.cvsps')
76 76 testmod('hgext.convert.filemap')
77 77 testmod('hgext.convert.p4')
78 78 testmod('hgext.convert.subversion')
79 79 testmod('hgext.mq')
80 80 # Helper scripts in tests/ that have doctests:
81 81 testmod('drawdag')
General Comments 0
You need to be logged in to leave comments. Login now