]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/test/test_dirnode.py
test_dirnode: improve coverage of not-mutable-error a bit
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_dirnode.py
1
2 import time
3 from zope.interface import implements
4 from twisted.trial import unittest
5 from allmydata import uri, dirnode, upload
6 from allmydata.interfaces import IURI, IClient, IMutableFileNode, \
7      INewDirectoryURI, IReadonlyNewDirectoryURI, IFileNode
8 from allmydata.util import hashutil, testutil
9 from allmydata.test.common import make_chk_file_uri, make_mutable_file_uri, \
10      FakeDirectoryNode, create_chk_filenode
11
12 # to test dirnode.py, we want to construct a tree of real DirectoryNodes that
13 # contain pointers to fake files. We start with a fake MutableFileNode that
14 # stores all of its data in a static table.
15
16 class Marker:
17     implements(IFileNode, IMutableFileNode) # sure, why not
18     def __init__(self, nodeuri):
19         if not isinstance(nodeuri, str):
20             nodeuri = nodeuri.to_string()
21         self.nodeuri = nodeuri
22         si = hashutil.tagged_hash("tag1", nodeuri)[:16]
23         fp = hashutil.tagged_hash("tag2", nodeuri)
24         self.verifieruri = uri.SSKVerifierURI(storage_index=si,
25                                               fingerprint=fp).to_string()
26     def get_uri(self):
27         return self.nodeuri
28     def get_readonly_uri(self):
29         return self.nodeuri
30     def get_verifier(self):
31         return self.verifieruri
32
33 # dirnode requires three methods from the client: upload(),
34 # create_node_from_uri(), and create_empty_dirnode(). Of these, upload() is
35 # only used by the convenience composite method add_file().
36
37 class FakeClient:
38     implements(IClient)
39
40     def upload(self, uploadable):
41         d = uploadable.get_size()
42         d.addCallback(lambda size: uploadable.read(size))
43         def _got_data(datav):
44             data = "".join(datav)
45             n = create_chk_filenode(self, data)
46             results = upload.UploadResults()
47             results.uri = n.get_uri()
48             return results
49         d.addCallback(_got_data)
50         return d
51
52     def create_node_from_uri(self, u):
53         u = IURI(u)
54         if (INewDirectoryURI.providedBy(u)
55             or IReadonlyNewDirectoryURI.providedBy(u)):
56             return FakeDirectoryNode(self).init_from_uri(u)
57         return Marker(u.to_string())
58
59     def create_empty_dirnode(self):
60         n = FakeDirectoryNode(self)
61         d = n.create()
62         d.addCallback(lambda res: n)
63         return d
64
65
66 class Dirnode(unittest.TestCase, testutil.ShouldFailMixin, testutil.StallMixin):
67     def setUp(self):
68         self.client = FakeClient()
69
70     def test_basic(self):
71         d = self.client.create_empty_dirnode()
72         def _done(res):
73             self.failUnless(isinstance(res, FakeDirectoryNode))
74             rep = str(res)
75             self.failUnless("RW" in rep)
76         d.addCallback(_done)
77         return d
78
79     def test_corrupt(self):
80         d = self.client.create_empty_dirnode()
81         def _created(dn):
82             u = make_mutable_file_uri()
83             d = dn.set_uri(u"child", u, {})
84             d.addCallback(lambda res: dn.list())
85             def _check1(children):
86                 self.failUnless(u"child" in children)
87             d.addCallback(_check1)
88             d.addCallback(lambda res:
89                           self.shouldFail(KeyError, "get bogus", None,
90                                           dn.get, u"bogus"))
91             def _corrupt(res):
92                 filenode = dn._node
93                 si = IURI(filenode.get_uri()).storage_index
94                 old_contents = filenode.all_contents[si]
95                 # we happen to know that the writecap is encrypted near the
96                 # end of the string. Flip one of its bits and make sure we
97                 # detect the corruption.
98                 new_contents = testutil.flip_bit(old_contents, -10)
99                 # TODO: also test flipping bits in the other portions
100                 filenode.all_contents[si] = new_contents
101             d.addCallback(_corrupt)
102             def _check2(res):
103                 self.shouldFail(hashutil.IntegrityCheckError, "corrupt",
104                                 "HMAC does not match, crypttext is corrupted",
105                                 dn.list)
106             d.addCallback(_check2)
107             return d
108         d.addCallback(_created)
109         return d
110
111     def test_check(self):
112         d = self.client.create_empty_dirnode()
113         d.addCallback(lambda dn: dn.check())
114         def _done(res):
115             pass
116         d.addCallback(_done)
117         return d
118
119     def test_readonly(self):
120         fileuri = make_chk_file_uri(1234)
121         filenode = self.client.create_node_from_uri(fileuri)
122         uploadable = upload.Data("some data", convergence="some convergence string")
123
124         d = self.client.create_empty_dirnode()
125         def _created(rw_dn):
126             d2 = rw_dn.set_uri(u"child", fileuri)
127             d2.addCallback(lambda res: rw_dn)
128             return d2
129         d.addCallback(_created)
130
131         def _ready(rw_dn):
132             ro_uri = rw_dn.get_readonly_uri()
133             ro_dn = self.client.create_node_from_uri(ro_uri)
134             self.failUnless(ro_dn.is_readonly())
135             self.failUnless(ro_dn.is_mutable())
136
137             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
138                             ro_dn.set_uri, u"newchild", fileuri)
139             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
140                             ro_dn.set_node, u"newchild", filenode)
141             self.shouldFail(dirnode.NotMutableError, "set_nodes ro", None,
142                             ro_dn.set_nodes, [ (u"newchild", filenode) ])
143             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
144                             ro_dn.add_file, u"newchild", uploadable)
145             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
146                             ro_dn.delete, u"child")
147             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
148                             ro_dn.create_empty_directory, u"newchild")
149             self.shouldFail(dirnode.NotMutableError, "set_metadata_for ro", None,
150                             ro_dn.set_metadata_for, u"child", {})
151             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
152                             ro_dn.move_child_to, u"child", rw_dn)
153             self.shouldFail(dirnode.NotMutableError, "set_uri ro", None,
154                             rw_dn.move_child_to, u"child", ro_dn)
155             return ro_dn.list()
156         d.addCallback(_ready)
157         def _listed(children):
158             self.failUnless(u"child" in children)
159         d.addCallback(_listed)
160         return d
161
162     def failUnlessGreaterThan(self, a, b):
163         self.failUnless(a > b, "%r should be > %r" % (a, b))
164
165     def failUnlessGreaterOrEqualThan(self, a, b):
166         self.failUnless(a >= b, "%r should be >= %r" % (a, b))
167
168     def test_create(self):
169         self.expected_manifest = []
170
171         d = self.client.create_empty_dirnode()
172         def _then(n):
173             # /
174             self.failUnless(n.is_mutable())
175             u = n.get_uri()
176             self.failUnless(u)
177             self.failUnless(u.startswith("URI:DIR2:"), u)
178             u_ro = n.get_readonly_uri()
179             self.failUnless(u_ro.startswith("URI:DIR2-RO:"), u_ro)
180             u_v = n.get_verifier()
181             self.failUnless(u_v.startswith("URI:DIR2-Verifier:"), u_v)
182             self.expected_manifest.append(u_v)
183
184             d = n.list()
185             d.addCallback(lambda res: self.failUnlessEqual(res, {}))
186             d.addCallback(lambda res: n.has_child(u"missing"))
187             d.addCallback(lambda res: self.failIf(res))
188             fake_file_uri = make_mutable_file_uri()
189             m = Marker(fake_file_uri)
190             ffu_v = m.get_verifier()
191             assert isinstance(ffu_v, str)
192             self.expected_manifest.append(ffu_v)
193             d.addCallback(lambda res: n.set_uri(u"child", fake_file_uri))
194             # /
195             # /child = mutable
196
197             d.addCallback(lambda res: n.create_empty_directory(u"subdir"))
198             # /
199             # /child = mutable
200             # /subdir = directory
201             def _created(subdir):
202                 self.failUnless(isinstance(subdir, FakeDirectoryNode))
203                 self.subdir = subdir
204                 new_v = subdir.get_verifier()
205                 assert isinstance(new_v, str)
206                 self.expected_manifest.append(new_v)
207             d.addCallback(_created)
208
209             d.addCallback(lambda res: n.list())
210             d.addCallback(lambda children:
211                           self.failUnlessEqual(sorted(children.keys()),
212                                                sorted([u"child", u"subdir"])))
213
214             d.addCallback(lambda res: n.build_manifest())
215             def _check_manifest(manifest):
216                 self.failUnlessEqual(sorted(manifest),
217                                      sorted(self.expected_manifest))
218             d.addCallback(_check_manifest)
219
220             d.addCallback(lambda res: n.deep_stats())
221             def _check_deepstats(stats):
222                 self.failUnless(isinstance(stats, dict))
223                 expected = {"count-immutable-files": 0,
224                             "count-mutable-files": 1,
225                             "count-literal-files": 0,
226                             "count-files": 1,
227                             "count-directories": 2,
228                             "size-immutable-files": 0,
229                             "size-literal-files": 0,
230                             #"size-directories": 616, # varies
231                             #"largest-directory": 616,
232                             "largest-directory-children": 2,
233                             "largest-immutable-file": 0,
234                             }
235                 for k,v in expected.iteritems():
236                     self.failUnlessEqual(stats[k], v,
237                                          "stats[%s] was %s, not %s" %
238                                          (k, stats[k], v))
239                 self.failUnless(stats["size-directories"] > 500,
240                                 stats["size-directories"])
241                 self.failUnless(stats["largest-directory"] > 500,
242                                 stats["largest-directory"])
243                 self.failUnlessEqual(stats["size-files-histogram"], [])
244             d.addCallback(_check_deepstats)
245
246             def _add_subsubdir(res):
247                 return self.subdir.create_empty_directory(u"subsubdir")
248             d.addCallback(_add_subsubdir)
249             d.addCallback(lambda res: n.get_child_at_path(u"subdir/subsubdir"))
250             d.addCallback(lambda subsubdir:
251                           self.failUnless(isinstance(subsubdir,
252                                                      FakeDirectoryNode)))
253             d.addCallback(lambda res: n.get_child_at_path(u""))
254             d.addCallback(lambda res: self.failUnlessEqual(res.get_uri(),
255                                                            n.get_uri()))
256
257             d.addCallback(lambda res: n.get_metadata_for(u"child"))
258             d.addCallback(lambda metadata:
259                           self.failUnlessEqual(sorted(metadata.keys()),
260                                                ["ctime", "mtime"]))
261
262             # set_uri + metadata
263             # it should be possible to add a child without any metadata
264             d.addCallback(lambda res: n.set_uri(u"c2", fake_file_uri, {}))
265             d.addCallback(lambda res: n.get_metadata_for(u"c2"))
266             d.addCallback(lambda metadata: self.failUnlessEqual(metadata, {}))
267
268             # if we don't set any defaults, the child should get timestamps
269             d.addCallback(lambda res: n.set_uri(u"c3", fake_file_uri))
270             d.addCallback(lambda res: n.get_metadata_for(u"c3"))
271             d.addCallback(lambda metadata:
272                           self.failUnlessEqual(sorted(metadata.keys()),
273                                                ["ctime", "mtime"]))
274
275             # or we can add specific metadata at set_uri() time, which
276             # overrides the timestamps
277             d.addCallback(lambda res: n.set_uri(u"c4", fake_file_uri,
278                                                 {"key": "value"}))
279             d.addCallback(lambda res: n.get_metadata_for(u"c4"))
280             d.addCallback(lambda metadata:
281                           self.failUnlessEqual(metadata, {"key": "value"}))
282
283             d.addCallback(lambda res: n.delete(u"c2"))
284             d.addCallback(lambda res: n.delete(u"c3"))
285             d.addCallback(lambda res: n.delete(u"c4"))
286
287             # set_node + metadata
288             # it should be possible to add a child without any metadata
289             d.addCallback(lambda res: n.set_node(u"d2", n, {}))
290             d.addCallback(lambda res: n.get_metadata_for(u"d2"))
291             d.addCallback(lambda metadata: self.failUnlessEqual(metadata, {}))
292
293             # if we don't set any defaults, the child should get timestamps
294             d.addCallback(lambda res: n.set_node(u"d3", n))
295             d.addCallback(lambda res: n.get_metadata_for(u"d3"))
296             d.addCallback(lambda metadata:
297                           self.failUnlessEqual(sorted(metadata.keys()),
298                                                ["ctime", "mtime"]))
299
300             # or we can add specific metadata at set_node() time, which
301             # overrides the timestamps
302             d.addCallback(lambda res: n.set_node(u"d4", n,
303                                                 {"key": "value"}))
304             d.addCallback(lambda res: n.get_metadata_for(u"d4"))
305             d.addCallback(lambda metadata:
306                           self.failUnlessEqual(metadata, {"key": "value"}))
307
308             d.addCallback(lambda res: n.delete(u"d2"))
309             d.addCallback(lambda res: n.delete(u"d3"))
310             d.addCallback(lambda res: n.delete(u"d4"))
311
312             # metadata through set_children()
313             d.addCallback(lambda res: n.set_children([ (u"e1", fake_file_uri),
314                                                    (u"e2", fake_file_uri, {}),
315                                                    (u"e3", fake_file_uri,
316                                                     {"key": "value"}),
317                                                    ]))
318             d.addCallback(lambda res: n.get_metadata_for(u"e1"))
319             d.addCallback(lambda metadata:
320                           self.failUnlessEqual(sorted(metadata.keys()),
321                                                ["ctime", "mtime"]))
322             d.addCallback(lambda res: n.get_metadata_for(u"e2"))
323             d.addCallback(lambda metadata: self.failUnlessEqual(metadata, {}))
324             d.addCallback(lambda res: n.get_metadata_for(u"e3"))
325             d.addCallback(lambda metadata:
326                           self.failUnlessEqual(metadata, {"key": "value"}))
327
328             d.addCallback(lambda res: n.delete(u"e1"))
329             d.addCallback(lambda res: n.delete(u"e2"))
330             d.addCallback(lambda res: n.delete(u"e3"))
331
332             # metadata through set_nodes()
333             d.addCallback(lambda res: n.set_nodes([ (u"f1", n),
334                                                     (u"f2", n, {}),
335                                                     (u"f3", n,
336                                                      {"key": "value"}),
337                                                     ]))
338             d.addCallback(lambda res: n.get_metadata_for(u"f1"))
339             d.addCallback(lambda metadata:
340                           self.failUnlessEqual(sorted(metadata.keys()),
341                                                ["ctime", "mtime"]))
342             d.addCallback(lambda res: n.get_metadata_for(u"f2"))
343             d.addCallback(lambda metadata: self.failUnlessEqual(metadata, {}))
344             d.addCallback(lambda res: n.get_metadata_for(u"f3"))
345             d.addCallback(lambda metadata:
346                           self.failUnlessEqual(metadata, {"key": "value"}))
347
348             d.addCallback(lambda res: n.delete(u"f1"))
349             d.addCallback(lambda res: n.delete(u"f2"))
350             d.addCallback(lambda res: n.delete(u"f3"))
351
352
353             d.addCallback(lambda res:
354                           n.set_metadata_for(u"child",
355                                              {"tags": ["web2.0-compatible"]}))
356             d.addCallback(lambda n1: n1.get_metadata_for(u"child"))
357             d.addCallback(lambda metadata:
358                           self.failUnlessEqual(metadata,
359                                                {"tags": ["web2.0-compatible"]}))
360
361             def _start(res):
362                 self._start_timestamp = time.time()
363             d.addCallback(_start)
364             # simplejson-1.7.1 (as shipped on Ubuntu 'gutsy') rounds all
365             # floats to hundredeths (it uses str(num) instead of repr(num)).
366             # simplejson-1.7.3 does not have this bug. To prevent this bug
367             # from causing the test to fail, stall for more than a few
368             # hundrededths of a second.
369             d.addCallback(self.stall, 0.1)
370             d.addCallback(lambda res: n.add_file(u"timestamps",
371                                                  upload.Data("stamp me", convergence="some convergence string")))
372             d.addCallback(self.stall, 0.1)
373             def _stop(res):
374                 self._stop_timestamp = time.time()
375             d.addCallback(_stop)
376
377             d.addCallback(lambda res: n.get_metadata_for(u"timestamps"))
378             def _check_timestamp1(metadata):
379                 self.failUnless("ctime" in metadata)
380                 self.failUnless("mtime" in metadata)
381                 self.failUnlessGreaterOrEqualThan(metadata["ctime"],
382                                                   self._start_timestamp)
383                 self.failUnlessGreaterOrEqualThan(self._stop_timestamp,
384                                                   metadata["ctime"])
385                 self.failUnlessGreaterOrEqualThan(metadata["mtime"],
386                                                   self._start_timestamp)
387                 self.failUnlessGreaterOrEqualThan(self._stop_timestamp,
388                                                   metadata["mtime"])
389                 # Our current timestamp rules say that replacing an existing
390                 # child should preserve the 'ctime' but update the mtime
391                 self._old_ctime = metadata["ctime"]
392                 self._old_mtime = metadata["mtime"]
393             d.addCallback(_check_timestamp1)
394             d.addCallback(self.stall, 2.0) # accomodate low-res timestamps
395             d.addCallback(lambda res: n.set_node(u"timestamps", n))
396             d.addCallback(lambda res: n.get_metadata_for(u"timestamps"))
397             def _check_timestamp2(metadata):
398                 self.failUnlessEqual(metadata["ctime"], self._old_ctime,
399                                      "%s != %s" % (metadata["ctime"],
400                                                    self._old_ctime))
401                 self.failUnlessGreaterThan(metadata["mtime"], self._old_mtime)
402                 return n.delete(u"timestamps")
403             d.addCallback(_check_timestamp2)
404
405             # also make sure we can add/update timestamps on a
406             # previously-existing child that didn't have any, since there are
407             # a lot of 0.7.0-generated edges around out there
408             d.addCallback(lambda res: n.set_node(u"no_timestamps", n, {}))
409             d.addCallback(lambda res: n.set_node(u"no_timestamps", n))
410             d.addCallback(lambda res: n.get_metadata_for(u"no_timestamps"))
411             d.addCallback(lambda metadata:
412                           self.failUnlessEqual(sorted(metadata.keys()),
413                                                ["ctime", "mtime"]))
414             d.addCallback(lambda res: n.delete(u"no_timestamps"))
415
416             d.addCallback(lambda res: n.delete(u"subdir"))
417             d.addCallback(lambda old_child:
418                           self.failUnlessEqual(old_child.get_uri(),
419                                                self.subdir.get_uri()))
420
421             d.addCallback(lambda res: n.list())
422             d.addCallback(lambda children:
423                           self.failUnlessEqual(sorted(children.keys()),
424                                                sorted([u"child"])))
425
426             uploadable = upload.Data("some data", convergence="some convergence string")
427             d.addCallback(lambda res: n.add_file(u"newfile", uploadable))
428             d.addCallback(lambda newnode:
429                           self.failUnless(IFileNode.providedBy(newnode)))
430             d.addCallback(lambda res: n.list())
431             d.addCallback(lambda children:
432                           self.failUnlessEqual(sorted(children.keys()),
433                                                sorted([u"child", u"newfile"])))
434             d.addCallback(lambda res: n.get_metadata_for(u"newfile"))
435             d.addCallback(lambda metadata:
436                           self.failUnlessEqual(sorted(metadata.keys()),
437                                                ["ctime", "mtime"]))
438
439             uploadable = upload.Data("some data", convergence="some convergence string")
440             d.addCallback(lambda res: n.add_file(u"newfile-metadata",
441                                                  uploadable,
442                                                  {"key": "value"}))
443             d.addCallback(lambda newnode:
444                           self.failUnless(IFileNode.providedBy(newnode)))
445             d.addCallback(lambda res: n.get_metadata_for(u"newfile-metadata"))
446             d.addCallback(lambda metadata:
447                           self.failUnlessEqual(metadata, {"key": "value"}))
448             d.addCallback(lambda res: n.delete(u"newfile-metadata"))
449
450             d.addCallback(lambda res: n.create_empty_directory(u"subdir2"))
451             def _created2(subdir2):
452                 self.subdir2 = subdir2
453             d.addCallback(_created2)
454
455             d.addCallback(lambda res:
456                           n.move_child_to(u"child", self.subdir2))
457             d.addCallback(lambda res: n.list())
458             d.addCallback(lambda children:
459                           self.failUnlessEqual(sorted(children.keys()),
460                                                sorted([u"newfile", u"subdir2"])))
461             d.addCallback(lambda res: self.subdir2.list())
462             d.addCallback(lambda children:
463                           self.failUnlessEqual(sorted(children.keys()),
464                                                sorted([u"child"])))
465
466             return d
467
468         d.addCallback(_then)
469
470         return d
471
472 class DeepStats(unittest.TestCase):
473     def test_stats(self):
474         ds = dirnode.DeepStats()
475         ds.add("count-files")
476         ds.add("size-immutable-files", 123)
477         ds.histogram("size-files-histogram", 123)
478         ds.max("largest-directory", 444)
479
480         s = ds.get_results()
481         self.failUnlessEqual(s["count-files"], 1)
482         self.failUnlessEqual(s["size-immutable-files"], 123)
483         self.failUnlessEqual(s["largest-directory"], 444)
484         self.failUnlessEqual(s["count-literal-files"], 0)
485
486         ds.add("count-files")
487         ds.add("size-immutable-files", 321)
488         ds.histogram("size-files-histogram", 321)
489         ds.max("largest-directory", 2)
490
491         s = ds.get_results()
492         self.failUnlessEqual(s["count-files"], 2)
493         self.failUnlessEqual(s["size-immutable-files"], 444)
494         self.failUnlessEqual(s["largest-directory"], 444)
495         self.failUnlessEqual(s["count-literal-files"], 0)
496         self.failUnlessEqual(s["size-files-histogram"],
497                              [ (101, 316, 1), (317, 1000, 1) ])
498
499         ds = dirnode.DeepStats()
500         for i in range(1, 1100):
501             ds.histogram("size-files-histogram", i)
502         ds.histogram("size-files-histogram", 4*1000*1000*1000*1000) # 4TB
503         s = ds.get_results()
504         self.failUnlessEqual(s["size-files-histogram"],
505                              [ (1, 3, 3),
506                                (4, 10, 7),
507                                (11, 31, 21),
508                                (32, 100, 69),
509                                (101, 316, 216),
510                                (317, 1000, 684),
511                                (1001, 3162, 99),
512                                (3162277660169L, 10000000000000L, 1),
513                                ])
514
515
516 netstring = hashutil.netstring
517 split_netstring = dirnode.split_netstring
518
519 class Netstring(unittest.TestCase):
520     def test_split(self):
521         a = netstring("hello") + netstring("world")
522         self.failUnlessEqual(split_netstring(a, 2), ("hello", "world"))
523         self.failUnlessEqual(split_netstring(a, 2, False), ("hello", "world"))
524         self.failUnlessEqual(split_netstring(a, 2, True),
525                              ("hello", "world", ""))
526         self.failUnlessRaises(ValueError, split_netstring, a, 3)
527         self.failUnlessRaises(ValueError, split_netstring, a+" extra", 2)
528         self.failUnlessRaises(ValueError, split_netstring, a+" extra", 2, False)
529
530     def test_extra(self):
531         a = netstring("hello")
532         self.failUnlessEqual(split_netstring(a, 1, True), ("hello", ""))
533         b = netstring("hello") + "extra stuff"
534         self.failUnlessEqual(split_netstring(b, 1, True),
535                              ("hello", "extra stuff"))
536
537     def test_nested(self):
538         a = netstring("hello") + netstring("world") + "extra stuff"
539         b = netstring("a") + netstring("is") + netstring(a) + netstring(".")
540         top = split_netstring(b, 4)
541         self.failUnlessEqual(len(top), 4)
542         self.failUnlessEqual(top[0], "a")
543         self.failUnlessEqual(top[1], "is")
544         self.failUnlessEqual(top[2], a)
545         self.failUnlessEqual(top[3], ".")
546         self.failUnlessRaises(ValueError, split_netstring, a, 2)
547         self.failUnlessRaises(ValueError, split_netstring, a, 2, False)
548         bottom = split_netstring(a, 2, True)
549         self.failUnlessEqual(bottom, ("hello", "world", "extra stuff"))
550