1 | """ |
---|
2 | Ported to Python 3. |
---|
3 | """ |
---|
4 | |
---|
5 | from six import ensure_text |
---|
6 | |
---|
7 | import os, json |
---|
8 | from urllib.parse import quote as url_quote |
---|
9 | |
---|
10 | from twisted.trial import unittest |
---|
11 | from twisted.internet import defer |
---|
12 | from twisted.internet.defer import inlineCallbacks, returnValue |
---|
13 | |
---|
14 | from allmydata.immutable import upload |
---|
15 | from allmydata.mutable.common import UnrecoverableFileError |
---|
16 | from allmydata.mutable.publish import MutableData |
---|
17 | from allmydata.util import idlib |
---|
18 | from allmydata.util import base32 |
---|
19 | from allmydata.interfaces import ICheckResults, ICheckAndRepairResults, \ |
---|
20 | IDeepCheckResults, IDeepCheckAndRepairResults |
---|
21 | from allmydata.monitor import Monitor, OperationCancelledError |
---|
22 | from allmydata.uri import LiteralFileURI |
---|
23 | |
---|
24 | from allmydata.test.common import ErrorMixin, _corrupt_mutable_share_data, \ |
---|
25 | ShouldFailMixin |
---|
26 | from .common_util import StallMixin, run_cli_unicode |
---|
27 | from .common_web import do_http |
---|
28 | from allmydata.test.no_network import GridTestMixin |
---|
29 | from .cli.common import CLITestMixin |
---|
30 | |
---|
31 | |
---|
32 | def run_cli(verb, *argv): |
---|
33 | """Match usage in existing tests by accept *args.""" |
---|
34 | return run_cli_unicode(verb, list(argv)) |
---|
35 | |
---|
36 | |
---|
37 | class MutableChecker(GridTestMixin, unittest.TestCase, ErrorMixin): |
---|
38 | def test_good(self): |
---|
39 | self.basedir = "deepcheck/MutableChecker/good" |
---|
40 | self.set_up_grid() |
---|
41 | CONTENTS = b"a little bit of data" |
---|
42 | CONTENTS_uploadable = MutableData(CONTENTS) |
---|
43 | d = self.g.clients[0].create_mutable_file(CONTENTS_uploadable) |
---|
44 | def _created(node): |
---|
45 | self.node = node |
---|
46 | self.fileurl = "uri/" + url_quote(node.get_uri()) |
---|
47 | d.addCallback(_created) |
---|
48 | # now make sure the webapi verifier sees no problems |
---|
49 | d.addCallback(lambda ign: self.GET(self.fileurl+"?t=check&verify=true", |
---|
50 | method="POST")) |
---|
51 | def _got_results(out): |
---|
52 | self.failUnless(b"<span>Healthy : Healthy</span>" in out, out) |
---|
53 | self.failUnless(b"Recoverable Versions: 10*seq1-" in out, out) |
---|
54 | self.failIf(b"Not Healthy!" in out, out) |
---|
55 | self.failIf(b"Unhealthy" in out, out) |
---|
56 | self.failIf(b"Corrupt Shares" in out, out) |
---|
57 | d.addCallback(_got_results) |
---|
58 | d.addErrback(self.explain_web_error) |
---|
59 | return d |
---|
60 | |
---|
61 | def test_corrupt(self): |
---|
62 | self.basedir = "deepcheck/MutableChecker/corrupt" |
---|
63 | self.set_up_grid() |
---|
64 | CONTENTS = b"a little bit of data" |
---|
65 | CONTENTS_uploadable = MutableData(CONTENTS) |
---|
66 | d = self.g.clients[0].create_mutable_file(CONTENTS_uploadable) |
---|
67 | def _stash_and_corrupt(node): |
---|
68 | self.node = node |
---|
69 | self.fileurl = "uri/" + url_quote(node.get_uri()) |
---|
70 | self.corrupt_shares_numbered(node.get_uri(), [0], |
---|
71 | _corrupt_mutable_share_data) |
---|
72 | d.addCallback(_stash_and_corrupt) |
---|
73 | # now make sure the webapi verifier notices it |
---|
74 | d.addCallback(lambda ign: self.GET(self.fileurl+"?t=check&verify=true", |
---|
75 | method="POST")) |
---|
76 | def _got_results(out): |
---|
77 | self.failUnless(b"Not Healthy!" in out, out) |
---|
78 | self.failUnless(b"Unhealthy: best version has only 9 shares (encoding is 3-of-10)" in out, out) |
---|
79 | self.failUnless(b"Corrupt Shares:" in out, out) |
---|
80 | d.addCallback(_got_results) |
---|
81 | |
---|
82 | # now make sure the webapi repairer can fix it |
---|
83 | d.addCallback(lambda ign: |
---|
84 | self.GET(self.fileurl+"?t=check&verify=true&repair=true", |
---|
85 | method="POST")) |
---|
86 | def _got_repair_results(out): |
---|
87 | self.failUnless(b"<div>Repair successful</div>" in out, out) |
---|
88 | d.addCallback(_got_repair_results) |
---|
89 | d.addCallback(lambda ign: self.GET(self.fileurl+"?t=check&verify=true", |
---|
90 | method="POST")) |
---|
91 | def _got_postrepair_results(out): |
---|
92 | self.failIf(b"Not Healthy!" in out, out) |
---|
93 | self.failUnless(b"Recoverable Versions: 10*seq" in out, out) |
---|
94 | d.addCallback(_got_postrepair_results) |
---|
95 | d.addErrback(self.explain_web_error) |
---|
96 | |
---|
97 | return d |
---|
98 | |
---|
99 | def test_delete_share(self): |
---|
100 | self.basedir = "deepcheck/MutableChecker/delete_share" |
---|
101 | self.set_up_grid() |
---|
102 | CONTENTS = b"a little bit of data" |
---|
103 | CONTENTS_uploadable = MutableData(CONTENTS) |
---|
104 | d = self.g.clients[0].create_mutable_file(CONTENTS_uploadable) |
---|
105 | def _stash_and_delete(node): |
---|
106 | self.node = node |
---|
107 | self.fileurl = "uri/" + url_quote(node.get_uri()) |
---|
108 | self.delete_shares_numbered(node.get_uri(), [0]) |
---|
109 | d.addCallback(_stash_and_delete) |
---|
110 | # now make sure the webapi checker notices it |
---|
111 | d.addCallback(lambda ign: self.GET(self.fileurl+"?t=check&verify=false", |
---|
112 | method="POST")) |
---|
113 | def _got_results(out): |
---|
114 | self.failUnless(b"Not Healthy!" in out, out) |
---|
115 | self.failUnless(b"Unhealthy: best version has only 9 shares (encoding is 3-of-10)" in out, out) |
---|
116 | self.failIf(b"Corrupt Shares" in out, out) |
---|
117 | d.addCallback(_got_results) |
---|
118 | |
---|
119 | # now make sure the webapi repairer can fix it |
---|
120 | d.addCallback(lambda ign: |
---|
121 | self.GET(self.fileurl+"?t=check&verify=false&repair=true", |
---|
122 | method="POST")) |
---|
123 | def _got_repair_results(out): |
---|
124 | self.failUnless(b"Repair successful" in out) |
---|
125 | d.addCallback(_got_repair_results) |
---|
126 | d.addCallback(lambda ign: self.GET(self.fileurl+"?t=check&verify=false", |
---|
127 | method="POST")) |
---|
128 | def _got_postrepair_results(out): |
---|
129 | self.failIf(b"Not Healthy!" in out, out) |
---|
130 | self.failUnless(b"Recoverable Versions: 10*seq" in out) |
---|
131 | d.addCallback(_got_postrepair_results) |
---|
132 | d.addErrback(self.explain_web_error) |
---|
133 | |
---|
134 | return d |
---|
135 | |
---|
136 | |
---|
137 | class DeepCheckBase(GridTestMixin, ErrorMixin, StallMixin, ShouldFailMixin, |
---|
138 | CLITestMixin): |
---|
139 | |
---|
140 | def web_json(self, n, **kwargs): |
---|
141 | kwargs["output"] = "json" |
---|
142 | d = self.web(n, "POST", **kwargs) |
---|
143 | d.addCallback(self.decode_json) |
---|
144 | return d |
---|
145 | |
---|
146 | def decode_json(self, args): |
---|
147 | (s, url) = args |
---|
148 | try: |
---|
149 | data = json.loads(s) |
---|
150 | except ValueError: |
---|
151 | self.fail("%s: not JSON: '%s'" % (url, s)) |
---|
152 | return data |
---|
153 | |
---|
154 | def parse_streamed_json(self, s): |
---|
155 | s = ensure_text(s) |
---|
156 | for unit in s.split("\n"): |
---|
157 | if not unit: |
---|
158 | # stream should end with a newline, so split returns "" |
---|
159 | continue |
---|
160 | try: |
---|
161 | yield json.loads(unit) |
---|
162 | except ValueError as le: |
---|
163 | le.args = tuple(le.args + (unit,)) |
---|
164 | raise |
---|
165 | |
---|
166 | @inlineCallbacks |
---|
167 | def web(self, n, method="GET", **kwargs): |
---|
168 | # returns (data, url) |
---|
169 | url = (self.client_baseurls[0] + "uri/%s" % url_quote(n.get_uri()) |
---|
170 | + "?" + "&".join(["%s=%s" % (k,str(v, "ascii") if isinstance(v, bytes) else v) for (k,v) in kwargs.items()])) |
---|
171 | data = yield do_http(method, url, browser_like_redirects=True) |
---|
172 | returnValue((data,url)) |
---|
173 | |
---|
174 | @inlineCallbacks |
---|
175 | def wait_for_operation(self, ophandle): |
---|
176 | url = self.client_baseurls[0] + "operations/" + str(ophandle, "ascii") |
---|
177 | url += "?t=status&output=JSON" |
---|
178 | while True: |
---|
179 | body = yield do_http("get", url) |
---|
180 | data = json.loads(body) |
---|
181 | if data["finished"]: |
---|
182 | break |
---|
183 | yield self.stall(delay=0.1) |
---|
184 | returnValue(data) |
---|
185 | |
---|
186 | @inlineCallbacks |
---|
187 | def get_operation_results(self, ophandle, output=None): |
---|
188 | url = self.client_baseurls[0] + "operations/" + str(ophandle, "ascii") |
---|
189 | url += "?t=status" |
---|
190 | if output: |
---|
191 | url += "&output=" + output |
---|
192 | body = yield do_http("get", url) |
---|
193 | if output and output.lower() == "json": |
---|
194 | data = json.loads(body) |
---|
195 | else: |
---|
196 | data = body |
---|
197 | returnValue(data) |
---|
198 | |
---|
199 | @inlineCallbacks |
---|
200 | def slow_web(self, n, output=None, **kwargs): |
---|
201 | # use ophandle= |
---|
202 | handle = base32.b2a(os.urandom(4)) |
---|
203 | yield self.web(n, "POST", ophandle=handle, **kwargs) |
---|
204 | yield self.wait_for_operation(handle) |
---|
205 | data = yield self.get_operation_results(handle, output=output) |
---|
206 | returnValue(data) |
---|
207 | |
---|
208 | |
---|
209 | class DeepCheckWebGood(DeepCheckBase, unittest.TestCase): |
---|
210 | # construct a small directory tree (with one dir, one immutable file, one |
---|
211 | # mutable file, two LIT files, one DIR2:LIT empty dir, one DIR2:LIT tiny |
---|
212 | # dir, and a loop), and then check/examine it in various ways. |
---|
213 | |
---|
214 | def set_up_tree(self): |
---|
215 | # 2.9s |
---|
216 | |
---|
217 | c0 = self.g.clients[0] |
---|
218 | d = c0.create_dirnode() |
---|
219 | def _created_root(n): |
---|
220 | self.root = n |
---|
221 | self.root_uri = n.get_uri() |
---|
222 | d.addCallback(_created_root) |
---|
223 | d.addCallback(lambda ign: |
---|
224 | c0.create_mutable_file(MutableData(b"mutable file contents"))) |
---|
225 | d.addCallback(lambda n: self.root.set_node(u"mutable", n)) |
---|
226 | def _created_mutable(n): |
---|
227 | self.mutable = n |
---|
228 | self.mutable_uri = n.get_uri() |
---|
229 | d.addCallback(_created_mutable) |
---|
230 | |
---|
231 | large = upload.Data(b"Lots of data\n" * 1000, None) |
---|
232 | d.addCallback(lambda ign: self.root.add_file(u"large", large)) |
---|
233 | def _created_large(n): |
---|
234 | self.large = n |
---|
235 | self.large_uri = n.get_uri() |
---|
236 | d.addCallback(_created_large) |
---|
237 | |
---|
238 | small = upload.Data(b"Small enough for a LIT", None) |
---|
239 | d.addCallback(lambda ign: self.root.add_file(u"small", small)) |
---|
240 | def _created_small(n): |
---|
241 | self.small = n |
---|
242 | self.small_uri = n.get_uri() |
---|
243 | d.addCallback(_created_small) |
---|
244 | |
---|
245 | small2 = upload.Data(b"Small enough for a LIT too", None) |
---|
246 | d.addCallback(lambda ign: self.root.add_file(u"small2", small2)) |
---|
247 | def _created_small2(n): |
---|
248 | self.small2 = n |
---|
249 | self.small2_uri = n.get_uri() |
---|
250 | d.addCallback(_created_small2) |
---|
251 | |
---|
252 | empty_litdir_uri = b"URI:DIR2-LIT:" |
---|
253 | tiny_litdir_uri = b"URI:DIR2-LIT:gqytunj2onug64tufqzdcosvkjetutcjkq5gw4tvm5vwszdgnz5hgyzufqydulbshj5x2lbm" # contains one child which is itself also LIT |
---|
254 | |
---|
255 | d.addCallback(lambda ign: self.root._create_and_validate_node(None, empty_litdir_uri, name=u"test_deepcheck empty_lit_dir")) |
---|
256 | def _created_empty_lit_dir(n): |
---|
257 | self.empty_lit_dir = n |
---|
258 | self.empty_lit_dir_uri = n.get_uri() |
---|
259 | self.root.set_node(u"empty_lit_dir", n) |
---|
260 | d.addCallback(_created_empty_lit_dir) |
---|
261 | |
---|
262 | d.addCallback(lambda ign: self.root._create_and_validate_node(None, tiny_litdir_uri, name=u"test_deepcheck tiny_lit_dir")) |
---|
263 | def _created_tiny_lit_dir(n): |
---|
264 | self.tiny_lit_dir = n |
---|
265 | self.tiny_lit_dir_uri = n.get_uri() |
---|
266 | self.root.set_node(u"tiny_lit_dir", n) |
---|
267 | d.addCallback(_created_tiny_lit_dir) |
---|
268 | |
---|
269 | d.addCallback(lambda ign: self.root.set_node(u"loop", self.root)) |
---|
270 | return d |
---|
271 | |
---|
272 | def check_is_healthy(self, cr, n, where, incomplete=False): |
---|
273 | self.failUnless(ICheckResults.providedBy(cr), where) |
---|
274 | self.failUnless(cr.is_healthy(), where) |
---|
275 | self.failUnlessEqual(cr.get_storage_index(), n.get_storage_index(), |
---|
276 | where) |
---|
277 | self.failUnlessEqual(cr.get_storage_index_string(), |
---|
278 | base32.b2a(n.get_storage_index()), where) |
---|
279 | num_servers = len(self.g.all_servers) |
---|
280 | self.failUnlessEqual(num_servers, 10, where) |
---|
281 | |
---|
282 | self.failUnlessEqual(cr.get_happiness(), num_servers, where) |
---|
283 | self.failUnlessEqual(cr.get_share_counter_good(), num_servers, where) |
---|
284 | self.failUnlessEqual(cr.get_encoding_needed(), 3, where) |
---|
285 | self.failUnlessEqual(cr.get_encoding_expected(), num_servers, where) |
---|
286 | if not incomplete: |
---|
287 | self.failUnlessEqual(cr.get_host_counter_good_shares(), |
---|
288 | num_servers, where) |
---|
289 | self.failUnlessEqual(cr.get_corrupt_shares(), [], where) |
---|
290 | if not incomplete: |
---|
291 | self.failUnlessEqual(sorted([s.get_serverid() |
---|
292 | for s in cr.get_servers_responding()]), |
---|
293 | sorted(self.g.get_all_serverids()), |
---|
294 | where) |
---|
295 | all_serverids = set() |
---|
296 | for (shareid, servers) in list(cr.get_sharemap().items()): |
---|
297 | all_serverids.update([s.get_serverid() for s in servers]) |
---|
298 | self.failUnlessEqual(sorted(all_serverids), |
---|
299 | sorted(self.g.get_all_serverids()), |
---|
300 | where) |
---|
301 | |
---|
302 | self.failUnlessEqual(cr.get_share_counter_wrong(), 0, where) |
---|
303 | self.failUnlessEqual(cr.get_version_counter_recoverable(), 1, where) |
---|
304 | self.failUnlessEqual(cr.get_version_counter_unrecoverable(), 0, where) |
---|
305 | |
---|
306 | |
---|
307 | def check_and_repair_is_healthy(self, cr, n, where, incomplete=False): |
---|
308 | self.failUnless(ICheckAndRepairResults.providedBy(cr), (where, cr)) |
---|
309 | self.failUnless(cr.get_pre_repair_results().is_healthy(), where) |
---|
310 | self.check_is_healthy(cr.get_pre_repair_results(), n, where, incomplete) |
---|
311 | self.failUnless(cr.get_post_repair_results().is_healthy(), where) |
---|
312 | self.check_is_healthy(cr.get_post_repair_results(), n, where, incomplete) |
---|
313 | self.failIf(cr.get_repair_attempted(), where) |
---|
314 | |
---|
315 | def deep_check_is_healthy(self, cr, num_healthy, where): |
---|
316 | self.failUnless(IDeepCheckResults.providedBy(cr)) |
---|
317 | self.failUnlessEqual(cr.get_counters()["count-objects-healthy"], |
---|
318 | num_healthy, where) |
---|
319 | |
---|
320 | def deep_check_and_repair_is_healthy(self, cr, num_healthy, where): |
---|
321 | self.failUnless(IDeepCheckAndRepairResults.providedBy(cr), where) |
---|
322 | c = cr.get_counters() |
---|
323 | self.failUnlessEqual(c["count-objects-healthy-pre-repair"], |
---|
324 | num_healthy, where) |
---|
325 | self.failUnlessEqual(c["count-objects-healthy-post-repair"], |
---|
326 | num_healthy, where) |
---|
327 | self.failUnlessEqual(c["count-repairs-attempted"], 0, where) |
---|
328 | |
---|
329 | def test_good(self): |
---|
330 | self.basedir = "deepcheck/DeepCheckWebGood/good" |
---|
331 | self.set_up_grid() |
---|
332 | d = self.set_up_tree() |
---|
333 | d.addCallback(self.do_stats) |
---|
334 | d.addCallback(self.do_web_stream_manifest) |
---|
335 | d.addCallback(self.do_web_stream_check) |
---|
336 | d.addCallback(self.do_test_check_good) |
---|
337 | d.addCallback(self.do_test_web_good) |
---|
338 | d.addCallback(self.do_test_cli_good) |
---|
339 | d.addErrback(self.explain_web_error) |
---|
340 | d.addErrback(self.explain_error) |
---|
341 | return d |
---|
342 | |
---|
343 | def do_stats(self, ignored): |
---|
344 | d = defer.succeed(None) |
---|
345 | d.addCallback(lambda ign: self.root.start_deep_stats().when_done()) |
---|
346 | d.addCallback(self.check_stats_good) |
---|
347 | return d |
---|
348 | |
---|
349 | def check_stats_good(self, s): |
---|
350 | self.failUnlessEqual(s["count-directories"], 3) |
---|
351 | self.failUnlessEqual(s["count-files"], 5) |
---|
352 | self.failUnlessEqual(s["count-immutable-files"], 1) |
---|
353 | self.failUnlessEqual(s["count-literal-files"], 3) |
---|
354 | self.failUnlessEqual(s["count-mutable-files"], 1) |
---|
355 | # don't check directories: their size will vary |
---|
356 | # s["largest-directory"] |
---|
357 | # s["size-directories"] |
---|
358 | self.failUnlessEqual(s["largest-directory-children"], 7) |
---|
359 | self.failUnlessEqual(s["largest-immutable-file"], 13000) |
---|
360 | # to re-use this function for both the local |
---|
361 | # dirnode.start_deep_stats() and the webapi t=start-deep-stats, we |
---|
362 | # coerce the result into a list of tuples. dirnode.start_deep_stats() |
---|
363 | # returns a list of tuples, but JSON only knows about lists., so |
---|
364 | # t=start-deep-stats returns a list of lists. |
---|
365 | histogram = [tuple(stuff) for stuff in s["size-files-histogram"]] |
---|
366 | self.failUnlessEqual(histogram, [(4, 10, 1), (11, 31, 2), |
---|
367 | (10001, 31622, 1), |
---|
368 | ]) |
---|
369 | self.failUnlessEqual(s["size-immutable-files"], 13000) |
---|
370 | self.failUnlessEqual(s["size-literal-files"], 56) |
---|
371 | |
---|
372 | def do_web_stream_manifest(self, ignored): |
---|
373 | d = self.web(self.root, method="POST", t="stream-manifest") |
---|
374 | d.addCallback(lambda output_and_url: |
---|
375 | self._check_streamed_manifest(output_and_url[0])) |
---|
376 | return d |
---|
377 | |
---|
378 | def _check_streamed_manifest(self, output): |
---|
379 | units = list(self.parse_streamed_json(output)) |
---|
380 | files = [u for u in units if u["type"] in ("file", "directory")] |
---|
381 | assert units[-1]["type"] == "stats" |
---|
382 | stats = units[-1]["stats"] |
---|
383 | self.failUnlessEqual(len(files), 8) |
---|
384 | # [root,mutable,large] are distributed, [small,small2,empty_litdir,tiny_litdir] are not |
---|
385 | self.failUnlessEqual(len([f for f in files |
---|
386 | if f["verifycap"] != ""]), 3) |
---|
387 | self.failUnlessEqual(len([f for f in files |
---|
388 | if f["verifycap"] == ""]), 5) |
---|
389 | self.failUnlessEqual(len([f for f in files |
---|
390 | if f["repaircap"] != ""]), 3) |
---|
391 | self.failUnlessEqual(len([f for f in files |
---|
392 | if f["repaircap"] == ""]), 5) |
---|
393 | self.failUnlessEqual(len([f for f in files |
---|
394 | if f["storage-index"] != ""]), 3) |
---|
395 | self.failUnlessEqual(len([f for f in files |
---|
396 | if f["storage-index"] == ""]), 5) |
---|
397 | # make sure that a mutable file has filecap==repaircap!=verifycap |
---|
398 | mutable = [f for f in files |
---|
399 | if f["cap"] is not None |
---|
400 | and f["cap"].startswith("URI:SSK:")][0] |
---|
401 | self.failUnlessEqual(mutable["cap"].encode("ascii"), self.mutable_uri) |
---|
402 | self.failIfEqual(mutable["cap"], mutable["verifycap"]) |
---|
403 | self.failUnlessEqual(mutable["cap"], mutable["repaircap"]) |
---|
404 | # for immutable file, verifycap==repaircap!=filecap |
---|
405 | large = [f for f in files |
---|
406 | if f["cap"] is not None |
---|
407 | and f["cap"].startswith("URI:CHK:")][0] |
---|
408 | self.failUnlessEqual(large["cap"].encode("ascii"), self.large_uri) |
---|
409 | self.failIfEqual(large["cap"], large["verifycap"]) |
---|
410 | self.failUnlessEqual(large["verifycap"], large["repaircap"]) |
---|
411 | self.check_stats_good(stats) |
---|
412 | |
---|
413 | def do_web_stream_check(self, ignored): |
---|
414 | # TODO |
---|
415 | return |
---|
416 | d = self.web(self.root, t="stream-deep-check") |
---|
417 | def _check(res): |
---|
418 | units = list(self.parse_streamed_json(res)) |
---|
419 | #files = [u for u in units if u["type"] in ("file", "directory")] |
---|
420 | assert units[-1]["type"] == "stats" |
---|
421 | #stats = units[-1]["stats"] |
---|
422 | # ... |
---|
423 | d.addCallback(_check) |
---|
424 | return d |
---|
425 | |
---|
426 | def do_test_check_good(self, ignored): |
---|
427 | d = defer.succeed(None) |
---|
428 | # check the individual items |
---|
429 | d.addCallback(lambda ign: self.root.check(Monitor())) |
---|
430 | d.addCallback(self.check_is_healthy, self.root, "root") |
---|
431 | d.addCallback(lambda ign: self.mutable.check(Monitor())) |
---|
432 | d.addCallback(self.check_is_healthy, self.mutable, "mutable") |
---|
433 | d.addCallback(lambda ign: self.large.check(Monitor())) |
---|
434 | d.addCallback(self.check_is_healthy, self.large, "large") |
---|
435 | d.addCallback(lambda ign: self.small.check(Monitor())) |
---|
436 | d.addCallback(self.failUnlessEqual, None, "small") |
---|
437 | d.addCallback(lambda ign: self.small2.check(Monitor())) |
---|
438 | d.addCallback(self.failUnlessEqual, None, "small2") |
---|
439 | d.addCallback(lambda ign: self.empty_lit_dir.check(Monitor())) |
---|
440 | d.addCallback(self.failUnlessEqual, None, "empty_lit_dir") |
---|
441 | d.addCallback(lambda ign: self.tiny_lit_dir.check(Monitor())) |
---|
442 | d.addCallback(self.failUnlessEqual, None, "tiny_lit_dir") |
---|
443 | |
---|
444 | # and again with verify=True |
---|
445 | d.addCallback(lambda ign: self.root.check(Monitor(), verify=True)) |
---|
446 | d.addCallback(self.check_is_healthy, self.root, "root") |
---|
447 | d.addCallback(lambda ign: self.mutable.check(Monitor(), verify=True)) |
---|
448 | d.addCallback(self.check_is_healthy, self.mutable, "mutable") |
---|
449 | d.addCallback(lambda ign: self.large.check(Monitor(), verify=True)) |
---|
450 | d.addCallback(self.check_is_healthy, self.large, "large", incomplete=True) |
---|
451 | d.addCallback(lambda ign: self.small.check(Monitor(), verify=True)) |
---|
452 | d.addCallback(self.failUnlessEqual, None, "small") |
---|
453 | d.addCallback(lambda ign: self.small2.check(Monitor(), verify=True)) |
---|
454 | d.addCallback(self.failUnlessEqual, None, "small2") |
---|
455 | d.addCallback(lambda ign: self.empty_lit_dir.check(Monitor(), verify=True)) |
---|
456 | d.addCallback(self.failUnlessEqual, None, "empty_lit_dir") |
---|
457 | d.addCallback(lambda ign: self.tiny_lit_dir.check(Monitor(), verify=True)) |
---|
458 | d.addCallback(self.failUnlessEqual, None, "tiny_lit_dir") |
---|
459 | |
---|
460 | # and check_and_repair(), which should be a nop |
---|
461 | d.addCallback(lambda ign: self.root.check_and_repair(Monitor())) |
---|
462 | d.addCallback(self.check_and_repair_is_healthy, self.root, "root") |
---|
463 | d.addCallback(lambda ign: self.mutable.check_and_repair(Monitor())) |
---|
464 | d.addCallback(self.check_and_repair_is_healthy, self.mutable, "mutable") |
---|
465 | d.addCallback(lambda ign: self.large.check_and_repair(Monitor())) |
---|
466 | d.addCallback(self.check_and_repair_is_healthy, self.large, "large") |
---|
467 | d.addCallback(lambda ign: self.small.check_and_repair(Monitor())) |
---|
468 | d.addCallback(self.failUnlessEqual, None, "small") |
---|
469 | d.addCallback(lambda ign: self.small2.check_and_repair(Monitor())) |
---|
470 | d.addCallback(self.failUnlessEqual, None, "small2") |
---|
471 | d.addCallback(lambda ign: self.empty_lit_dir.check_and_repair(Monitor())) |
---|
472 | d.addCallback(self.failUnlessEqual, None, "empty_lit_dir") |
---|
473 | d.addCallback(lambda ign: self.tiny_lit_dir.check_and_repair(Monitor())) |
---|
474 | |
---|
475 | # check_and_repair(verify=True) |
---|
476 | d.addCallback(lambda ign: self.root.check_and_repair(Monitor(), verify=True)) |
---|
477 | d.addCallback(self.check_and_repair_is_healthy, self.root, "root") |
---|
478 | d.addCallback(lambda ign: self.mutable.check_and_repair(Monitor(), verify=True)) |
---|
479 | d.addCallback(self.check_and_repair_is_healthy, self.mutable, "mutable") |
---|
480 | d.addCallback(lambda ign: self.large.check_and_repair(Monitor(), verify=True)) |
---|
481 | d.addCallback(self.check_and_repair_is_healthy, self.large, "large", incomplete=True) |
---|
482 | d.addCallback(lambda ign: self.small.check_and_repair(Monitor(), verify=True)) |
---|
483 | d.addCallback(self.failUnlessEqual, None, "small") |
---|
484 | d.addCallback(lambda ign: self.small2.check_and_repair(Monitor(), verify=True)) |
---|
485 | d.addCallback(self.failUnlessEqual, None, "small2") |
---|
486 | d.addCallback(self.failUnlessEqual, None, "small2") |
---|
487 | d.addCallback(lambda ign: self.empty_lit_dir.check_and_repair(Monitor(), verify=True)) |
---|
488 | d.addCallback(self.failUnlessEqual, None, "empty_lit_dir") |
---|
489 | d.addCallback(lambda ign: self.tiny_lit_dir.check_and_repair(Monitor(), verify=True)) |
---|
490 | |
---|
491 | |
---|
492 | # now deep-check the root, with various verify= and repair= options |
---|
493 | d.addCallback(lambda ign: |
---|
494 | self.root.start_deep_check().when_done()) |
---|
495 | d.addCallback(self.deep_check_is_healthy, 3, "root") |
---|
496 | d.addCallback(lambda ign: |
---|
497 | self.root.start_deep_check(verify=True).when_done()) |
---|
498 | d.addCallback(self.deep_check_is_healthy, 3, "root") |
---|
499 | d.addCallback(lambda ign: |
---|
500 | self.root.start_deep_check_and_repair().when_done()) |
---|
501 | d.addCallback(self.deep_check_and_repair_is_healthy, 3, "root") |
---|
502 | d.addCallback(lambda ign: |
---|
503 | self.root.start_deep_check_and_repair(verify=True).when_done()) |
---|
504 | d.addCallback(self.deep_check_and_repair_is_healthy, 3, "root") |
---|
505 | |
---|
506 | # and finally, start a deep-check, but then cancel it. |
---|
507 | d.addCallback(lambda ign: self.root.start_deep_check()) |
---|
508 | def _checking(monitor): |
---|
509 | monitor.cancel() |
---|
510 | d = monitor.when_done() |
---|
511 | # this should fire as soon as the next dirnode.list finishes. |
---|
512 | # TODO: add a counter to measure how many list() calls are made, |
---|
513 | # assert that no more than one gets to run before the cancel() |
---|
514 | # takes effect. |
---|
515 | def _finished_normally(res): |
---|
516 | self.fail("this was supposed to fail, not finish normally") |
---|
517 | def _cancelled(f): |
---|
518 | f.trap(OperationCancelledError) |
---|
519 | d.addCallbacks(_finished_normally, _cancelled) |
---|
520 | return d |
---|
521 | d.addCallback(_checking) |
---|
522 | |
---|
523 | return d |
---|
524 | |
---|
525 | def json_check_is_healthy(self, data, n, where, incomplete=False): |
---|
526 | |
---|
527 | self.failUnlessEqual(data["storage-index"], |
---|
528 | str(base32.b2a(n.get_storage_index()), "ascii"), where) |
---|
529 | self.failUnless("summary" in data, (where, data)) |
---|
530 | self.failUnlessEqual(data["summary"].lower(), "healthy", |
---|
531 | "%s: '%s'" % (where, data["summary"])) |
---|
532 | r = data["results"] |
---|
533 | self.failUnlessEqual(r["healthy"], True, where) |
---|
534 | num_servers = len(self.g.all_servers) |
---|
535 | self.failUnlessEqual(num_servers, 10) |
---|
536 | |
---|
537 | self.failIfIn("needs-rebalancing", r) |
---|
538 | self.failUnlessEqual(r["count-happiness"], num_servers, where) |
---|
539 | self.failUnlessEqual(r["count-shares-good"], num_servers, where) |
---|
540 | self.failUnlessEqual(r["count-shares-needed"], 3, where) |
---|
541 | self.failUnlessEqual(r["count-shares-expected"], num_servers, where) |
---|
542 | if not incomplete: |
---|
543 | self.failUnlessEqual(r["count-good-share-hosts"], num_servers, |
---|
544 | where) |
---|
545 | self.failUnlessEqual(r["count-corrupt-shares"], 0, where) |
---|
546 | self.failUnlessEqual(r["list-corrupt-shares"], [], where) |
---|
547 | if not incomplete: |
---|
548 | self.failUnlessEqual(sorted(r["servers-responding"]), |
---|
549 | sorted([idlib.nodeid_b2a(sid) |
---|
550 | for sid in self.g.get_all_serverids()]), |
---|
551 | where) |
---|
552 | self.failUnless("sharemap" in r, where) |
---|
553 | all_serverids = set() |
---|
554 | for (shareid, serverids_s) in list(r["sharemap"].items()): |
---|
555 | all_serverids.update(serverids_s) |
---|
556 | self.failUnlessEqual(sorted(all_serverids), |
---|
557 | sorted([idlib.nodeid_b2a(sid) |
---|
558 | for sid in self.g.get_all_serverids()]), |
---|
559 | where) |
---|
560 | self.failUnlessEqual(r["count-wrong-shares"], 0, where) |
---|
561 | self.failUnlessEqual(r["count-recoverable-versions"], 1, where) |
---|
562 | self.failUnlessEqual(r["count-unrecoverable-versions"], 0, where) |
---|
563 | |
---|
564 | def json_check_and_repair_is_healthy(self, data, n, where, incomplete=False): |
---|
565 | self.failUnlessEqual(data["storage-index"], |
---|
566 | str(base32.b2a(n.get_storage_index()), "ascii"), where) |
---|
567 | self.failUnlessEqual(data["repair-attempted"], False, where) |
---|
568 | self.json_check_is_healthy(data["pre-repair-results"], |
---|
569 | n, where, incomplete) |
---|
570 | self.json_check_is_healthy(data["post-repair-results"], |
---|
571 | n, where, incomplete) |
---|
572 | |
---|
573 | def json_full_deepcheck_is_healthy(self, data, n, where): |
---|
574 | self.failUnlessEqual(data["root-storage-index"], |
---|
575 | str(base32.b2a(n.get_storage_index()), "ascii"), where) |
---|
576 | self.failUnlessEqual(data["count-objects-checked"], 3, where) |
---|
577 | self.failUnlessEqual(data["count-objects-healthy"], 3, where) |
---|
578 | self.failUnlessEqual(data["count-objects-unhealthy"], 0, where) |
---|
579 | self.failUnlessEqual(data["count-corrupt-shares"], 0, where) |
---|
580 | self.failUnlessEqual(data["list-corrupt-shares"], [], where) |
---|
581 | self.failUnlessEqual(data["list-unhealthy-files"], [], where) |
---|
582 | self.json_check_stats_good(data["stats"], where) |
---|
583 | |
---|
584 | def json_full_deepcheck_and_repair_is_healthy(self, data, n, where): |
---|
585 | self.failUnlessEqual(data["root-storage-index"], |
---|
586 | str(base32.b2a(n.get_storage_index()), "ascii"), where) |
---|
587 | self.failUnlessEqual(data["count-objects-checked"], 3, where) |
---|
588 | |
---|
589 | self.failUnlessEqual(data["count-objects-healthy-pre-repair"], 3, where) |
---|
590 | self.failUnlessEqual(data["count-objects-unhealthy-pre-repair"], 0, where) |
---|
591 | self.failUnlessEqual(data["count-corrupt-shares-pre-repair"], 0, where) |
---|
592 | |
---|
593 | self.failUnlessEqual(data["count-objects-healthy-post-repair"], 3, where) |
---|
594 | self.failUnlessEqual(data["count-objects-unhealthy-post-repair"], 0, where) |
---|
595 | self.failUnlessEqual(data["count-corrupt-shares-post-repair"], 0, where) |
---|
596 | |
---|
597 | self.failUnlessEqual(data["list-corrupt-shares"], [], where) |
---|
598 | self.failUnlessEqual(data["list-remaining-corrupt-shares"], [], where) |
---|
599 | self.failUnlessEqual(data["list-unhealthy-files"], [], where) |
---|
600 | |
---|
601 | self.failUnlessEqual(data["count-repairs-attempted"], 0, where) |
---|
602 | self.failUnlessEqual(data["count-repairs-successful"], 0, where) |
---|
603 | self.failUnlessEqual(data["count-repairs-unsuccessful"], 0, where) |
---|
604 | |
---|
605 | |
---|
606 | def json_check_lit(self, data, n, where): |
---|
607 | self.failUnlessEqual(data["storage-index"], "", where) |
---|
608 | self.failUnlessEqual(data["results"]["healthy"], True, where) |
---|
609 | |
---|
610 | def json_check_stats_good(self, data, where): |
---|
611 | self.check_stats_good(data) |
---|
612 | |
---|
613 | def do_test_web_good(self, ignored): |
---|
614 | d = defer.succeed(None) |
---|
615 | |
---|
616 | # stats |
---|
617 | d.addCallback(lambda ign: |
---|
618 | self.slow_web(self.root, |
---|
619 | t="start-deep-stats", output="json")) |
---|
620 | d.addCallback(self.json_check_stats_good, "deep-stats") |
---|
621 | |
---|
622 | # check, no verify |
---|
623 | d.addCallback(lambda ign: self.web_json(self.root, t="check")) |
---|
624 | d.addCallback(self.json_check_is_healthy, self.root, "root") |
---|
625 | d.addCallback(lambda ign: self.web_json(self.mutable, t="check")) |
---|
626 | d.addCallback(self.json_check_is_healthy, self.mutable, "mutable") |
---|
627 | d.addCallback(lambda ign: self.web_json(self.large, t="check")) |
---|
628 | d.addCallback(self.json_check_is_healthy, self.large, "large") |
---|
629 | d.addCallback(lambda ign: self.web_json(self.small, t="check")) |
---|
630 | d.addCallback(self.json_check_lit, self.small, "small") |
---|
631 | d.addCallback(lambda ign: self.web_json(self.small2, t="check")) |
---|
632 | d.addCallback(self.json_check_lit, self.small2, "small2") |
---|
633 | d.addCallback(lambda ign: self.web_json(self.empty_lit_dir, t="check")) |
---|
634 | d.addCallback(self.json_check_lit, self.empty_lit_dir, "empty_lit_dir") |
---|
635 | d.addCallback(lambda ign: self.web_json(self.tiny_lit_dir, t="check")) |
---|
636 | d.addCallback(self.json_check_lit, self.tiny_lit_dir, "tiny_lit_dir") |
---|
637 | |
---|
638 | # check and verify |
---|
639 | d.addCallback(lambda ign: |
---|
640 | self.web_json(self.root, t="check", verify="true")) |
---|
641 | d.addCallback(self.json_check_is_healthy, self.root, "root+v") |
---|
642 | d.addCallback(lambda ign: |
---|
643 | self.web_json(self.mutable, t="check", verify="true")) |
---|
644 | d.addCallback(self.json_check_is_healthy, self.mutable, "mutable+v") |
---|
645 | d.addCallback(lambda ign: |
---|
646 | self.web_json(self.large, t="check", verify="true")) |
---|
647 | d.addCallback(self.json_check_is_healthy, self.large, "large+v", |
---|
648 | incomplete=True) |
---|
649 | d.addCallback(lambda ign: |
---|
650 | self.web_json(self.small, t="check", verify="true")) |
---|
651 | d.addCallback(self.json_check_lit, self.small, "small+v") |
---|
652 | d.addCallback(lambda ign: |
---|
653 | self.web_json(self.small2, t="check", verify="true")) |
---|
654 | d.addCallback(self.json_check_lit, self.small2, "small2+v") |
---|
655 | d.addCallback(lambda ign: self.web_json(self.empty_lit_dir, t="check", verify="true")) |
---|
656 | d.addCallback(self.json_check_lit, self.empty_lit_dir, "empty_lit_dir+v") |
---|
657 | d.addCallback(lambda ign: self.web_json(self.tiny_lit_dir, t="check", verify="true")) |
---|
658 | d.addCallback(self.json_check_lit, self.tiny_lit_dir, "tiny_lit_dir+v") |
---|
659 | |
---|
660 | # check and repair, no verify |
---|
661 | d.addCallback(lambda ign: |
---|
662 | self.web_json(self.root, t="check", repair="true")) |
---|
663 | d.addCallback(self.json_check_and_repair_is_healthy, self.root, "root+r") |
---|
664 | d.addCallback(lambda ign: |
---|
665 | self.web_json(self.mutable, t="check", repair="true")) |
---|
666 | d.addCallback(self.json_check_and_repair_is_healthy, self.mutable, "mutable+r") |
---|
667 | d.addCallback(lambda ign: |
---|
668 | self.web_json(self.large, t="check", repair="true")) |
---|
669 | d.addCallback(self.json_check_and_repair_is_healthy, self.large, "large+r") |
---|
670 | d.addCallback(lambda ign: |
---|
671 | self.web_json(self.small, t="check", repair="true")) |
---|
672 | d.addCallback(self.json_check_lit, self.small, "small+r") |
---|
673 | d.addCallback(lambda ign: |
---|
674 | self.web_json(self.small2, t="check", repair="true")) |
---|
675 | d.addCallback(self.json_check_lit, self.small2, "small2+r") |
---|
676 | d.addCallback(lambda ign: self.web_json(self.empty_lit_dir, t="check", repair="true")) |
---|
677 | d.addCallback(self.json_check_lit, self.empty_lit_dir, "empty_lit_dir+r") |
---|
678 | d.addCallback(lambda ign: self.web_json(self.tiny_lit_dir, t="check", repair="true")) |
---|
679 | d.addCallback(self.json_check_lit, self.tiny_lit_dir, "tiny_lit_dir+r") |
---|
680 | |
---|
681 | # check+verify+repair |
---|
682 | d.addCallback(lambda ign: |
---|
683 | self.web_json(self.root, t="check", repair="true", verify="true")) |
---|
684 | d.addCallback(self.json_check_and_repair_is_healthy, self.root, "root+vr") |
---|
685 | d.addCallback(lambda ign: |
---|
686 | self.web_json(self.mutable, t="check", repair="true", verify="true")) |
---|
687 | d.addCallback(self.json_check_and_repair_is_healthy, self.mutable, "mutable+vr") |
---|
688 | d.addCallback(lambda ign: |
---|
689 | self.web_json(self.large, t="check", repair="true", verify="true")) |
---|
690 | d.addCallback(self.json_check_and_repair_is_healthy, self.large, "large+vr", incomplete=True) |
---|
691 | d.addCallback(lambda ign: |
---|
692 | self.web_json(self.small, t="check", repair="true", verify="true")) |
---|
693 | d.addCallback(self.json_check_lit, self.small, "small+vr") |
---|
694 | d.addCallback(lambda ign: |
---|
695 | self.web_json(self.small2, t="check", repair="true", verify="true")) |
---|
696 | d.addCallback(self.json_check_lit, self.small2, "small2+vr") |
---|
697 | d.addCallback(lambda ign: self.web_json(self.empty_lit_dir, t="check", repair="true", verify=True)) |
---|
698 | d.addCallback(self.json_check_lit, self.empty_lit_dir, "empty_lit_dir+vr") |
---|
699 | d.addCallback(lambda ign: self.web_json(self.tiny_lit_dir, t="check", repair="true", verify=True)) |
---|
700 | d.addCallback(self.json_check_lit, self.tiny_lit_dir, "tiny_lit_dir+vr") |
---|
701 | |
---|
702 | # now run a deep-check, with various verify= and repair= flags |
---|
703 | d.addCallback(lambda ign: |
---|
704 | self.slow_web(self.root, t="start-deep-check", output="json")) |
---|
705 | d.addCallback(self.json_full_deepcheck_is_healthy, self.root, "root+d") |
---|
706 | d.addCallback(lambda ign: |
---|
707 | self.slow_web(self.root, t="start-deep-check", verify="true", |
---|
708 | output="json")) |
---|
709 | d.addCallback(self.json_full_deepcheck_is_healthy, self.root, "root+dv") |
---|
710 | d.addCallback(lambda ign: |
---|
711 | self.slow_web(self.root, t="start-deep-check", repair="true", |
---|
712 | output="json")) |
---|
713 | d.addCallback(self.json_full_deepcheck_and_repair_is_healthy, self.root, "root+dr") |
---|
714 | d.addCallback(lambda ign: |
---|
715 | self.slow_web(self.root, t="start-deep-check", verify="true", repair="true", output="json")) |
---|
716 | d.addCallback(self.json_full_deepcheck_and_repair_is_healthy, self.root, "root+dvr") |
---|
717 | |
---|
718 | # now look at t=info |
---|
719 | d.addCallback(lambda ign: self.web(self.root, t="info")) |
---|
720 | # TODO: examine the output |
---|
721 | d.addCallback(lambda ign: self.web(self.mutable, t="info")) |
---|
722 | d.addCallback(lambda ign: self.web(self.large, t="info")) |
---|
723 | d.addCallback(lambda ign: self.web(self.small, t="info")) |
---|
724 | d.addCallback(lambda ign: self.web(self.small2, t="info")) |
---|
725 | d.addCallback(lambda ign: self.web(self.empty_lit_dir, t="info")) |
---|
726 | d.addCallback(lambda ign: self.web(self.tiny_lit_dir, t="info")) |
---|
727 | |
---|
728 | return d |
---|
729 | |
---|
730 | def do_test_cli_good(self, ignored): |
---|
731 | d = defer.succeed(None) |
---|
732 | d.addCallback(lambda ign: self.do_cli_manifest_stream1()) |
---|
733 | d.addCallback(lambda ign: self.do_cli_manifest_stream2()) |
---|
734 | d.addCallback(lambda ign: self.do_cli_manifest_stream3()) |
---|
735 | d.addCallback(lambda ign: self.do_cli_manifest_stream4()) |
---|
736 | d.addCallback(lambda ign: self.do_cli_manifest_stream5()) |
---|
737 | d.addCallback(lambda ign: self.do_cli_stats1()) |
---|
738 | d.addCallback(lambda ign: self.do_cli_stats2()) |
---|
739 | return d |
---|
740 | |
---|
741 | def _check_manifest_storage_index(self, out): |
---|
742 | lines = [l.encode("utf-8") for l in out.split("\n") if l] |
---|
743 | self.failUnlessEqual(len(lines), 3) |
---|
744 | self.failUnless(base32.b2a(self.root.get_storage_index()) in lines) |
---|
745 | self.failUnless(base32.b2a(self.mutable.get_storage_index()) in lines) |
---|
746 | self.failUnless(base32.b2a(self.large.get_storage_index()) in lines) |
---|
747 | |
---|
748 | def do_cli_manifest_stream1(self): |
---|
749 | d = self.do_cli("manifest", self.root_uri) |
---|
750 | def _check(args): |
---|
751 | (rc, out, err) = args |
---|
752 | self.failUnlessEqual(err, "") |
---|
753 | lines = [l for l in out.split("\n") if l] |
---|
754 | self.failUnlessEqual(len(lines), 8) |
---|
755 | caps = {} |
---|
756 | for l in lines: |
---|
757 | try: |
---|
758 | cap, path = l.split(None, 1) |
---|
759 | except ValueError: |
---|
760 | cap = l.strip() |
---|
761 | path = "" |
---|
762 | caps[cap.encode("ascii")] = path |
---|
763 | self.failUnless(self.root.get_uri() in caps) |
---|
764 | self.failUnlessEqual(caps[self.root.get_uri()], "") |
---|
765 | self.failUnlessEqual(caps[self.mutable.get_uri()], "mutable") |
---|
766 | self.failUnlessEqual(caps[self.large.get_uri()], "large") |
---|
767 | self.failUnlessEqual(caps[self.small.get_uri()], "small") |
---|
768 | self.failUnlessEqual(caps[self.small2.get_uri()], "small2") |
---|
769 | self.failUnlessEqual(caps[self.empty_lit_dir.get_uri()], "empty_lit_dir") |
---|
770 | self.failUnlessEqual(caps[self.tiny_lit_dir.get_uri()], "tiny_lit_dir") |
---|
771 | d.addCallback(_check) |
---|
772 | return d |
---|
773 | |
---|
774 | def do_cli_manifest_stream2(self): |
---|
775 | d = self.do_cli("manifest", "--raw", self.root_uri) |
---|
776 | def _check(args): |
---|
777 | (rc, out, err) = args |
---|
778 | self.failUnlessEqual(err, "") |
---|
779 | # this should be the same as the POST t=stream-manifest output |
---|
780 | self._check_streamed_manifest(out) |
---|
781 | d.addCallback(_check) |
---|
782 | return d |
---|
783 | |
---|
784 | def do_cli_manifest_stream3(self): |
---|
785 | d = self.do_cli("manifest", "--storage-index", self.root_uri) |
---|
786 | def _check(args): |
---|
787 | (rc, out, err) = args |
---|
788 | self.failUnlessEqual(err, "") |
---|
789 | self._check_manifest_storage_index(out) |
---|
790 | d.addCallback(_check) |
---|
791 | return d |
---|
792 | |
---|
793 | def do_cli_manifest_stream4(self): |
---|
794 | d = self.do_cli("manifest", "--verify-cap", self.root_uri) |
---|
795 | def _check(args): |
---|
796 | (rc, out, err) = args |
---|
797 | self.failUnlessEqual(err, "") |
---|
798 | lines = [l.encode("utf-8") for l in out.split("\n") if l] |
---|
799 | self.failUnlessEqual(len(lines), 3) |
---|
800 | self.failUnless(self.root.get_verify_cap().to_string() in lines) |
---|
801 | self.failUnless(self.mutable.get_verify_cap().to_string() in lines) |
---|
802 | self.failUnless(self.large.get_verify_cap().to_string() in lines) |
---|
803 | d.addCallback(_check) |
---|
804 | return d |
---|
805 | |
---|
806 | def do_cli_manifest_stream5(self): |
---|
807 | d = self.do_cli("manifest", "--repair-cap", self.root_uri) |
---|
808 | def _check(args): |
---|
809 | (rc, out, err) = args |
---|
810 | self.failUnlessEqual(err, "") |
---|
811 | lines = [l.encode("utf-8") for l in out.split("\n") if l] |
---|
812 | self.failUnlessEqual(len(lines), 3) |
---|
813 | self.failUnless(self.root.get_repair_cap().to_string() in lines) |
---|
814 | self.failUnless(self.mutable.get_repair_cap().to_string() in lines) |
---|
815 | self.failUnless(self.large.get_repair_cap().to_string() in lines) |
---|
816 | d.addCallback(_check) |
---|
817 | return d |
---|
818 | |
---|
819 | def do_cli_stats1(self): |
---|
820 | d = self.do_cli("stats", self.root_uri) |
---|
821 | def _check3(args): |
---|
822 | (rc, out, err) = args |
---|
823 | lines = [l.strip() for l in out.split("\n") if l] |
---|
824 | self.failUnless("count-immutable-files: 1" in lines) |
---|
825 | self.failUnless("count-mutable-files: 1" in lines) |
---|
826 | self.failUnless("count-literal-files: 3" in lines) |
---|
827 | self.failUnless("count-files: 5" in lines) |
---|
828 | self.failUnless("count-directories: 3" in lines) |
---|
829 | self.failUnless("size-immutable-files: 13000 (13.00 kB, 12.70 kiB)" in lines, lines) |
---|
830 | self.failUnless("size-literal-files: 56" in lines, lines) |
---|
831 | self.failUnless(" 4-10 : 1 (10 B, 10 B)".strip() in lines, lines) |
---|
832 | self.failUnless(" 11-31 : 2 (31 B, 31 B)".strip() in lines, lines) |
---|
833 | self.failUnless("10001-31622 : 1 (31.62 kB, 30.88 kiB)".strip() in lines, lines) |
---|
834 | d.addCallback(_check3) |
---|
835 | return d |
---|
836 | |
---|
837 | def do_cli_stats2(self): |
---|
838 | d = self.do_cli("stats", "--raw", self.root_uri) |
---|
839 | def _check4(args): |
---|
840 | (rc, out, err) = args |
---|
841 | data = json.loads(out) |
---|
842 | self.failUnlessEqual(data["count-immutable-files"], 1) |
---|
843 | self.failUnlessEqual(data["count-immutable-files"], 1) |
---|
844 | self.failUnlessEqual(data["count-mutable-files"], 1) |
---|
845 | self.failUnlessEqual(data["count-literal-files"], 3) |
---|
846 | self.failUnlessEqual(data["count-files"], 5) |
---|
847 | self.failUnlessEqual(data["count-directories"], 3) |
---|
848 | self.failUnlessEqual(data["size-immutable-files"], 13000) |
---|
849 | self.failUnlessEqual(data["size-literal-files"], 56) |
---|
850 | self.failUnless([4,10,1] in data["size-files-histogram"]) |
---|
851 | self.failUnless([11,31,2] in data["size-files-histogram"]) |
---|
852 | self.failUnless([10001,31622,1] in data["size-files-histogram"]) |
---|
853 | d.addCallback(_check4) |
---|
854 | return d |
---|
855 | |
---|
856 | |
---|
857 | class DeepCheckWebBad(DeepCheckBase, unittest.TestCase): |
---|
858 | def test_bad(self): |
---|
859 | self.basedir = "deepcheck/DeepCheckWebBad/bad" |
---|
860 | self.set_up_grid() |
---|
861 | d = self.set_up_damaged_tree() |
---|
862 | d.addCallback(self.do_check) |
---|
863 | d.addCallback(self.do_deepcheck) |
---|
864 | d.addCallback(self.do_deepcheck_broken) |
---|
865 | d.addCallback(self.do_test_web_bad) |
---|
866 | d.addErrback(self.explain_web_error) |
---|
867 | d.addErrback(self.explain_error) |
---|
868 | return d |
---|
869 | |
---|
870 | |
---|
871 | |
---|
872 | def set_up_damaged_tree(self): |
---|
873 | # 6.4s |
---|
874 | |
---|
875 | # root |
---|
876 | # mutable-good |
---|
877 | # mutable-missing-shares |
---|
878 | # mutable-corrupt-shares |
---|
879 | # mutable-unrecoverable |
---|
880 | # large-good |
---|
881 | # large-missing-shares |
---|
882 | # large-corrupt-shares |
---|
883 | # large-unrecoverable |
---|
884 | # broken |
---|
885 | # large1-good |
---|
886 | # subdir-good |
---|
887 | # large2-good |
---|
888 | # subdir-unrecoverable |
---|
889 | # large3-good |
---|
890 | |
---|
891 | self.nodes = {} |
---|
892 | |
---|
893 | c0 = self.g.clients[0] |
---|
894 | d = c0.create_dirnode() |
---|
895 | def _created_root(n): |
---|
896 | self.root = n |
---|
897 | self.root_uri = n.get_uri() |
---|
898 | d.addCallback(_created_root) |
---|
899 | d.addCallback(self.create_mangled, "mutable-good") |
---|
900 | d.addCallback(self.create_mangled, "mutable-missing-shares") |
---|
901 | d.addCallback(self.create_mangled, "mutable-corrupt-shares") |
---|
902 | d.addCallback(self.create_mangled, "mutable-unrecoverable") |
---|
903 | d.addCallback(self.create_mangled, "large-good") |
---|
904 | d.addCallback(self.create_mangled, "large-missing-shares") |
---|
905 | d.addCallback(self.create_mangled, "large-corrupt-shares") |
---|
906 | d.addCallback(self.create_mangled, "large-unrecoverable") |
---|
907 | d.addCallback(lambda ignored: c0.create_dirnode()) |
---|
908 | d.addCallback(self._stash_node, "broken") |
---|
909 | large1 = upload.Data(b"Lots of data\n" * 1000 + b"large1" + b"\n", None) |
---|
910 | d.addCallback(lambda ignored: |
---|
911 | self.nodes["broken"].add_file(u"large1", large1)) |
---|
912 | d.addCallback(lambda ignored: |
---|
913 | self.nodes["broken"].create_subdirectory(u"subdir-good")) |
---|
914 | large2 = upload.Data(b"Lots of data\n" * 1000 + b"large2" + b"\n", None) |
---|
915 | d.addCallback(lambda subdir: subdir.add_file(u"large2-good", large2)) |
---|
916 | d.addCallback(lambda ignored: |
---|
917 | self.nodes["broken"].create_subdirectory(u"subdir-unrecoverable")) |
---|
918 | d.addCallback(self._stash_node, "subdir-unrecoverable") |
---|
919 | large3 = upload.Data(b"Lots of data\n" * 1000 + b"large3" + b"\n", None) |
---|
920 | d.addCallback(lambda subdir: subdir.add_file(u"large3-good", large3)) |
---|
921 | d.addCallback(lambda ignored: |
---|
922 | self._delete_most_shares(self.nodes["broken"])) |
---|
923 | return d |
---|
924 | |
---|
925 | def _stash_node(self, node, name): |
---|
926 | self.nodes[name] = node |
---|
927 | return node |
---|
928 | |
---|
929 | def create_mangled(self, ignored, name): |
---|
930 | nodetype, mangletype = name.split("-", 1) |
---|
931 | if nodetype == "mutable": |
---|
932 | mutable_uploadable = MutableData(b"mutable file contents") |
---|
933 | d = self.g.clients[0].create_mutable_file(mutable_uploadable) |
---|
934 | d.addCallback(lambda n: self.root.set_node(str(name), n)) # TODO drop str() once strings are unicode |
---|
935 | elif nodetype == "large": |
---|
936 | large = upload.Data(b"Lots of data\n" * 1000 + name.encode("ascii") + b"\n", None) |
---|
937 | d = self.root.add_file(str(name), large) |
---|
938 | elif nodetype == "small": |
---|
939 | small = upload.Data(b"Small enough for a LIT", None) |
---|
940 | d = self.root.add_file(str(name), small) |
---|
941 | |
---|
942 | d.addCallback(self._stash_node, name) |
---|
943 | |
---|
944 | if mangletype == "good": |
---|
945 | pass |
---|
946 | elif mangletype == "missing-shares": |
---|
947 | d.addCallback(self._delete_some_shares) |
---|
948 | elif mangletype == "corrupt-shares": |
---|
949 | d.addCallback(self._corrupt_some_shares) |
---|
950 | else: |
---|
951 | assert mangletype == "unrecoverable" |
---|
952 | d.addCallback(self._delete_most_shares) |
---|
953 | |
---|
954 | return d |
---|
955 | |
---|
956 | def _delete_some_shares(self, node): |
---|
957 | self.delete_shares_numbered(node.get_uri(), [0,1]) |
---|
958 | |
---|
959 | @defer.inlineCallbacks |
---|
960 | def _corrupt_some_shares(self, node): |
---|
961 | for (shnum, serverid, sharefile) in self.find_uri_shares(node.get_uri()): |
---|
962 | if shnum in (0,1): |
---|
963 | yield run_cli("debug", "corrupt-share", sharefile) |
---|
964 | |
---|
965 | def _delete_most_shares(self, node): |
---|
966 | self.delete_shares_numbered(node.get_uri(), list(range(1,10))) |
---|
967 | |
---|
968 | |
---|
969 | def check_is_healthy(self, cr, where): |
---|
970 | try: |
---|
971 | self.failUnless(ICheckResults.providedBy(cr), (cr, type(cr), where)) |
---|
972 | self.failUnless(cr.is_healthy(), (cr.get_report(), cr.is_healthy(), cr.get_summary(), where)) |
---|
973 | self.failUnless(cr.is_recoverable(), where) |
---|
974 | self.failUnlessEqual(cr.get_version_counter_recoverable(), 1, where) |
---|
975 | self.failUnlessEqual(cr.get_version_counter_unrecoverable(), 0, where) |
---|
976 | return cr |
---|
977 | except Exception as le: |
---|
978 | le.args = tuple(le.args + (where,)) |
---|
979 | raise |
---|
980 | |
---|
981 | def check_is_missing_shares(self, cr, where): |
---|
982 | self.failUnless(ICheckResults.providedBy(cr), where) |
---|
983 | self.failIf(cr.is_healthy(), where) |
---|
984 | self.failUnless(cr.is_recoverable(), where) |
---|
985 | self.failUnlessEqual(cr.get_version_counter_recoverable(), 1, where) |
---|
986 | self.failUnlessEqual(cr.get_version_counter_unrecoverable(), 0, where) |
---|
987 | return cr |
---|
988 | |
---|
989 | def check_has_corrupt_shares(self, cr, where): |
---|
990 | # by "corrupt-shares" we mean the file is still recoverable |
---|
991 | self.failUnless(ICheckResults.providedBy(cr), where) |
---|
992 | self.failIf(cr.is_healthy(), (where, cr)) |
---|
993 | self.failUnless(cr.is_recoverable(), where) |
---|
994 | self.failUnless(cr.get_share_counter_good() < 10, where) |
---|
995 | self.failUnless(cr.get_corrupt_shares(), where) |
---|
996 | return cr |
---|
997 | |
---|
998 | def check_is_unrecoverable(self, cr, where): |
---|
999 | self.failUnless(ICheckResults.providedBy(cr), where) |
---|
1000 | self.failIf(cr.is_healthy(), where) |
---|
1001 | self.failIf(cr.is_recoverable(), where) |
---|
1002 | self.failUnless(cr.get_share_counter_good() < cr.get_encoding_needed(), |
---|
1003 | (cr.get_share_counter_good(), cr.get_encoding_needed(), |
---|
1004 | where)) |
---|
1005 | self.failUnlessEqual(cr.get_version_counter_recoverable(), 0, where) |
---|
1006 | self.failUnlessEqual(cr.get_version_counter_unrecoverable(), 1, where) |
---|
1007 | return cr |
---|
1008 | |
---|
1009 | def do_check(self, ignored): |
---|
1010 | d = defer.succeed(None) |
---|
1011 | |
---|
1012 | # check the individual items, without verification. This will not |
---|
1013 | # detect corrupt shares. |
---|
1014 | def _check(which, checker): |
---|
1015 | d = self.nodes[which].check(Monitor()) |
---|
1016 | d.addCallback(checker, which + "--check") |
---|
1017 | return d |
---|
1018 | |
---|
1019 | d.addCallback(lambda ign: _check("mutable-good", self.check_is_healthy)) |
---|
1020 | d.addCallback(lambda ign: _check("mutable-missing-shares", |
---|
1021 | self.check_is_missing_shares)) |
---|
1022 | d.addCallback(lambda ign: _check("mutable-corrupt-shares", |
---|
1023 | self.check_is_healthy)) |
---|
1024 | d.addCallback(lambda ign: _check("mutable-unrecoverable", |
---|
1025 | self.check_is_unrecoverable)) |
---|
1026 | d.addCallback(lambda ign: _check("large-good", self.check_is_healthy)) |
---|
1027 | d.addCallback(lambda ign: _check("large-missing-shares", |
---|
1028 | self.check_is_missing_shares)) |
---|
1029 | d.addCallback(lambda ign: _check("large-corrupt-shares", |
---|
1030 | self.check_is_healthy)) |
---|
1031 | d.addCallback(lambda ign: _check("large-unrecoverable", |
---|
1032 | self.check_is_unrecoverable)) |
---|
1033 | |
---|
1034 | # and again with verify=True, which *does* detect corrupt shares. |
---|
1035 | def _checkv(which, checker): |
---|
1036 | d = self.nodes[which].check(Monitor(), verify=True) |
---|
1037 | d.addCallback(checker, which + "--check-and-verify") |
---|
1038 | return d |
---|
1039 | |
---|
1040 | d.addCallback(lambda ign: _checkv("mutable-good", self.check_is_healthy)) |
---|
1041 | d.addCallback(lambda ign: _checkv("mutable-missing-shares", |
---|
1042 | self.check_is_missing_shares)) |
---|
1043 | d.addCallback(lambda ign: _checkv("mutable-corrupt-shares", |
---|
1044 | self.check_has_corrupt_shares)) |
---|
1045 | d.addCallback(lambda ign: _checkv("mutable-unrecoverable", |
---|
1046 | self.check_is_unrecoverable)) |
---|
1047 | d.addCallback(lambda ign: _checkv("large-good", self.check_is_healthy)) |
---|
1048 | d.addCallback(lambda ign: _checkv("large-missing-shares", self.check_is_missing_shares)) |
---|
1049 | d.addCallback(lambda ign: _checkv("large-corrupt-shares", self.check_has_corrupt_shares)) |
---|
1050 | d.addCallback(lambda ign: _checkv("large-unrecoverable", |
---|
1051 | self.check_is_unrecoverable)) |
---|
1052 | |
---|
1053 | return d |
---|
1054 | |
---|
1055 | def do_deepcheck(self, ignored): |
---|
1056 | d = defer.succeed(None) |
---|
1057 | |
---|
1058 | # now deep-check the root, with various verify= and repair= options |
---|
1059 | d.addCallback(lambda ign: |
---|
1060 | self.root.start_deep_check().when_done()) |
---|
1061 | def _check1(cr): |
---|
1062 | self.failUnless(IDeepCheckResults.providedBy(cr)) |
---|
1063 | c = cr.get_counters() |
---|
1064 | self.failUnlessEqual(c["count-objects-checked"], 9) |
---|
1065 | self.failUnlessEqual(c["count-objects-healthy"], 5) |
---|
1066 | self.failUnlessEqual(c["count-objects-unhealthy"], 4) |
---|
1067 | self.failUnlessEqual(c["count-objects-unrecoverable"], 2) |
---|
1068 | d.addCallback(_check1) |
---|
1069 | |
---|
1070 | d.addCallback(lambda ign: |
---|
1071 | self.root.start_deep_check(verify=True).when_done()) |
---|
1072 | def _check2(cr): |
---|
1073 | self.failUnless(IDeepCheckResults.providedBy(cr)) |
---|
1074 | c = cr.get_counters() |
---|
1075 | self.failUnlessEqual(c["count-objects-checked"], 9) |
---|
1076 | self.failUnlessEqual(c["count-objects-healthy"], 3) |
---|
1077 | self.failUnlessEqual(c["count-objects-unhealthy"], 6) |
---|
1078 | self.failUnlessEqual(c["count-objects-healthy"], 3) # root, mutable good, large good |
---|
1079 | self.failUnlessEqual(c["count-objects-unrecoverable"], 2) # mutable unrecoverable, large unrecoverable |
---|
1080 | d.addCallback(_check2) |
---|
1081 | |
---|
1082 | return d |
---|
1083 | |
---|
1084 | def do_deepcheck_broken(self, ignored): |
---|
1085 | # deep-check on the broken directory should fail, because of the |
---|
1086 | # untraversable subdir |
---|
1087 | def _do_deep_check(): |
---|
1088 | return self.nodes["broken"].start_deep_check().when_done() |
---|
1089 | d = self.shouldFail(UnrecoverableFileError, "do_deep_check", |
---|
1090 | "no recoverable versions", |
---|
1091 | _do_deep_check) |
---|
1092 | return d |
---|
1093 | |
---|
1094 | def json_is_healthy(self, data, where): |
---|
1095 | r = data["results"] |
---|
1096 | self.failUnless(r["healthy"], where) |
---|
1097 | self.failUnless(r["recoverable"], where) |
---|
1098 | self.failUnlessEqual(r["count-recoverable-versions"], 1, where) |
---|
1099 | self.failUnlessEqual(r["count-unrecoverable-versions"], 0, where) |
---|
1100 | |
---|
1101 | def json_is_missing_shares(self, data, where): |
---|
1102 | r = data["results"] |
---|
1103 | self.failIf(r["healthy"], where) |
---|
1104 | self.failUnless(r["recoverable"], where) |
---|
1105 | self.failUnlessEqual(r["count-recoverable-versions"], 1, where) |
---|
1106 | self.failUnlessEqual(r["count-unrecoverable-versions"], 0, where) |
---|
1107 | |
---|
1108 | def json_has_corrupt_shares(self, data, where): |
---|
1109 | # by "corrupt-shares" we mean the file is still recoverable |
---|
1110 | r = data["results"] |
---|
1111 | self.failIf(r["healthy"], where) |
---|
1112 | self.failUnless(r["recoverable"], where) |
---|
1113 | self.failUnless(r["count-shares-good"] < 10, where) |
---|
1114 | self.failUnless(r["count-corrupt-shares"], where) |
---|
1115 | self.failUnless(r["list-corrupt-shares"], where) |
---|
1116 | |
---|
1117 | def json_is_unrecoverable(self, data, where): |
---|
1118 | r = data["results"] |
---|
1119 | self.failIf(r["healthy"], where) |
---|
1120 | self.failIf(r["recoverable"], where) |
---|
1121 | self.failUnless(r["count-shares-good"] < r["count-shares-needed"], |
---|
1122 | where) |
---|
1123 | self.failUnlessEqual(r["count-recoverable-versions"], 0, where) |
---|
1124 | self.failUnlessEqual(r["count-unrecoverable-versions"], 1, where) |
---|
1125 | |
---|
1126 | def do_test_web_bad(self, ignored): |
---|
1127 | d = defer.succeed(None) |
---|
1128 | |
---|
1129 | # check, no verify |
---|
1130 | def _check(which, checker): |
---|
1131 | d = self.web_json(self.nodes[which], t="check") |
---|
1132 | d.addCallback(checker, which + "--webcheck") |
---|
1133 | return d |
---|
1134 | |
---|
1135 | d.addCallback(lambda ign: _check("mutable-good", |
---|
1136 | self.json_is_healthy)) |
---|
1137 | d.addCallback(lambda ign: _check("mutable-missing-shares", |
---|
1138 | self.json_is_missing_shares)) |
---|
1139 | d.addCallback(lambda ign: _check("mutable-corrupt-shares", |
---|
1140 | self.json_is_healthy)) |
---|
1141 | d.addCallback(lambda ign: _check("mutable-unrecoverable", |
---|
1142 | self.json_is_unrecoverable)) |
---|
1143 | d.addCallback(lambda ign: _check("large-good", |
---|
1144 | self.json_is_healthy)) |
---|
1145 | d.addCallback(lambda ign: _check("large-missing-shares", |
---|
1146 | self.json_is_missing_shares)) |
---|
1147 | d.addCallback(lambda ign: _check("large-corrupt-shares", |
---|
1148 | self.json_is_healthy)) |
---|
1149 | d.addCallback(lambda ign: _check("large-unrecoverable", |
---|
1150 | self.json_is_unrecoverable)) |
---|
1151 | |
---|
1152 | # check and verify |
---|
1153 | def _checkv(which, checker): |
---|
1154 | d = self.web_json(self.nodes[which], t="check", verify="true") |
---|
1155 | d.addCallback(checker, which + "--webcheck-and-verify") |
---|
1156 | return d |
---|
1157 | |
---|
1158 | d.addCallback(lambda ign: _checkv("mutable-good", |
---|
1159 | self.json_is_healthy)) |
---|
1160 | d.addCallback(lambda ign: _checkv("mutable-missing-shares", |
---|
1161 | self.json_is_missing_shares)) |
---|
1162 | d.addCallback(lambda ign: _checkv("mutable-corrupt-shares", |
---|
1163 | self.json_has_corrupt_shares)) |
---|
1164 | d.addCallback(lambda ign: _checkv("mutable-unrecoverable", |
---|
1165 | self.json_is_unrecoverable)) |
---|
1166 | d.addCallback(lambda ign: _checkv("large-good", |
---|
1167 | self.json_is_healthy)) |
---|
1168 | d.addCallback(lambda ign: _checkv("large-missing-shares", self.json_is_missing_shares)) |
---|
1169 | d.addCallback(lambda ign: _checkv("large-corrupt-shares", self.json_has_corrupt_shares)) |
---|
1170 | d.addCallback(lambda ign: _checkv("large-unrecoverable", |
---|
1171 | self.json_is_unrecoverable)) |
---|
1172 | |
---|
1173 | return d |
---|
1174 | |
---|
1175 | class Large(DeepCheckBase, unittest.TestCase): |
---|
1176 | def test_lots_of_lits(self): |
---|
1177 | self.basedir = "deepcheck/Large/lots_of_lits" |
---|
1178 | self.set_up_grid() |
---|
1179 | # create the following directory structure: |
---|
1180 | # root/ |
---|
1181 | # subdir/ |
---|
1182 | # 000-large (CHK) |
---|
1183 | # 001-small (LIT) |
---|
1184 | # 002-small |
---|
1185 | # ... |
---|
1186 | # 399-small |
---|
1187 | # then do a deepcheck and make sure it doesn't cause a |
---|
1188 | # Deferred-tail-recursion stack overflow |
---|
1189 | |
---|
1190 | COUNT = 400 |
---|
1191 | c0 = self.g.clients[0] |
---|
1192 | d = c0.create_dirnode() |
---|
1193 | self.stash = {} |
---|
1194 | def _created_root(n): |
---|
1195 | self.root = n |
---|
1196 | return n |
---|
1197 | d.addCallback(_created_root) |
---|
1198 | d.addCallback(lambda root: root.create_subdirectory(u"subdir")) |
---|
1199 | def _add_children(subdir_node): |
---|
1200 | self.subdir_node = subdir_node |
---|
1201 | kids = {} |
---|
1202 | for i in range(1, COUNT): |
---|
1203 | litcap = LiteralFileURI(b"%03d-data" % i).to_string() |
---|
1204 | kids[u"%03d-small" % i] = (litcap, litcap) |
---|
1205 | return subdir_node.set_children(kids) |
---|
1206 | d.addCallback(_add_children) |
---|
1207 | up = upload.Data(b"large enough for CHK" * 100, b"") |
---|
1208 | d.addCallback(lambda ign: self.subdir_node.add_file(u"0000-large", up)) |
---|
1209 | |
---|
1210 | def _start_deepcheck(ignored): |
---|
1211 | return self.web(self.root, method="POST", t="stream-deep-check") |
---|
1212 | d.addCallback(_start_deepcheck) |
---|
1213 | def _check(output_and_url): |
---|
1214 | (output, url) = output_and_url |
---|
1215 | units = list(self.parse_streamed_json(output)) |
---|
1216 | self.failUnlessEqual(len(units), 2+COUNT+1) |
---|
1217 | d.addCallback(_check) |
---|
1218 | |
---|
1219 | return d |
---|