]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/test/test_mutable.py
MDMFSlotReadProxy: remove the queue
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_mutable.py
index deb4a58ceb0873e2a0d9cf21203facdc90e0f460..14b62ee2eae13da976608c296fe3c92638671b47 100644 (file)
@@ -20,6 +20,7 @@ from foolscap.api import eventually, fireEventually
 from foolscap.logging import log
 from allmydata.storage_client import StorageFarmBroker
 from allmydata.storage.common import storage_index_to_dir
+from allmydata.scripts import debug
 
 from allmydata.mutable.filenode import MutableFileNode, BackoffAgent
 from allmydata.mutable.common import ResponseCache, \
@@ -71,7 +72,9 @@ class FakeStorage:
         d = defer.Deferred()
         if not self._pending:
             self._pending_timer = reactor.callLater(1.0, self._fire_readers)
-        self._pending[peerid] = (d, shares)
+        if peerid not in self._pending:
+            self._pending[peerid] = []
+        self._pending[peerid].append( (d, shares) )
         return d
 
     def _fire_readers(self):
@@ -80,10 +83,11 @@ class FakeStorage:
         self._pending = {}
         for peerid in self._sequence:
             if peerid in pending:
-                d, shares = pending.pop(peerid)
+                for (d, shares) in pending.pop(peerid):
+                    eventually(d.callback, shares)
+        for peerid in pending:
+            for (d, shares) in pending[peerid]:
                 eventually(d.callback, shares)
-        for (d, shares) in pending.values():
-            eventually(d.callback, shares)
 
     def write(self, peerid, storage_index, shnum, offset, data):
         if peerid not in self._peers:
@@ -344,7 +348,7 @@ class Filenode(unittest.TestCase, testutil.ShouldFailMixin):
         def _created(n):
             self.failUnless(isinstance(n, MutableFileNode))
             cap = n.get_cap()
-            self.failUnless(isinstance(cap, uri.WritableMDMFFileURI))
+            self.failUnless(isinstance(cap, uri.WriteableMDMFFileURI))
             rcap = n.get_readcap()
             self.failUnless(isinstance(rcap, uri.ReadonlyMDMFFileURI))
             vcap = n.get_verify_cap()
@@ -2918,41 +2922,103 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
         self.nm = self.c.nodemaker
         self.data = "test data" * 100000 # about 900 KiB; MDMF
         self.small_data = "test data" * 10 # about 90 B; SDMF
-        return self.do_upload()
 
 
-    def do_upload(self):
-        d1 = self.nm.create_mutable_file(MutableData(self.data),
-                                         version=MDMF_VERSION)
-        d2 = self.nm.create_mutable_file(MutableData(self.small_data))
-        dl = gatherResults([d1, d2])
-        def _then((n1, n2)):
-            assert isinstance(n1, MutableFileNode)
-            assert isinstance(n2, MutableFileNode)
-
-            self.mdmf_node = n1
-            self.sdmf_node = n2
-        dl.addCallback(_then)
-        return dl
+    def do_upload_mdmf(self):
+        d = self.nm.create_mutable_file(MutableData(self.data),
+                                        version=MDMF_VERSION)
+        def _then(n):
+            assert isinstance(n, MutableFileNode)
+            assert n._protocol_version == MDMF_VERSION
+            self.mdmf_node = n
+            return n
+        d.addCallback(_then)
+        return d
 
+    def do_upload_sdmf(self):
+        d = self.nm.create_mutable_file(MutableData(self.small_data))
+        def _then(n):
+            assert isinstance(n, MutableFileNode)
+            assert n._protocol_version == SDMF_VERSION
+            self.sdmf_node = n
+            return n
+        d.addCallback(_then)
+        return d
 
-    def test_get_readonly_mutable_version(self):
-        # Attempting to get a mutable version of a mutable file from a
-        # filenode initialized with a readcap should return a readonly
-        # version of that same node.
-        ro = self.mdmf_node.get_readonly()
-        d = ro.get_best_mutable_version()
-        d.addCallback(lambda version:
-            self.failUnless(version.is_readonly()))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.get_readonly())
-        d.addCallback(lambda version:
-            self.failUnless(version.is_readonly()))
+    def do_upload_empty_sdmf(self):
+        d = self.nm.create_mutable_file(MutableData(""))
+        def _then(n):
+            assert isinstance(n, MutableFileNode)
+            self.sdmf_zero_length_node = n
+            assert n._protocol_version == SDMF_VERSION
+            return n
+        d.addCallback(_then)
         return d
 
+    def do_upload(self):
+        d = self.do_upload_mdmf()
+        d.addCallback(lambda ign: self.do_upload_sdmf())
+        return d
+
+    def test_debug(self):
+        d = self.do_upload_mdmf()
+        def _debug(n):
+            fso = debug.FindSharesOptions()
+            storage_index = base32.b2a(n.get_storage_index())
+            fso.si_s = storage_index
+            fso.nodedirs = [unicode(os.path.dirname(os.path.abspath(storedir)))
+                            for (i,ss,storedir)
+                            in self.iterate_servers()]
+            fso.stdout = StringIO()
+            fso.stderr = StringIO()
+            debug.find_shares(fso)
+            sharefiles = fso.stdout.getvalue().splitlines()
+            expected = self.nm.default_encoding_parameters["n"]
+            self.failUnlessEqual(len(sharefiles), expected)
+
+            do = debug.DumpOptions()
+            do["filename"] = sharefiles[0]
+            do.stdout = StringIO()
+            debug.dump_share(do)
+            output = do.stdout.getvalue()
+            lines = set(output.splitlines())
+            self.failUnless("Mutable slot found:" in lines, output)
+            self.failUnless(" share_type: MDMF" in lines, output)
+            self.failUnless(" num_extra_leases: 0" in lines, output)
+            self.failUnless(" MDMF contents:" in lines, output)
+            self.failUnless("  seqnum: 1" in lines, output)
+            self.failUnless("  required_shares: 3" in lines, output)
+            self.failUnless("  total_shares: 10" in lines, output)
+            self.failUnless("  segsize: 131073" in lines, output)
+            self.failUnless("  datalen: %d" % len(self.data) in lines, output)
+            vcap = n.get_verify_cap().to_string()
+            self.failUnless("  verify-cap: %s" % vcap in lines, output)
+
+            cso = debug.CatalogSharesOptions()
+            cso.nodedirs = fso.nodedirs
+            cso.stdout = StringIO()
+            cso.stderr = StringIO()
+            debug.catalog_shares(cso)
+            shares = cso.stdout.getvalue().splitlines()
+            oneshare = shares[0] # all shares should be MDMF
+            self.failIf(oneshare.startswith("UNKNOWN"), oneshare)
+            self.failUnless(oneshare.startswith("MDMF"), oneshare)
+            fields = oneshare.split()
+            self.failUnlessEqual(fields[0], "MDMF")
+            self.failUnlessEqual(fields[1], storage_index)
+            self.failUnlessEqual(fields[2], "3/10")
+            self.failUnlessEqual(fields[3], "%d" % len(self.data))
+            self.failUnless(fields[4].startswith("#1:"), fields[3])
+            # the rest of fields[4] is the roothash, which depends upon
+            # encryption salts and is not constant. fields[5] is the
+            # remaining time on the longest lease, which is timing dependent.
+            # The rest of the line is the quoted pathname to the share.
+        d.addCallback(_debug)
+        return d
 
     def test_get_sequence_number(self):
-        d = self.mdmf_node.get_best_readable_version()
+        d = self.do_upload()
+        d.addCallback(lambda ign: self.mdmf_node.get_best_readable_version())
         d.addCallback(lambda bv:
             self.failUnlessEqual(bv.get_sequence_number(), 1))
         d.addCallback(lambda ignored:
@@ -2984,7 +3050,8 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
         # We need to define an API by which an uploader can set the
         # extension parameters, and by which a downloader can retrieve
         # extensions.
-        d = self.mdmf_node.get_best_mutable_version()
+        d = self.do_upload_mdmf()
+        d.addCallback(lambda ign: self.mdmf_node.get_best_mutable_version())
         def _got_version(version):
             hints = version.get_downloader_hints()
             # Should be empty at this point.
@@ -3000,9 +3067,12 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
         # If we initialize a mutable file with a cap that has extension
         # parameters in it and then grab the extension parameters using
         # our API, we should see that they're set correctly.
-        mdmf_uri = self.mdmf_node.get_uri()
-        new_node = self.nm.create_from_cap(mdmf_uri)
-        d = new_node.get_best_mutable_version()
+        d = self.do_upload_mdmf()
+        def _then(ign):
+            mdmf_uri = self.mdmf_node.get_uri()
+            new_node = self.nm.create_from_cap(mdmf_uri)
+            return new_node.get_best_mutable_version()
+        d.addCallback(_then)
         def _got_version(version):
             hints = version.get_downloader_hints()
             self.failUnlessIn("k", hints)
@@ -3037,66 +3107,64 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
         # it's an MDMF file, we should get an MDMF cap back from that
         # file and should be able to use that.
         # That's essentially what MDMF node is, so just check that.
-        mdmf_uri = self.mdmf_node.get_uri()
-        cap = uri.from_string(mdmf_uri)
-        self.failUnless(isinstance(cap, uri.WritableMDMFFileURI))
-        readonly_mdmf_uri = self.mdmf_node.get_readonly_uri()
-        cap = uri.from_string(readonly_mdmf_uri)
-        self.failUnless(isinstance(cap, uri.ReadonlyMDMFFileURI))
-
-
-    def test_get_writekey(self):
-        d = self.mdmf_node.get_best_mutable_version()
-        d.addCallback(lambda bv:
-            self.failUnlessEqual(bv.get_writekey(),
-                                 self.mdmf_node.get_writekey()))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.get_best_mutable_version())
-        d.addCallback(lambda bv:
-            self.failUnlessEqual(bv.get_writekey(),
-                                 self.sdmf_node.get_writekey()))
+        d = self.do_upload_mdmf()
+        def _then(ign):
+            mdmf_uri = self.mdmf_node.get_uri()
+            cap = uri.from_string(mdmf_uri)
+            self.failUnless(isinstance(cap, uri.WriteableMDMFFileURI))
+            readonly_mdmf_uri = self.mdmf_node.get_readonly_uri()
+            cap = uri.from_string(readonly_mdmf_uri)
+            self.failUnless(isinstance(cap, uri.ReadonlyMDMFFileURI))
+        d.addCallback(_then)
         return d
 
-
-    def test_get_storage_index(self):
-        d = self.mdmf_node.get_best_mutable_version()
-        d.addCallback(lambda bv:
-            self.failUnlessEqual(bv.get_storage_index(),
-                                 self.mdmf_node.get_storage_index()))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.get_best_mutable_version())
-        d.addCallback(lambda bv:
-            self.failUnlessEqual(bv.get_storage_index(),
-                                 self.sdmf_node.get_storage_index()))
+    def test_mutable_version(self):
+        # assert that getting parameters from the IMutableVersion object
+        # gives us the same data as getting them from the filenode itself
+        d = self.do_upload()
+        d.addCallback(lambda ign: self.mdmf_node.get_best_mutable_version())
+        def _check_mdmf(bv):
+            n = self.mdmf_node
+            self.failUnlessEqual(bv.get_writekey(), n.get_writekey())
+            self.failUnlessEqual(bv.get_storage_index(), n.get_storage_index())
+            self.failIf(bv.is_readonly())
+        d.addCallback(_check_mdmf)
+        d.addCallback(lambda ign: self.sdmf_node.get_best_mutable_version())
+        def _check_sdmf(bv):
+            n = self.sdmf_node
+            self.failUnlessEqual(bv.get_writekey(), n.get_writekey())
+            self.failUnlessEqual(bv.get_storage_index(), n.get_storage_index())
+            self.failIf(bv.is_readonly())
+        d.addCallback(_check_sdmf)
         return d
 
 
     def test_get_readonly_version(self):
-        d = self.mdmf_node.get_best_readable_version()
-        d.addCallback(lambda bv:
-            self.failUnless(bv.is_readonly()))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.get_best_readable_version())
-        d.addCallback(lambda bv:
-            self.failUnless(bv.is_readonly()))
-        return d
+        d = self.do_upload()
+        d.addCallback(lambda ign: self.mdmf_node.get_best_readable_version())
+        d.addCallback(lambda bv: self.failUnless(bv.is_readonly()))
 
+        # Attempting to get a mutable version of a mutable file from a
+        # filenode initialized with a readcap should return a readonly
+        # version of that same node.
+        d.addCallback(lambda ign: self.mdmf_node.get_readonly())
+        d.addCallback(lambda ro: ro.get_best_mutable_version())
+        d.addCallback(lambda v: self.failUnless(v.is_readonly()))
 
-    def test_get_mutable_version(self):
-        d = self.mdmf_node.get_best_mutable_version()
-        d.addCallback(lambda bv:
-            self.failIf(bv.is_readonly()))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.get_best_mutable_version())
-        d.addCallback(lambda bv:
-            self.failIf(bv.is_readonly()))
+        d.addCallback(lambda ign: self.sdmf_node.get_best_readable_version())
+        d.addCallback(lambda bv: self.failUnless(bv.is_readonly()))
+
+        d.addCallback(lambda ign: self.sdmf_node.get_readonly())
+        d.addCallback(lambda ro: ro.get_best_mutable_version())
+        d.addCallback(lambda v: self.failUnless(v.is_readonly()))
         return d
 
 
     def test_toplevel_overwrite(self):
         new_data = MutableData("foo bar baz" * 100000)
         new_small_data = MutableData("foo bar baz" * 10)
-        d = self.mdmf_node.overwrite(new_data)
+        d = self.do_upload()
+        d.addCallback(lambda ign: self.mdmf_node.overwrite(new_data))
         d.addCallback(lambda ignored:
             self.mdmf_node.download_best_version())
         d.addCallback(lambda data:
@@ -3111,9 +3179,10 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
 
 
     def test_toplevel_modify(self):
+        d = self.do_upload()
         def modifier(old_contents, servermap, first_time):
             return old_contents + "modified"
-        d = self.mdmf_node.modify(modifier)
+        d.addCallback(lambda ign: self.mdmf_node.modify(modifier))
         d.addCallback(lambda ignored:
             self.mdmf_node.download_best_version())
         d.addCallback(lambda data:
@@ -3131,9 +3200,10 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
         # TODO: When we can publish multiple versions, alter this test
         # to modify a version other than the best usable version, then
         # test to see that the best recoverable version is that.
+        d = self.do_upload()
         def modifier(old_contents, servermap, first_time):
             return old_contents + "modified"
-        d = self.mdmf_node.modify(modifier)
+        d.addCallback(lambda ign: self.mdmf_node.modify(modifier))
         d.addCallback(lambda ignored:
             self.mdmf_node.download_best_version())
         d.addCallback(lambda data:
@@ -3185,7 +3255,8 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
 
 
     def test_download_nonexistent_version(self):
-        d = self.mdmf_node.get_servermap(mode=MODE_WRITE)
+        d = self.do_upload_mdmf()
+        d.addCallback(lambda ign: self.mdmf_node.get_servermap(mode=MODE_WRITE))
         def _set_servermap(servermap):
             self.servermap = servermap
         d.addCallback(_set_servermap)
@@ -3198,9 +3269,21 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
 
 
     def test_partial_read(self):
-        # read only a few bytes at a time, and see that the results are
+        d = self.do_upload_mdmf()
+        d.addCallback(lambda ign: self.mdmf_node.get_best_readable_version())
+        modes = [("start_on_segment_boundary",
+                  mathutil.next_multiple(128 * 1024, 3), 50),
+                 ("ending_one_byte_after_segment_boundary",
+                  mathutil.next_multiple(128 * 1024, 3)-50, 51),
+                 ("zero_length_at_start", 0, 0),
+                 ("zero_length_in_middle", 50, 0),
+                 ("zero_length_at_segment_boundary",
+                  mathutil.next_multiple(128 * 1024, 3), 0),
+                 ]
+        for (name, offset, length) in modes:
+            d.addCallback(self._do_partial_read, name, offset, length)
+        # then read only a few bytes at a time, and see that the results are
         # what we expect.
-        d = self.mdmf_node.get_best_readable_version()
         def _read_data(version):
             c = consumer.MemoryConsumer()
             d2 = defer.succeed(None)
@@ -3211,51 +3294,49 @@ class Version(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin, \
             return d2
         d.addCallback(_read_data)
         return d
-
-    def test_partial_read_starting_on_segment_boundary(self):
-        d = self.mdmf_node.get_best_readable_version()
+    def _do_partial_read(self, version, name, offset, length):
         c = consumer.MemoryConsumer()
-        offset = mathutil.next_multiple(128 * 1024, 3)
-        d.addCallback(lambda version:
-            version.read(c, offset, 50))
-        expected = self.data[offset:offset+50]
-        d.addCallback(lambda ignored:
-            self.failUnlessEqual(expected, "".join(c.chunks)))
+        d = version.read(c, offset, length)
+        expected = self.data[offset:offset+length]
+        d.addCallback(lambda ignored: "".join(c.chunks))
+        def _check(results):
+            if results != expected:
+                print
+                print "got: %s ... %s" % (results[:20], results[-20:])
+                print "exp: %s ... %s" % (expected[:20], expected[-20:])
+                self.fail("results[%s] != expected" % name)
+            return version # daisy-chained to next call
+        d.addCallback(_check)
         return d
 
-    def test_partial_read_ending_on_segment_boundary(self):
-        d = self.mdmf_node.get_best_readable_version()
-        c = consumer.MemoryConsumer()
-        offset = mathutil.next_multiple(128 * 1024, 3)
-        start = offset - 50
-        d.addCallback(lambda version:
-            version.read(c, start, 51))
-        expected = self.data[offset-50:offset+1]
-        d.addCallback(lambda ignored:
-            self.failUnlessEqual(expected, "".join(c.chunks)))
-        return d
 
-    def test_read(self):
-        d = self.mdmf_node.get_best_readable_version()
+    def _test_read_and_download(self, node, expected):
+        d = node.get_best_readable_version()
         def _read_data(version):
             c = consumer.MemoryConsumer()
             d2 = defer.succeed(None)
             d2.addCallback(lambda ignored: version.read(c))
             d2.addCallback(lambda ignored:
-                self.failUnlessEqual("".join(c.chunks), self.data))
+                self.failUnlessEqual(expected, "".join(c.chunks)))
             return d2
         d.addCallback(_read_data)
+        d.addCallback(lambda ignored: node.download_best_version())
+        d.addCallback(lambda data: self.failUnlessEqual(expected, data))
         return d
 
+    def test_read_and_download_mdmf(self):
+        d = self.do_upload_mdmf()
+        d.addCallback(self._test_read_and_download, self.data)
+        return d
 
-    def test_download_best_version(self):
-        d = self.mdmf_node.download_best_version()
-        d.addCallback(lambda data:
-            self.failUnlessEqual(data, self.data))
-        d.addCallback(lambda ignored:
-            self.sdmf_node.download_best_version())
-        d.addCallback(lambda data:
-            self.failUnlessEqual(data, self.small_data))
+    def test_read_and_download_sdmf(self):
+        d = self.do_upload_sdmf()
+        d.addCallback(self._test_read_and_download, self.small_data)
+        return d
+
+    def test_read_and_download_sdmf_zero_length(self):
+        d = self.do_upload_empty_sdmf()
+        d.addCallback(self._test_read_and_download, "")
         return d
 
 
@@ -3270,102 +3351,97 @@ class Update(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
         self.nm = self.c.nodemaker
         self.data = "testdata " * 100000 # about 900 KiB; MDMF
         self.small_data = "test data" * 10 # about 90 B; SDMF
-        return self.do_upload()
 
 
-    def do_upload(self):
-        d1 = self.nm.create_mutable_file(MutableData(self.data),
-                                         version=MDMF_VERSION)
-        d2 = self.nm.create_mutable_file(MutableData(self.small_data))
-        dl = gatherResults([d1, d2])
-        def _then((n1, n2)):
-            assert isinstance(n1, MutableFileNode)
-            assert isinstance(n2, MutableFileNode)
-
-            self.mdmf_node = n1
-            self.sdmf_node = n2
-        dl.addCallback(_then)
-        # Make SDMF and MDMF mutable file nodes that have 255 shares.
-        def _make_max_shares(ign):
+    def do_upload_sdmf(self):
+        d = self.nm.create_mutable_file(MutableData(self.small_data))
+        def _then(n):
+            assert isinstance(n, MutableFileNode)
+            self.sdmf_node = n
+            # Make SDMF node that has 255 shares.
             self.nm.default_encoding_parameters['n'] = 255
             self.nm.default_encoding_parameters['k'] = 127
-            d1 = self.nm.create_mutable_file(MutableData(self.data),
-                                             version=MDMF_VERSION)
-            d2 = \
-                self.nm.create_mutable_file(MutableData(self.small_data))
-            return gatherResults([d1, d2])
-        dl.addCallback(_make_max_shares)
-        def _stash((n1, n2)):
-            assert isinstance(n1, MutableFileNode)
-            assert isinstance(n2, MutableFileNode)
-
-            self.mdmf_max_shares_node = n1
-            self.sdmf_max_shares_node = n2
-        dl.addCallback(_stash)
-        return dl
+            return self.nm.create_mutable_file(MutableData(self.small_data))
+        d.addCallback(_then)
+        def _then2(n):
+            assert isinstance(n, MutableFileNode)
+            self.sdmf_max_shares_node = n
+        d.addCallback(_then2)
+        return d
+
+    def do_upload_mdmf(self):
+        d = self.nm.create_mutable_file(MutableData(self.data),
+                                        version=MDMF_VERSION)
+        def _then(n):
+            assert isinstance(n, MutableFileNode)
+            self.mdmf_node = n
+            # Make MDMF node that has 255 shares.
+            self.nm.default_encoding_parameters['n'] = 255
+            self.nm.default_encoding_parameters['k'] = 127
+            return self.nm.create_mutable_file(MutableData(self.data),
+                                               version=MDMF_VERSION)
+        d.addCallback(_then)
+        def _then2(n):
+            assert isinstance(n, MutableFileNode)
+            self.mdmf_max_shares_node = n
+        d.addCallback(_then2)
+        return d
+
+    def _test_replace(self, offset, new_data):
+        expected = self.data[:offset]+new_data+self.data[offset+len(new_data):]
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            d = defer.succeed(None)
+            for node in (self.mdmf_node, self.mdmf_max_shares_node):
+                d.addCallback(lambda ign: node.get_best_mutable_version())
+                d.addCallback(lambda mv:
+                    mv.update(MutableData(new_data), offset))
+                # close around node.
+                d.addCallback(lambda ignored, node=node:
+                    node.download_best_version())
+                def _check(results):
+                    if results != expected:
+                        print
+                        print "got: %s ... %s" % (results[:20], results[-20:])
+                        print "exp: %s ... %s" % (expected[:20], expected[-20:])
+                        self.fail("results != expected")
+                d.addCallback(_check)
+            return d
+        d0.addCallback(_run)
+        return d0
 
     def test_append(self):
-        # We should be able to append data to the middle of a mutable
-        # file and get what we expect.
-        new_data = self.data + "appended"
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData("appended"), len(self.data)))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
+        # We should be able to append data to a mutable file and get
+        # what we expect.
+        return self._test_replace(len(self.data), "appended")
 
-    def test_replace(self):
+    def test_replace_middle(self):
         # We should be able to replace data in the middle of a mutable
-        # file and get what we expect back. 
-        new_data = self.data[:100]
-        new_data += "appended"
-        new_data += self.data[108:]
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData("appended"), 100))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
+        # file and get what we expect back.
+        return self._test_replace(100, "replaced")
 
     def test_replace_beginning(self):
         # We should be able to replace data at the beginning of the file
         # without truncating the file
-        B = "beginning"
-        new_data = B + self.data[len(B):]
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv: mv.update(MutableData(B), 0))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results: self.failUnlessEqual(results, new_data))
-        return d
+        return self._test_replace(0, "beginning")
 
     def test_replace_segstart1(self):
-        offset = 128*1024+1
-        new_data = "NNNN"
-        expected = self.data[:offset]+new_data+self.data[offset+4:]
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData(new_data), offset))
-            # close around node.
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            def _check(results):
-                if results != expected:
-                    print
-                    print "got: %s ... %s" % (results[:20], results[-20:])
-                    print "exp: %s ... %s" % (expected[:20], expected[-20:])
-                    self.fail("results != expected")
-            d.addCallback(_check)
-        return d
+        return self._test_replace(128*1024+1, "NNNN")
+
+    def test_replace_zero_length_beginning(self):
+        return self._test_replace(0, "")
+
+    def test_replace_zero_length_middle(self):
+        return self._test_replace(50, "")
+
+    def test_replace_zero_length_segstart1(self):
+        return self._test_replace(128*1024+1, "")
+
+    def test_replace_and_extend(self):
+        # We should be able to replace data in the middle of a mutable
+        # file and extend that mutable file and get what we expect.
+        return self._test_replace(100, "modified " * 100000)
+
 
     def _check_differences(self, got, expected):
         # displaying arbitrary file corruption is tricky for a
@@ -3401,62 +3477,55 @@ class Update(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
 
     def test_replace_locations(self):
         # exercise fencepost conditions
-        expected = self.data
         SEGSIZE = 128*1024
         suspects = range(SEGSIZE-3, SEGSIZE+1)+range(2*SEGSIZE-3, 2*SEGSIZE+1)
         letters = iter("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-        d = defer.succeed(None)
-        for offset in suspects:
-            new_data = letters.next()*2 # "AA", then "BB", etc
-            expected = expected[:offset]+new_data+expected[offset+2:]
-            d.addCallback(lambda ign:
-                          self.mdmf_node.get_best_mutable_version())
-            def _modify(mv, offset=offset, new_data=new_data):
-                # close over 'offset','new_data'
-                md = MutableData(new_data)
-                return mv.update(md, offset)
-            d.addCallback(_modify)
-            d.addCallback(lambda ignored:
-                          self.mdmf_node.download_best_version())
-            d.addCallback(self._check_differences, expected)
-        return d
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            expected = self.data
+            d = defer.succeed(None)
+            for offset in suspects:
+                new_data = letters.next()*2 # "AA", then "BB", etc
+                expected = expected[:offset]+new_data+expected[offset+2:]
+                d.addCallback(lambda ign:
+                              self.mdmf_node.get_best_mutable_version())
+                def _modify(mv, offset=offset, new_data=new_data):
+                    # close over 'offset','new_data'
+                    md = MutableData(new_data)
+                    return mv.update(md, offset)
+                d.addCallback(_modify)
+                d.addCallback(lambda ignored:
+                              self.mdmf_node.download_best_version())
+                d.addCallback(self._check_differences, expected)
+            return d
+        d0.addCallback(_run)
+        return d0
 
     def test_replace_locations_max_shares(self):
         # exercise fencepost conditions
-        expected = self.data
         SEGSIZE = 128*1024
         suspects = range(SEGSIZE-3, SEGSIZE+1)+range(2*SEGSIZE-3, 2*SEGSIZE+1)
         letters = iter("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
-        d = defer.succeed(None)
-        for offset in suspects:
-            new_data = letters.next()*2 # "AA", then "BB", etc
-            expected = expected[:offset]+new_data+expected[offset+2:]
-            d.addCallback(lambda ign:
-                          self.mdmf_max_shares_node.get_best_mutable_version())
-            def _modify(mv, offset=offset, new_data=new_data):
-                # close over 'offset','new_data'
-                md = MutableData(new_data)
-                return mv.update(md, offset)
-            d.addCallback(_modify)
-            d.addCallback(lambda ignored:
-                          self.mdmf_max_shares_node.download_best_version())
-            d.addCallback(self._check_differences, expected)
-        return d
-
-    def test_replace_and_extend(self):
-        # We should be able to replace data in the middle of a mutable
-        # file and extend that mutable file and get what we expect.
-        new_data = self.data[:100]
-        new_data += "modified " * 100000
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData("modified " * 100000), 100))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            expected = self.data
+            d = defer.succeed(None)
+            for offset in suspects:
+                new_data = letters.next()*2 # "AA", then "BB", etc
+                expected = expected[:offset]+new_data+expected[offset+2:]
+                d.addCallback(lambda ign:
+                              self.mdmf_max_shares_node.get_best_mutable_version())
+                def _modify(mv, offset=offset, new_data=new_data):
+                    # close over 'offset','new_data'
+                    md = MutableData(new_data)
+                    return mv.update(md, offset)
+                d.addCallback(_modify)
+                d.addCallback(lambda ignored:
+                              self.mdmf_max_shares_node.download_best_version())
+                d.addCallback(self._check_differences, expected)
+            return d
+        d0.addCallback(_run)
+        return d0
 
 
     def test_append_power_of_two(self):
@@ -3470,29 +3539,38 @@ class Update(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
         # power-of-two boundary.
         segment = "a" * DEFAULT_MAX_SEGMENT_SIZE
         new_data = self.data + (segment * 2)
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData(segment * 2), len(self.data)))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
-
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            d = defer.succeed(None)
+            for node in (self.mdmf_node, self.mdmf_max_shares_node):
+                d.addCallback(lambda ign: node.get_best_mutable_version())
+                d.addCallback(lambda mv:
+                    mv.update(MutableData(segment * 2), len(self.data)))
+                d.addCallback(lambda ignored, node=node:
+                    node.download_best_version())
+                d.addCallback(lambda results:
+                    self.failUnlessEqual(results, new_data))
+            return d
+        d0.addCallback(_run)
+        return d0
 
     def test_update_sdmf(self):
         # Running update on a single-segment file should still work.
         new_data = self.small_data + "appended"
-        for node in (self.sdmf_node, self.sdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData("appended"), len(self.small_data)))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
+        d0 = self.do_upload_sdmf()
+        def _run(ign):
+            d = defer.succeed(None)
+            for node in (self.sdmf_node, self.sdmf_max_shares_node):
+                d.addCallback(lambda ign: node.get_best_mutable_version())
+                d.addCallback(lambda mv:
+                    mv.update(MutableData("appended"), len(self.small_data)))
+                d.addCallback(lambda ignored, node=node:
+                    node.download_best_version())
+                d.addCallback(lambda results:
+                    self.failUnlessEqual(results, new_data))
+            return d
+        d0.addCallback(_run)
+        return d0
 
     def test_replace_in_last_segment(self):
         # The wrapper should know how to handle the tail segment
@@ -3501,16 +3579,20 @@ class Update(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
         new_data = self.data[:replace_offset] + "replaced"
         rest_offset = replace_offset + len("replaced")
         new_data += self.data[rest_offset:]
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData("replaced"), replace_offset))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
-
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            d = defer.succeed(None)
+            for node in (self.mdmf_node, self.mdmf_max_shares_node):
+                d.addCallback(lambda ign: node.get_best_mutable_version())
+                d.addCallback(lambda mv:
+                    mv.update(MutableData("replaced"), replace_offset))
+                d.addCallback(lambda ignored, node=node:
+                    node.download_best_version())
+                d.addCallback(lambda results:
+                    self.failUnlessEqual(results, new_data))
+            return d
+        d0.addCallback(_run)
+        return d0
 
     def test_multiple_segment_replace(self):
         replace_offset = 2 * DEFAULT_MAX_SEGMENT_SIZE
@@ -3520,16 +3602,21 @@ class Update(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
         new_data += "replaced"
         rest_offset = len(new_data)
         new_data += self.data[rest_offset:]
-        for node in (self.mdmf_node, self.mdmf_max_shares_node):
-            d = node.get_best_mutable_version()
-            d.addCallback(lambda mv:
-                mv.update(MutableData((2 * new_segment) + "replaced"),
-                          replace_offset))
-            d.addCallback(lambda ignored, node=node:
-                node.download_best_version())
-            d.addCallback(lambda results:
-                self.failUnlessEqual(results, new_data))
-        return d
+        d0 = self.do_upload_mdmf()
+        def _run(ign):
+            d = defer.succeed(None)
+            for node in (self.mdmf_node, self.mdmf_max_shares_node):
+                d.addCallback(lambda ign: node.get_best_mutable_version())
+                d.addCallback(lambda mv:
+                    mv.update(MutableData((2 * new_segment) + "replaced"),
+                              replace_offset))
+                d.addCallback(lambda ignored, node=node:
+                    node.download_best_version())
+                d.addCallback(lambda results:
+                    self.failUnlessEqual(results, new_data))
+            return d
+        d0.addCallback(_run)
+        return d0
 
 class Interoperability(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixin):
     sdmf_old_shares = {}
@@ -3577,3 +3664,25 @@ class Interoperability(GridTestMixin, unittest.TestCase, testutil.ShouldFailMixi
         d = n.download_best_version()
         d.addCallback(self.failUnlessEqual, self.sdmf_old_contents)
         return d
+
+class DifferentEncoding(unittest.TestCase):
+    def setUp(self):
+        self._storage = s = FakeStorage()
+        self.nodemaker = make_nodemaker(s)
+
+    def test_filenode(self):
+        # create a file with 3-of-20, then modify it with a client configured
+        # to do 3-of-10. #1510 tracks a failure here
+        self.nodemaker.default_encoding_parameters["n"] = 20
+        d = self.nodemaker.create_mutable_file("old contents")
+        def _created(n):
+            filecap = n.get_cap().to_string()
+            del n # we want a new object, not the cached one
+            self.nodemaker.default_encoding_parameters["n"] = 10
+            n2 = self.nodemaker.create_from_cap(filecap)
+            return n2
+        d.addCallback(_created)
+        def modifier(old_contents, servermap, first_time):
+            return "new contents"
+        d.addCallback(lambda n: n.modify(modifier))
+        return d