]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/test/test_sftp.py
Improve SFTP error handling and remove use of IFinishableConsumer. fixes #1525
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_sftp.py
index b648154e28fe0220a22e84ba9c6a3e0dc33efd8b..69fedce4e6854fda00d60304131db069549f8ca1 100644 (file)
@@ -29,12 +29,17 @@ from allmydata.mutable.common import NotWriteableError
 
 from allmydata.util.consumer import download_to_data
 from allmydata.immutable import upload
+from allmydata.mutable import publish
 from allmydata.test.no_network import GridTestMixin
 from allmydata.test.common import ShouldFailMixin
 from allmydata.test.common_util import ReallyEqualMixin
 
 timeout = 240
 
+#defer.setDebugging(True)
+#from twisted.internet import base
+#base.DelayedCall.debug = True
+
 class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCase):
     """This is a no-network unit test of the SFTPUserHandler and the abstractions it uses."""
 
@@ -76,7 +81,8 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         return d
 
     def _set_up_tree(self):
-        d = self.client.create_mutable_file("mutable file contents")
+        u = publish.MutableData("mutable file contents")
+        d = self.client.create_mutable_file(u)
         d.addCallback(lambda node: self.root.set_node(u"mutable", node))
         def _created_mutable(n):
             self.mutable = n
@@ -207,7 +213,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
 
     def test_convert_error(self):
         self.failUnlessReallyEqual(sftpd._convert_error(None, "request"), None)
-        
+
         d = defer.succeed(None)
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_convert_error SFTPError",
@@ -517,6 +523,43 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
             return d2
         d.addCallback(_read_short)
 
+        # check that failed downloads cause failed reads
+        d.addCallback(lambda ign: self.handler.openFile("uri/"+self.gross_uri, sftp.FXF_READ, {}))
+        def _read_broken(rf):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read broken",
+                                             rf.readChunk, 0, 100))
+            # close shouldn't fail
+            d2.addCallback(lambda ign: rf.close())
+            d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
+            return d2
+        d.addCallback(_read_broken)
+
+        d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
+        d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
+        return d
+
+    def test_openFile_read_error(self):
+        # The check at the end of openFile_read tested this for large files, but it trashed
+        # the grid in the process, so this needs to be a separate test.
+        small = upload.Data("0123456789"*10, None)
+        d = self._set_up("openFile_read_error")
+        d.addCallback(lambda ign: self.root.add_file(u"small", small))
+        d.addCallback(lambda n: self.handler.openFile("/uri/"+n.get_uri(), sftp.FXF_READ, {}))
+        def _read_broken(rf):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read broken",
+                                             rf.readChunk, 0, 100))
+            # close shouldn't fail
+            d2.addCallback(lambda ign: rf.close())
+            d2.addCallback(lambda res: self.failUnlessReallyEqual(res, None))
+            return d2
+        d.addCallback(_read_broken)
+
         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
@@ -911,6 +954,9 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                       self.handler.openFile("small", sftp.FXF_READ | sftp.FXF_WRITE, {}))
         def _read_write(rwf):
             d2 = rwf.writeChunk(8, "0123")
+            # test immediate read starting after the old end-of-file
+            d2.addCallback(lambda ign: rwf.readChunk(11, 1))
+            d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "3"))
             d2.addCallback(lambda ign: rwf.readChunk(0, 100))
             d2.addCallback(lambda data: self.failUnlessReallyEqual(data, "012345670123"))
             d2.addCallback(lambda ign: rwf.close())
@@ -977,6 +1023,24 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
                       self.shouldFail(NoSuchChildError, "rename new while open", "new",
                                       self.root.get, u"new"))
 
+        # check that failed downloads cause failed reads and failed close, when open for writing
+        gross = u"gro\u00DF".encode("utf-8")
+        d.addCallback(lambda ign: self.handler.openFile(gross, sftp.FXF_READ | sftp.FXF_WRITE, {}))
+        def _read_write_broken(rwf):
+            d2 = rwf.writeChunk(0, "abcdefghij")
+            d2.addCallback(lambda ign: self.g.nuke_from_orbit())
+
+            # reading should fail (reliably if we read past the written chunk)
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read/write broken",
+                                             rwf.readChunk, 0, 100))
+            # close should fail in this case
+            d2.addCallback(lambda ign:
+                self.shouldFailWithSFTPError(sftp.FX_FAILURE, "read/write broken close",
+                                             rwf.close))
+            return d2
+        d.addCallback(_read_write_broken)
+
         d.addCallback(lambda ign: self.failUnlessEqual(sftpd.all_heisenfiles, {}))
         d.addCallback(lambda ign: self.failUnlessEqual(self.handler._heisenfiles, {}))
         return d
@@ -997,7 +1061,7 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
                                          self.handler.removeFile, ""))
-            
+
         # removing a directory should fail
         d.addCallback(lambda ign:
             self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
@@ -1327,49 +1391,69 @@ class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCas
         return d
 
     def test_execCommand_and_openShell(self):
-        class FakeProtocol:
+        class MockProtocol:
             def __init__(self):
                 self.output = ""
+                self.error = ""
                 self.reason = None
+
             def write(self, data):
+                return self.outReceived(data)
+
+            def outReceived(self, data):
                 self.output += data
                 return defer.succeed(None)
+
+            def errReceived(self, data):
+                self.error += data
+                return defer.succeed(None)
+
             def processEnded(self, reason):
                 self.reason = reason
                 return defer.succeed(None)
 
+        def _lines_end_in_crlf(s):
+            return s.replace('\r\n', '').find('\n') == -1 and s.endswith('\r\n')
+
         d = self._set_up("execCommand_and_openShell")
 
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _exec_df(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.execCommand(protocol, "df -P -k /")
             d2.addCallback(lambda ign: self.failUnlessIn("1024-blocks", protocol.output))
+            d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.output), protocol.output))
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.error, ""))
             d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessDone)))
             d2.addCallback(lambda ign: session.eofReceived())
             d2.addCallback(lambda ign: session.closed())
             return d2
         d.addCallback(_exec_df)
 
+        def _check_unsupported(protocol):
+            d2 = defer.succeed(None)
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.output, ""))
+            d2.addCallback(lambda ign: self.failUnlessIn("only the SFTP protocol", protocol.error))
+            d2.addCallback(lambda ign: self.failUnless(_lines_end_in_crlf(protocol.error), protocol.error))
+            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
+            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            return d2
+
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _exec_error(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.execCommand(protocol, "error")
             d2.addCallback(lambda ign: session.windowChanged(None))
-            d2.addCallback(lambda ign: self.failUnlessEqual("", protocol.output))
-            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
-            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            d2.addCallback(lambda ign: _check_unsupported(protocol))
             d2.addCallback(lambda ign: session.closed())
             return d2
         d.addCallback(_exec_error)
 
         d.addCallback(lambda ign: conch_interfaces.ISession(self.handler))
         def _openShell(session):
-            protocol = FakeProtocol()
+            protocol = MockProtocol()
             d2 = session.openShell(protocol)
-            d2.addCallback(lambda ign: self.failUnlessIn("only SFTP", protocol.output))
-            d2.addCallback(lambda ign: self.failUnless(isinstance(protocol.reason.value, ProcessTerminated)))
-            d2.addCallback(lambda ign: self.failUnlessEqual(protocol.reason.value.exitCode, 1))
+            d2.addCallback(lambda ign: _check_unsupported(protocol))
             d2.addCallback(lambda ign: session.closed())
             return d2
         d.addCallback(_openShell)