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