##// END OF EJS Templates
dirstate-v2: fix infinite loop in pure packer...
Raphaël Gomès -
r49614:46d12f77 stable
parent child Browse files
Show More
@@ -1,414 +1,435 b''
1 1 # v2.py - Pure-Python implementation of the dirstate-v2 file format
2 2 #
3 3 # Copyright Mercurial Contributors
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 struct
11 11
12 12 from ..thirdparty import attr
13 13 from .. import error, policy
14 14
15 15 parsers = policy.importmod('parsers')
16 16
17 17
18 18 # Must match the constant of the same name in
19 19 # `rust/hg-core/src/dirstate_tree/on_disk.rs`
20 20 TREE_METADATA_SIZE = 44
21 21 NODE_SIZE = 44
22 22
23 23
24 24 # Must match the `TreeMetadata` Rust struct in
25 25 # `rust/hg-core/src/dirstate_tree/on_disk.rs`. See doc-comments there.
26 26 #
27 27 # * 4 bytes: start offset of root nodes
28 28 # * 4 bytes: number of root nodes
29 29 # * 4 bytes: total number of nodes in the tree that have an entry
30 30 # * 4 bytes: total number of nodes in the tree that have a copy source
31 31 # * 4 bytes: number of bytes in the data file that are not used anymore
32 32 # * 4 bytes: unused
33 33 # * 20 bytes: SHA-1 hash of ignore patterns
34 34 TREE_METADATA = struct.Struct('>LLLLL4s20s')
35 35
36 36
37 37 # Must match the `Node` Rust struct in
38 38 # `rust/hg-core/src/dirstate_tree/on_disk.rs`. See doc-comments there.
39 39 #
40 40 # * 4 bytes: start offset of full path
41 41 # * 2 bytes: length of the full path
42 42 # * 2 bytes: length within the full path before its "base name"
43 43 # * 4 bytes: start offset of the copy source if any, or zero for no copy source
44 44 # * 2 bytes: length of the copy source if any, or unused
45 45 # * 4 bytes: start offset of child nodes
46 46 # * 4 bytes: number of child nodes
47 47 # * 4 bytes: number of descendant nodes that have an entry
48 48 # * 4 bytes: number of descendant nodes that have a "tracked" state
49 49 # * 1 byte: flags
50 50 # * 4 bytes: expected size
51 51 # * 4 bytes: mtime seconds
52 52 # * 4 bytes: mtime nanoseconds
53 53 NODE = struct.Struct('>LHHLHLLLLHlll')
54 54
55 55
56 56 assert TREE_METADATA_SIZE == TREE_METADATA.size
57 57 assert NODE_SIZE == NODE.size
58 58
59 59 # match constant in mercurial/pure/parsers.py
60 60 DIRSTATE_V2_DIRECTORY = 1 << 5
61 61
62 62
63 63 def parse_dirstate(map, copy_map, data, tree_metadata):
64 64 """parse a full v2-dirstate from a binary data into dictionnaries:
65 65
66 66 - map: a {path: entry} mapping that will be filled
67 67 - copy_map: a {path: copy-source} mapping that will be filled
68 68 - data: a binary blob contains v2 nodes data
69 69 - tree_metadata:: a binary blob of the top level node (from the docket)
70 70 """
71 71 (
72 72 root_nodes_start,
73 73 root_nodes_len,
74 74 _nodes_with_entry_count,
75 75 _nodes_with_copy_source_count,
76 76 _unreachable_bytes,
77 77 _unused,
78 78 _ignore_patterns_hash,
79 79 ) = TREE_METADATA.unpack(tree_metadata)
80 80 parse_nodes(map, copy_map, data, root_nodes_start, root_nodes_len)
81 81
82 82
83 83 def parse_nodes(map, copy_map, data, start, len):
84 84 """parse <len> nodes from <data> starting at offset <start>
85 85
86 86 This is used by parse_dirstate to recursively fill `map` and `copy_map`.
87 87
88 88 All directory specific information is ignored and do not need any
89 89 processing (DIRECTORY, ALL_UNKNOWN_RECORDED, ALL_IGNORED_RECORDED)
90 90 """
91 91 for i in range(len):
92 92 node_start = start + NODE_SIZE * i
93 93 node_bytes = slice_with_len(data, node_start, NODE_SIZE)
94 94 (
95 95 path_start,
96 96 path_len,
97 97 _basename_start,
98 98 copy_source_start,
99 99 copy_source_len,
100 100 children_start,
101 101 children_count,
102 102 _descendants_with_entry_count,
103 103 _tracked_descendants_count,
104 104 flags,
105 105 size,
106 106 mtime_s,
107 107 mtime_ns,
108 108 ) = NODE.unpack(node_bytes)
109 109
110 110 # Parse child nodes of this node recursively
111 111 parse_nodes(map, copy_map, data, children_start, children_count)
112 112
113 113 item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s, mtime_ns)
114 114 if not item.any_tracked:
115 115 continue
116 116 path = slice_with_len(data, path_start, path_len)
117 117 map[path] = item
118 118 if copy_source_start:
119 119 copy_map[path] = slice_with_len(
120 120 data, copy_source_start, copy_source_len
121 121 )
122 122
123 123
124 124 def slice_with_len(data, start, len):
125 125 return data[start : start + len]
126 126
127 127
128 128 @attr.s
129 129 class Node(object):
130 130 path = attr.ib()
131 131 entry = attr.ib()
132 132 parent = attr.ib(default=None)
133 133 children_count = attr.ib(default=0)
134 134 children_offset = attr.ib(default=0)
135 135 descendants_with_entry = attr.ib(default=0)
136 136 tracked_descendants = attr.ib(default=0)
137 137
138 138 def pack(self, copy_map, paths_offset):
139 139 path = self.path
140 140 copy = copy_map.get(path)
141 141 entry = self.entry
142 142
143 143 path_start = paths_offset
144 144 path_len = len(path)
145 145 basename_start = path.rfind(b'/') + 1 # 0 if rfind returns -1
146 146 if copy is not None:
147 147 copy_source_start = paths_offset + len(path)
148 148 copy_source_len = len(copy)
149 149 else:
150 150 copy_source_start = 0
151 151 copy_source_len = 0
152 152 if entry is not None:
153 153 flags, size, mtime_s, mtime_ns = entry.v2_data()
154 154 else:
155 155 # There are no mtime-cached directories in the Python implementation
156 156 flags = DIRSTATE_V2_DIRECTORY
157 157 size = 0
158 158 mtime_s = 0
159 159 mtime_ns = 0
160 160 return NODE.pack(
161 161 path_start,
162 162 path_len,
163 163 basename_start,
164 164 copy_source_start,
165 165 copy_source_len,
166 166 self.children_offset,
167 167 self.children_count,
168 168 self.descendants_with_entry,
169 169 self.tracked_descendants,
170 170 flags,
171 171 size,
172 172 mtime_s,
173 173 mtime_ns,
174 174 )
175 175
176 176
177 177 def pack_dirstate(map, copy_map, now):
178 178 """
179 179 Pack `map` and `copy_map` into the dirstate v2 binary format and return
180 180 the bytearray.
181 181 `now` is a timestamp of the current filesystem time used to detect race
182 182 conditions in writing the dirstate to disk, see inline comment.
183 183
184 184 The on-disk format expects a tree-like structure where the leaves are
185 185 written first (and sorted per-directory), going up levels until the root
186 186 node and writing that one to the docket. See more details on the on-disk
187 187 format in `mercurial/helptext/internals/dirstate-v2`.
188 188
189 189 Since both `map` and `copy_map` are flat dicts we need to figure out the
190 190 hierarchy. This algorithm does so without having to build the entire tree
191 191 in-memory: it only keeps the minimum number of nodes around to satisfy the
192 192 format.
193 193
194 194 # Algorithm explanation
195 195
196 196 This explanation does not talk about the different counters for tracked
197 197 descendents and storing the copies, but that work is pretty simple once this
198 198 algorithm is in place.
199 199
200 200 ## Building a subtree
201 201
202 202 First, sort `map`: this makes it so the leaves of the tree are contiguous
203 203 per directory (i.e. a/b/c and a/b/d will be next to each other in the list),
204 204 and enables us to use the ordering of folders to have a "cursor" of the
205 205 current folder we're in without ever going twice in the same branch of the
206 206 tree. The cursor is a node that remembers its parent and any information
207 207 relevant to the format (see the `Node` class), building the relevant part
208 208 of the tree lazily.
209 209 Then, for each file in `map`, move the cursor into the tree to the
210 210 corresponding folder of the file: for example, if the very first file
211 211 is "a/b/c", we start from `Node[""]`, create `Node["a"]` which points to
212 212 its parent `Node[""]`, then create `Node["a/b"]`, which points to its parent
213 213 `Node["a"]`. These nodes are kept around in a stack.
214 214 If the next file in `map` is in the same subtree ("a/b/d" or "a/b/e/f"), we
215 215 add it to the stack and keep looping with the same logic of creating the
216 216 tree nodes as needed. If however the next file in `map` is *not* in the same
217 217 subtree ("a/other", if we're still in the "a/b" folder), then we know that
218 218 the subtree we're in is complete.
219 219
220 220 ## Writing the subtree
221 221
222 222 We have the entire subtree in the stack, so we start writing it to disk
223 223 folder by folder. The way we write a folder is to pop the stack into a list
224 224 until the folder changes, revert this list of direct children (to satisfy
225 225 the format requirement that children be sorted). This process repeats until
226 226 we hit the "other" subtree.
227 227
228 228 An example:
229 229 a
230 230 dir1/b
231 231 dir1/c
232 232 dir2/dir3/d
233 233 dir2/dir3/e
234 234 dir2/f
235 235
236 236 Would have us:
237 237 - add to the stack until "dir2/dir3/e"
238 238 - realize that "dir2/f" is in a different subtree
239 239 - pop "dir2/dir3/e", "dir2/dir3/d", reverse them so they're sorted and
240 240 pack them since the next entry is "dir2/dir3"
241 241 - go back up to "dir2"
242 242 - add "dir2/f" to the stack
243 243 - realize we're done with the map
244 244 - pop "dir2/f", "dir2/dir3" from the stack, reverse and pack them
245 245 - go up to the root node, do the same to write "a", "dir1" and "dir2" in
246 246 that order
247 247
248 248 ## Special case for the root node
249 249
250 250 The root node is not serialized in the format, but its information is
251 251 written to the docket. Again, see more details on the on-disk format in
252 252 `mercurial/helptext/internals/dirstate-v2`.
253 253 """
254 254 data = bytearray()
255 255 root_nodes_start = 0
256 256 root_nodes_len = 0
257 257 nodes_with_entry_count = 0
258 258 nodes_with_copy_source_count = 0
259 259 # Will always be 0 since this implementation always re-writes everything
260 260 # to disk
261 261 unreachable_bytes = 0
262 262 unused = b'\x00' * 4
263 263 # This is an optimization that's only useful for the Rust implementation
264 264 ignore_patterns_hash = b'\x00' * 20
265 265
266 266 if len(map) == 0:
267 267 tree_metadata = TREE_METADATA.pack(
268 268 root_nodes_start,
269 269 root_nodes_len,
270 270 nodes_with_entry_count,
271 271 nodes_with_copy_source_count,
272 272 unreachable_bytes,
273 273 unused,
274 274 ignore_patterns_hash,
275 275 )
276 276 return data, tree_metadata
277 277
278 278 sorted_map = sorted(map.items(), key=lambda x: x[0])
279 279
280 280 # Use a stack to not have to only remember the nodes we currently need
281 281 # instead of building the entire tree in memory
282 282 stack = []
283 283 current_node = Node(b"", None)
284 284 stack.append(current_node)
285 285
286 286 for index, (path, entry) in enumerate(sorted_map, 1):
287 287 if entry.need_delay(now):
288 288 # The file was last modified "simultaneously" with the current
289 289 # write to dirstate (i.e. within the same second for file-
290 290 # systems with a granularity of 1 sec). This commonly happens
291 291 # for at least a couple of files on 'update'.
292 292 # The user could change the file without changing its size
293 293 # within the same second. Invalidate the file's mtime in
294 294 # dirstate, forcing future 'status' calls to compare the
295 295 # contents of the file if the size is the same. This prevents
296 296 # mistakenly treating such files as clean.
297 297 entry.set_possibly_dirty()
298 298 nodes_with_entry_count += 1
299 299 if path in copy_map:
300 300 nodes_with_copy_source_count += 1
301 301 current_folder = get_folder(path)
302 302 current_node = move_to_correct_node_in_tree(
303 303 current_folder, current_node, stack
304 304 )
305 305
306 306 current_node.children_count += 1
307 307 # Entries from `map` are never `None`
308 308 if entry.tracked:
309 309 current_node.tracked_descendants += 1
310 310 current_node.descendants_with_entry += 1
311 311 stack.append(Node(path, entry, current_node))
312 312
313 313 should_pack = True
314 314 next_path = None
315 315 if index < len(sorted_map):
316 316 # Determine if the next entry is in the same sub-tree, if so don't
317 317 # pack yet
318 318 next_path = sorted_map[index][0]
319 should_pack = not get_folder(next_path).startswith(current_folder)
319 should_pack = not is_ancestor(next_path, current_folder)
320 320 if should_pack:
321 321 pack_directory_children(current_node, copy_map, data, stack)
322 322 while stack and current_node.path != b"":
323 323 # Go up the tree and write until we reach the folder of the next
324 324 # entry (if any, otherwise the root)
325 325 parent = current_node.parent
326 in_parent_folder_of_next_entry = next_path is not None and (
327 get_folder(next_path).startswith(get_folder(stack[-1].path))
326 in_ancestor_of_next_path = next_path is not None and (
327 is_ancestor(next_path, get_folder(stack[-1].path))
328 328 )
329 if parent is None or in_parent_folder_of_next_entry:
329 if parent is None or in_ancestor_of_next_path:
330 330 break
331 331 pack_directory_children(parent, copy_map, data, stack)
332 332 current_node = parent
333 333
334 334 # Special case for the root node since we don't write it to disk, only its
335 335 # children to the docket
336 336 current_node = stack.pop()
337 337 assert current_node.path == b"", current_node.path
338 338 assert len(stack) == 0, len(stack)
339 339
340 340 tree_metadata = TREE_METADATA.pack(
341 341 current_node.children_offset,
342 342 current_node.children_count,
343 343 nodes_with_entry_count,
344 344 nodes_with_copy_source_count,
345 345 unreachable_bytes,
346 346 unused,
347 347 ignore_patterns_hash,
348 348 )
349 349
350 350 return data, tree_metadata
351 351
352 352
353 353 def get_folder(path):
354 354 """
355 355 Return the folder of the path that's given, an empty string for root paths.
356 356 """
357 357 return path.rsplit(b'/', 1)[0] if b'/' in path else b''
358 358
359 359
360 def is_ancestor(path, maybe_ancestor):
361 """Returns whether `maybe_ancestor` is an ancestor of `path`.
362
363 >>> is_ancestor(b"a", b"")
364 True
365 >>> is_ancestor(b"a/b/c", b"a/b/c")
366 False
367 >>> is_ancestor(b"hgext3rd/__init__.py", b"hgext")
368 False
369 >>> is_ancestor(b"hgext3rd/__init__.py", b"hgext3rd")
370 True
371 """
372 if maybe_ancestor == b"":
373 return True
374 if path <= maybe_ancestor:
375 return False
376 path_components = path.split(b"/")
377 ancestor_components = maybe_ancestor.split(b"/")
378 return all(c == o for c, o in zip(path_components, ancestor_components))
379
380
360 381 def move_to_correct_node_in_tree(target_folder, current_node, stack):
361 382 """
362 383 Move inside the dirstate node tree to the node corresponding to
363 384 `target_folder`, creating the missing nodes along the way if needed.
364 385 """
365 386 while target_folder != current_node.path:
366 if target_folder.startswith(current_node.path):
387 if is_ancestor(target_folder, current_node.path):
367 388 # We need to go down a folder
368 389 prefix = target_folder[len(current_node.path) :].lstrip(b'/')
369 390 subfolder_name = prefix.split(b'/', 1)[0]
370 391 if current_node.path:
371 392 subfolder_path = current_node.path + b'/' + subfolder_name
372 393 else:
373 394 subfolder_path = subfolder_name
374 395 next_node = stack[-1]
375 396 if next_node.path == target_folder:
376 397 # This folder is now a file and only contains removed entries
377 398 # merge with the last node
378 399 current_node = next_node
379 400 else:
380 401 current_node.children_count += 1
381 402 current_node = Node(subfolder_path, None, current_node)
382 403 stack.append(current_node)
383 404 else:
384 405 # We need to go up a folder
385 406 current_node = current_node.parent
386 407 return current_node
387 408
388 409
389 410 def pack_directory_children(node, copy_map, data, stack):
390 411 """
391 412 Write the binary representation of the direct sorted children of `node` to
392 413 `data`
393 414 """
394 415 direct_children = []
395 416
396 417 while stack[-1].path != b"" and get_folder(stack[-1].path) == node.path:
397 418 direct_children.append(stack.pop())
398 419 if not direct_children:
399 420 raise error.ProgrammingError(b"no direct children for %r" % node.path)
400 421
401 422 # Reverse the stack to get the correct sorted order
402 423 direct_children.reverse()
403 424 packed_children = bytearray()
404 425 # Write the paths to `data`. Pack child nodes but don't write them yet
405 426 for child in direct_children:
406 427 packed = child.pack(copy_map=copy_map, paths_offset=len(data))
407 428 packed_children.extend(packed)
408 429 data.extend(child.path)
409 430 data.extend(copy_map.get(child.path, b""))
410 431 node.tracked_descendants += child.tracked_descendants
411 432 node.descendants_with_entry += child.descendants_with_entry
412 433 # Write the fixed-size child nodes all together
413 434 node.children_offset = len(data)
414 435 data.extend(packed_children)
@@ -1,105 +1,123 b''
1 1 #testcases dirstate-v1 dirstate-v2
2 2
3 3 #if dirstate-v2
4 4 $ cat >> $HGRCPATH << EOF
5 5 > [format]
6 6 > use-dirstate-v2=1
7 7 > [storage]
8 8 > dirstate-v2.slow-path=allow
9 9 > EOF
10 10 #endif
11 11
12 12 ------ Test dirstate._dirs refcounting
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ mkdir -p a/b/c/d
17 17 $ touch a/b/c/d/x
18 18 $ touch a/b/c/d/y
19 19 $ touch a/b/c/d/z
20 20 $ hg ci -Am m
21 21 adding a/b/c/d/x
22 22 adding a/b/c/d/y
23 23 adding a/b/c/d/z
24 24 $ hg mv a z
25 25 moving a/b/c/d/x to z/b/c/d/x
26 26 moving a/b/c/d/y to z/b/c/d/y
27 27 moving a/b/c/d/z to z/b/c/d/z
28 28
29 29 Test name collisions
30 30
31 31 $ rm z/b/c/d/x
32 32 $ mkdir z/b/c/d/x
33 33 $ touch z/b/c/d/x/y
34 34 $ hg add z/b/c/d/x/y
35 35 abort: file 'z/b/c/d/x' in dirstate clashes with 'z/b/c/d/x/y'
36 36 [255]
37 37 $ rm -rf z/b/c/d
38 38 $ touch z/b/c/d
39 39 $ hg add z/b/c/d
40 40 abort: directory 'z/b/c/d' already in dirstate
41 41 [255]
42 42
43 43 $ cd ..
44 44
45 45 Issue1790: dirstate entry locked into unset if file mtime is set into
46 46 the future
47 47
48 48 Prepare test repo:
49 49
50 50 $ hg init u
51 51 $ cd u
52 52 $ echo a > a
53 53 $ hg add
54 54 adding a
55 55 $ hg ci -m1
56 56
57 57 Set mtime of a into the future:
58 58
59 59 $ touch -t 203101011200 a
60 60
61 61 Status must not set a's entry to unset (issue1790):
62 62
63 63 $ hg status
64 64 $ hg debugstate
65 65 n 644 2 2031-01-01 12:00:00 a
66 66
67 67 Test modulo storage/comparison of absurd dates:
68 68
69 69 #if no-aix
70 70 $ touch -t 195001011200 a
71 71 $ hg st
72 72 $ hg debugstate
73 73 n 644 2 2018-01-19 15:14:08 a
74 74 #endif
75 75
76 76 Verify that exceptions during a dirstate change leave the dirstate
77 77 coherent (issue4353)
78 78
79 79 $ cat > ../dirstateexception.py <<EOF
80 80 > from __future__ import absolute_import
81 81 > from mercurial import (
82 82 > error,
83 83 > extensions,
84 84 > mergestate as mergestatemod,
85 85 > )
86 86 >
87 87 > def wraprecordupdates(*args):
88 88 > raise error.Abort(b"simulated error while recording dirstateupdates")
89 89 >
90 90 > def reposetup(ui, repo):
91 91 > extensions.wrapfunction(mergestatemod, 'recordupdates',
92 92 > wraprecordupdates)
93 93 > EOF
94 94
95 95 $ hg rm a
96 96 $ hg commit -m 'rm a'
97 97 $ echo "[extensions]" >> .hg/hgrc
98 98 $ echo "dirstateex=../dirstateexception.py" >> .hg/hgrc
99 99 $ hg up 0
100 100 abort: simulated error while recording dirstateupdates
101 101 [255]
102 102 $ hg log -r . -T '{rev}\n'
103 103 1
104 104 $ hg status
105 105 ? a
106
107 #if dirstate-v2
108 Check that folders that are prefixes of others do not throw the packer into an
109 infinite loop.
110
111 $ cd ..
112 $ hg init infinite-loop
113 $ cd infinite-loop
114 $ mkdir hgext3rd hgext
115 $ touch hgext3rd/__init__.py hgext/zeroconf.py
116 $ hg commit -Aqm0
117
118 $ hg st -c
119 C hgext/zeroconf.py
120 C hgext3rd/__init__.py
121
122 $ cd ..
123 #endif
@@ -1,177 +1,178 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 from __future__ import print_function
5 5
6 6 import doctest
7 7 import os
8 8 import re
9 9 import subprocess
10 10 import sys
11 11
12 12 ispy3 = sys.version_info[0] >= 3
13 13
14 14 if 'TERM' in os.environ:
15 15 del os.environ['TERM']
16 16
17 17
18 18 class py3docchecker(doctest.OutputChecker):
19 19 def check_output(self, want, got, optionflags):
20 20 want2 = re.sub(r'''\bu(['"])(.*?)\1''', r'\1\2\1', want) # py2: u''
21 21 got2 = re.sub(r'''\bb(['"])(.*?)\1''', r'\1\2\1', got) # py3: b''
22 22 # py3: <exc.name>: b'<msg>' -> <name>: <msg>
23 23 # <exc.name>: <others> -> <name>: <others>
24 24 got2 = re.sub(
25 25 r'''^mercurial\.\w+\.(\w+): (['"])(.*?)\2''',
26 26 r'\1: \3',
27 27 got2,
28 28 re.MULTILINE,
29 29 )
30 30 got2 = re.sub(r'^mercurial\.\w+\.(\w+): ', r'\1: ', got2, re.MULTILINE)
31 31 return any(
32 32 doctest.OutputChecker.check_output(self, w, g, optionflags)
33 33 for w, g in [(want, got), (want2, got2)]
34 34 )
35 35
36 36
37 37 def testmod(name, optionflags=0, testtarget=None):
38 38 __import__(name)
39 39 mod = sys.modules[name]
40 40 if testtarget is not None:
41 41 mod = getattr(mod, testtarget)
42 42
43 43 # minimal copy of doctest.testmod()
44 44 finder = doctest.DocTestFinder()
45 45 checker = None
46 46 if ispy3:
47 47 checker = py3docchecker()
48 48 runner = doctest.DocTestRunner(checker=checker, optionflags=optionflags)
49 49 for test in finder.find(mod, name):
50 50 runner.run(test)
51 51 runner.summarize()
52 52
53 53
54 54 DONT_RUN = []
55 55
56 56 # Exceptions to the defaults for a given detected module. The value for each
57 57 # module name is a list of dicts that specify the kwargs to pass to testmod.
58 58 # testmod is called once per item in the list, so an empty list will cause the
59 59 # module to not be tested.
60 60 testmod_arg_overrides = {
61 61 'i18n.check-translation': DONT_RUN, # may require extra installation
62 62 'mercurial.dagparser': [{'optionflags': doctest.NORMALIZE_WHITESPACE}],
63 63 'mercurial.keepalive': DONT_RUN, # >>> is an example, not a doctest
64 64 'mercurial.posix': DONT_RUN, # run by mercurial.platform
65 65 'mercurial.statprof': DONT_RUN, # >>> is an example, not a doctest
66 66 'mercurial.util': [{}, {'testtarget': 'platform'}], # run twice!
67 67 'mercurial.windows': DONT_RUN, # run by mercurial.platform
68 68 'tests.test-url': [{'optionflags': doctest.NORMALIZE_WHITESPACE}],
69 69 }
70 70
71 71 fileset = 'set:(**.py)'
72 72
73 73 cwd = os.path.dirname(os.environ["TESTDIR"])
74 74
75 75 if not os.path.isdir(os.path.join(cwd, ".hg")):
76 76 sys.exit(0)
77 77
78 78 files = subprocess.check_output(
79 79 "hg files --print0 \"%s\"" % fileset,
80 80 shell=True,
81 81 cwd=cwd,
82 82 ).split(b'\0')
83 83
84 84 if sys.version_info[0] >= 3:
85 85 cwd = os.fsencode(cwd)
86 86
87 87 mods_tested = set()
88 88 for f in files:
89 89 if not f:
90 90 continue
91 91
92 92 with open(os.path.join(cwd, f), "rb") as fh:
93 93 if not re.search(br'\n\s*>>>', fh.read()):
94 94 continue
95 95
96 96 if ispy3:
97 97 f = f.decode()
98 98
99 99 modname = f.replace('.py', '').replace('\\', '.').replace('/', '.')
100 100
101 101 # Third-party modules aren't our responsibility to test, and the modules in
102 102 # contrib generally do not have doctests in a good state, plus they're hard
103 103 # to import if this test is running with py2, so we just skip both for now.
104 104 if modname.startswith('mercurial.thirdparty.') or modname.startswith(
105 105 'contrib.'
106 106 ):
107 107 continue
108 108
109 109 for kwargs in testmod_arg_overrides.get(modname, [{}]):
110 110 mods_tested.add((modname, '%r' % (kwargs,)))
111 111 if modname.startswith('tests.'):
112 112 # On py2, we can't import from tests.foo, but it works on both py2
113 113 # and py3 with the way that PYTHONPATH is setup to import without
114 114 # the 'tests.' prefix, so we do that.
115 115 modname = modname[len('tests.') :]
116 116
117 117 testmod(modname, **kwargs)
118 118
119 119 # Meta-test: let's make sure that we actually ran what we expected to, above.
120 120 # Each item in the set is a 2-tuple of module name and stringified kwargs passed
121 121 # to testmod.
122 122 expected_mods_tested = set(
123 123 [
124 124 ('hgext.convert.convcmd', '{}'),
125 125 ('hgext.convert.cvsps', '{}'),
126 126 ('hgext.convert.filemap', '{}'),
127 127 ('hgext.convert.p4', '{}'),
128 128 ('hgext.convert.subversion', '{}'),
129 129 ('hgext.fix', '{}'),
130 130 ('hgext.mq', '{}'),
131 131 ('mercurial.changelog', '{}'),
132 132 ('mercurial.cmdutil', '{}'),
133 133 ('mercurial.color', '{}'),
134 134 ('mercurial.dagparser', "{'optionflags': 4}"),
135 ('mercurial.dirstateutils.v2', '{}'),
135 136 ('mercurial.encoding', '{}'),
136 137 ('mercurial.fancyopts', '{}'),
137 138 ('mercurial.formatter', '{}'),
138 139 ('mercurial.hg', '{}'),
139 140 ('mercurial.hgweb.hgwebdir_mod', '{}'),
140 141 ('mercurial.match', '{}'),
141 142 ('mercurial.mdiff', '{}'),
142 143 ('mercurial.minirst', '{}'),
143 144 ('mercurial.parser', '{}'),
144 145 ('mercurial.patch', '{}'),
145 146 ('mercurial.pathutil', '{}'),
146 147 ('mercurial.pycompat', '{}'),
147 148 ('mercurial.revlogutils.deltas', '{}'),
148 149 ('mercurial.revset', '{}'),
149 150 ('mercurial.revsetlang', '{}'),
150 151 ('mercurial.simplemerge', '{}'),
151 152 ('mercurial.smartset', '{}'),
152 153 ('mercurial.store', '{}'),
153 154 ('mercurial.subrepo', '{}'),
154 155 ('mercurial.templater', '{}'),
155 156 ('mercurial.ui', '{}'),
156 157 ('mercurial.util', "{'testtarget': 'platform'}"),
157 158 ('mercurial.util', '{}'),
158 159 ('mercurial.utils.dateutil', '{}'),
159 160 ('mercurial.utils.stringutil', '{}'),
160 161 ('mercurial.utils.urlutil', '{}'),
161 162 ('tests.drawdag', '{}'),
162 163 ('tests.test-run-tests', '{}'),
163 164 ('tests.test-url', "{'optionflags': 4}"),
164 165 ]
165 166 )
166 167
167 168 unexpectedly_run = mods_tested.difference(expected_mods_tested)
168 169 not_run = expected_mods_tested.difference(mods_tested)
169 170
170 171 if unexpectedly_run:
171 172 print('Unexpectedly ran (probably need to add to list):')
172 173 for r in sorted(unexpectedly_run):
173 174 print(' %r' % (r,))
174 175 if not_run:
175 176 print('Expected to run, but was not run (doctest removed?):')
176 177 for r in sorted(not_run):
177 178 print(' %r' % (r,))
General Comments 0
You need to be logged in to leave comments. Login now