Show More
@@ -0,0 +1,290 b'' | |||
|
1 | # test revlog interaction about raw data (flagprocessor) | |
|
2 | ||
|
3 | from __future__ import absolute_import, print_function | |
|
4 | ||
|
5 | import sys | |
|
6 | ||
|
7 | from mercurial import ( | |
|
8 | encoding, | |
|
9 | node, | |
|
10 | revlog, | |
|
11 | transaction, | |
|
12 | vfs, | |
|
13 | ) | |
|
14 | ||
|
15 | # TESTTMP is optional. This makes it convenient to run without run-tests.py | |
|
16 | tvfs = vfs.vfs(encoding.environ.get('TESTTMP', b'/tmp')) | |
|
17 | ||
|
18 | # Enable generaldelta otherwise revlog won't use delta as expected by the test | |
|
19 | tvfs.options = {'generaldelta': True, 'revlogv1': True, 'revlogv1': True} | |
|
20 | ||
|
21 | # The test wants to control whether to use delta explicitly, based on | |
|
22 | # "storedeltachains". | |
|
23 | revlog.revlog._isgooddelta = lambda self, d, textlen: self.storedeltachains | |
|
24 | ||
|
25 | def abort(msg): | |
|
26 | print('abort: %s' % msg) | |
|
27 | # Return 0 so run-tests.py could compare the output. | |
|
28 | sys.exit() | |
|
29 | ||
|
30 | # Register a revlog processor for flag EXTSTORED. | |
|
31 | # | |
|
32 | # It simply prepends a fixed header, and replaces '1' to 'i'. So it has | |
|
33 | # insertion and replacement, and may be interesting to test revlog's line-based | |
|
34 | # deltas. | |
|
35 | _extheader = b'E\n' | |
|
36 | ||
|
37 | def readprocessor(self, rawtext): | |
|
38 | # True: the returned text could be used to verify hash | |
|
39 | text = rawtext[len(_extheader):].replace(b'i', b'1') | |
|
40 | return text, True | |
|
41 | ||
|
42 | def writeprocessor(self, text): | |
|
43 | # False: the returned rawtext shouldn't be used to verify hash | |
|
44 | rawtext = _extheader + text.replace(b'1', b'i') | |
|
45 | return rawtext, False | |
|
46 | ||
|
47 | def rawprocessor(self, rawtext): | |
|
48 | # False: do not verify hash. Only the content returned by "readprocessor" | |
|
49 | # can be used to verify hash. | |
|
50 | return False | |
|
51 | ||
|
52 | revlog.addflagprocessor(revlog.REVIDX_EXTSTORED, | |
|
53 | (readprocessor, writeprocessor, rawprocessor)) | |
|
54 | ||
|
55 | # Utilities about reading and appending revlog | |
|
56 | ||
|
57 | def newtransaction(): | |
|
58 | # A transaction is required to write revlogs | |
|
59 | report = lambda msg: None | |
|
60 | return transaction.transaction(report, tvfs, {'plain': tvfs}, b'journal') | |
|
61 | ||
|
62 | def newrevlog(name=b'_testrevlog.i', recreate=False): | |
|
63 | if recreate: | |
|
64 | tvfs.tryunlink(name) | |
|
65 | rlog = revlog.revlog(tvfs, name) | |
|
66 | return rlog | |
|
67 | ||
|
68 | def appendrev(rlog, text, tr, isext=False, isdelta=True): | |
|
69 | '''Append a revision. If isext is True, set the EXTSTORED flag so flag | |
|
70 | processor will be used (and rawtext is different from text). If isdelta is | |
|
71 | True, force the revision to be a delta, otherwise it's full text. | |
|
72 | ''' | |
|
73 | nextrev = len(rlog) | |
|
74 | p1 = rlog.node(nextrev - 1) | |
|
75 | p2 = node.nullid | |
|
76 | if isext: | |
|
77 | flags = revlog.REVIDX_EXTSTORED | |
|
78 | else: | |
|
79 | flags = revlog.REVIDX_DEFAULT_FLAGS | |
|
80 | # Change storedeltachains temporarily, to override revlog's delta decision | |
|
81 | rlog.storedeltachains = isdelta | |
|
82 | try: | |
|
83 | rlog.addrevision(text, tr, nextrev, p1, p2, flags=flags) | |
|
84 | return nextrev | |
|
85 | except Exception as ex: | |
|
86 | abort('rev %d: failed to append: %s' % (nextrev, ex)) | |
|
87 | finally: | |
|
88 | # Restore storedeltachains. It is always True, see revlog.__init__ | |
|
89 | rlog.storedeltachains = True | |
|
90 | ||
|
91 | def addgroupcopy(rlog, tr, destname=b'_destrevlog.i', optimaldelta=True): | |
|
92 | '''Copy revlog to destname using revlog.addgroup. Return the copied revlog. | |
|
93 | ||
|
94 | This emulates push or pull. They use changegroup. Changegroup requires | |
|
95 | repo to work. We don't have a repo, so a dummy changegroup is used. | |
|
96 | ||
|
97 | If optimaldelta is True, use optimized delta parent, so the destination | |
|
98 | revlog could probably reuse it. Otherwise it builds sub-optimal delta, and | |
|
99 | the destination revlog needs more work to use it. | |
|
100 | ||
|
101 | This exercises some revlog.addgroup (and revlog._addrevision(text=None)) | |
|
102 | code path, which is not covered by "appendrev" alone. | |
|
103 | ''' | |
|
104 | class dummychangegroup(object): | |
|
105 | @staticmethod | |
|
106 | def deltachunk(pnode): | |
|
107 | pnode = pnode or node.nullid | |
|
108 | parentrev = rlog.rev(pnode) | |
|
109 | r = parentrev + 1 | |
|
110 | if r >= len(rlog): | |
|
111 | return {} | |
|
112 | if optimaldelta: | |
|
113 | deltaparent = parentrev | |
|
114 | else: | |
|
115 | # suboptimal deltaparent | |
|
116 | deltaparent = min(0, parentrev) | |
|
117 | return {'node': rlog.node(r), 'p1': pnode, 'p2': node.nullid, | |
|
118 | 'cs': rlog.node(rlog.linkrev(r)), 'flags': rlog.flags(r), | |
|
119 | 'deltabase': rlog.node(deltaparent), | |
|
120 | 'delta': rlog.revdiff(deltaparent, r)} | |
|
121 | ||
|
122 | def linkmap(lnode): | |
|
123 | return rlog.rev(lnode) | |
|
124 | ||
|
125 | dlog = newrevlog(destname, recreate=True) | |
|
126 | dlog.addgroup(dummychangegroup(), linkmap, tr) | |
|
127 | return dlog | |
|
128 | ||
|
129 | def lowlevelcopy(rlog, tr, destname=b'_destrevlog.i'): | |
|
130 | '''Like addgroupcopy, but use the low level revlog._addrevision directly. | |
|
131 | ||
|
132 | It exercises some code paths that are hard to reach easily otherwise. | |
|
133 | ''' | |
|
134 | dlog = newrevlog(destname, recreate=True) | |
|
135 | for r in rlog: | |
|
136 | p1 = rlog.node(r - 1) | |
|
137 | p2 = node.nullid | |
|
138 | if r == 0: | |
|
139 | text = rlog.revision(r, raw=True) | |
|
140 | cachedelta = None | |
|
141 | else: | |
|
142 | # deltaparent is more interesting if it has the EXTSTORED flag. | |
|
143 | deltaparent = max([0] + [p for p in range(r - 2) if rlog.flags(p)]) | |
|
144 | text = None | |
|
145 | cachedelta = (deltaparent, rlog.revdiff(deltaparent, r)) | |
|
146 | flags = rlog.flags(r) | |
|
147 | ifh = dlog.opener(dlog.indexfile, 'a+') | |
|
148 | dfh = None | |
|
149 | if not dlog._inline: | |
|
150 | dfh = dlog.opener(dlog.datafile, 'a+') | |
|
151 | dlog._addrevision(rlog.node(r), text, tr, r, p1, p2, flags, cachedelta, | |
|
152 | ifh, dfh) | |
|
153 | return dlog | |
|
154 | ||
|
155 | # Utilities to generate revisions for testing | |
|
156 | ||
|
157 | def genbits(n): | |
|
158 | '''Given a number n, generate (2 ** (n * 2) + 1) numbers in range(2 ** n). | |
|
159 | i.e. the generated numbers have a width of n bits. | |
|
160 | ||
|
161 | The combination of two adjacent numbers will cover all possible cases. | |
|
162 | That is to say, given any x, y where both x, and y are in range(2 ** n), | |
|
163 | there is an x followed immediately by y in the generated sequence. | |
|
164 | ''' | |
|
165 | m = 2 ** n | |
|
166 | ||
|
167 | # Gray Code. See https://en.wikipedia.org/wiki/Gray_code | |
|
168 | gray = lambda x: x ^ (x >> 1) | |
|
169 | ||
|
170 | # Generate (n * 2) bit gray code, yield lower n bits as X, and look for | |
|
171 | # the next unused gray code where higher n bits equal to X. | |
|
172 | ||
|
173 | # For gray codes whose higher bits are X, a[X] of them have been used. | |
|
174 | a = [0] * m | |
|
175 | ||
|
176 | # Iterate from 0. | |
|
177 | x = 0 | |
|
178 | yield x | |
|
179 | for i in range(m * m): | |
|
180 | y = gray(a[x] + x * m) & (m - 1) | |
|
181 | a[x] += 1 | |
|
182 | x = y | |
|
183 | yield x | |
|
184 | ||
|
185 | def gentext(rev): | |
|
186 | '''Given a revision number, generate dummy text''' | |
|
187 | return b''.join(b'%d\n' % j for j in range(-1, rev % 5)) | |
|
188 | ||
|
189 | def writecases(rlog, tr): | |
|
190 | '''Write some revisions interested to the test. | |
|
191 | ||
|
192 | The test is interested in 3 properties of a revision: | |
|
193 | ||
|
194 | - Is it a delta or a full text? (isdelta) | |
|
195 | This is to catch some delta application issues. | |
|
196 | - Does it have a flag of EXTSTORED? (isext) | |
|
197 | This is to catch some flag processor issues. Especially when | |
|
198 | interacted with revlog deltas. | |
|
199 | - Is its text empty? (isempty) | |
|
200 | This is less important. It is intended to try to catch some careless | |
|
201 | checks like "if text" instead of "if text is None". Note: if flag | |
|
202 | processor is involved, raw text may be not empty. | |
|
203 | ||
|
204 | Write 65 revisions. So that all combinations of the above flags for | |
|
205 | adjacent revisions are covered. That is to say, | |
|
206 | ||
|
207 | len(set( | |
|
208 | (r.delta, r.ext, r.empty, (r+1).delta, (r+1).ext, (r+1).empty) | |
|
209 | for r in range(len(rlog) - 1) | |
|
210 | )) is 64. | |
|
211 | ||
|
212 | Where "r.delta", "r.ext", and "r.empty" are booleans matching properties | |
|
213 | mentioned above. | |
|
214 | ||
|
215 | Return expected [(text, rawtext)]. | |
|
216 | ''' | |
|
217 | result = [] | |
|
218 | for i, x in enumerate(genbits(3)): | |
|
219 | isdelta, isext, isempty = bool(x & 1), bool(x & 2), bool(x & 4) | |
|
220 | if isempty: | |
|
221 | text = b'' | |
|
222 | else: | |
|
223 | text = gentext(i) | |
|
224 | rev = appendrev(rlog, text, tr, isext=isext, isdelta=isdelta) | |
|
225 | ||
|
226 | # Verify text, rawtext, and rawsize | |
|
227 | if isext: | |
|
228 | rawtext = writeprocessor(None, text)[0] | |
|
229 | else: | |
|
230 | rawtext = text | |
|
231 | if rlog.rawsize(rev) != len(rawtext): | |
|
232 | abort('rev %d: wrong rawsize' % rev) | |
|
233 | if rlog.revision(rev, raw=False) != text: | |
|
234 | abort('rev %d: wrong text' % rev) | |
|
235 | if rlog.revision(rev, raw=True) != rawtext: | |
|
236 | abort('rev %d: wrong rawtext' % rev) | |
|
237 | result.append((text, rawtext)) | |
|
238 | ||
|
239 | # Verify flags like isdelta, isext work as expected | |
|
240 | if bool(rlog.deltaparent(rev) > -1) != isdelta: | |
|
241 | abort('rev %d: isdelta is ineffective' % rev) | |
|
242 | if bool(rlog.flags(rev)) != isext: | |
|
243 | abort('rev %d: isext is ineffective' % rev) | |
|
244 | return result | |
|
245 | ||
|
246 | # Main test and checking | |
|
247 | ||
|
248 | def checkrevlog(rlog, expected): | |
|
249 | '''Check if revlog has expected contents. expected is [(text, rawtext)]''' | |
|
250 | # Test using different access orders. This could expose some issues | |
|
251 | # depending on revlog caching (see revlog._cache). | |
|
252 | for r0 in range(len(rlog) - 1): | |
|
253 | r1 = r0 + 1 | |
|
254 | for revorder in [[r0, r1], [r1, r0]]: | |
|
255 | for raworder in [[True], [False], [True, False], [False, True]]: | |
|
256 | nlog = newrevlog() | |
|
257 | for rev in revorder: | |
|
258 | for raw in raworder: | |
|
259 | t = nlog.revision(rev, raw=raw) | |
|
260 | if t != expected[rev][int(raw)]: | |
|
261 | abort('rev %d: corrupted %stext' | |
|
262 | % (rev, raw and 'raw' or '')) | |
|
263 | ||
|
264 | def maintest(): | |
|
265 | expected = rl = None | |
|
266 | with newtransaction() as tr: | |
|
267 | rl = newrevlog(recreate=True) | |
|
268 | expected = writecases(rl, tr) | |
|
269 | checkrevlog(rl, expected) | |
|
270 | print('local test passed') | |
|
271 | # Copy via revlog.addgroup | |
|
272 | rl1 = addgroupcopy(rl, tr) | |
|
273 | checkrevlog(rl1, expected) | |
|
274 | rl2 = addgroupcopy(rl, tr, optimaldelta=False) | |
|
275 | checkrevlog(rl2, expected) | |
|
276 | print('addgroupcopy test passed') | |
|
277 | # Copy via revlog.clone | |
|
278 | rl3 = newrevlog(name='_destrevlog3.i', recreate=True) | |
|
279 | rl.clone(tr, rl3) | |
|
280 | checkrevlog(rl3, expected) | |
|
281 | print('clone test passed') | |
|
282 | # Copy via low-level revlog._addrevision | |
|
283 | rl4 = lowlevelcopy(rl, tr) | |
|
284 | checkrevlog(rl4, expected) | |
|
285 | print('lowlevelcopy test passed') | |
|
286 | ||
|
287 | try: | |
|
288 | maintest() | |
|
289 | except Exception as ex: | |
|
290 | abort('crashed: %s' % ex) |
@@ -0,0 +1,1 b'' | |||
|
1 | abort: rev 5: wrong text |
General Comments 0
You need to be logged in to leave comments.
Login now