[tor-commits] [stem/master] Integration tests for the ControlSocket class

atagar at torproject.org atagar at torproject.org
Sun Feb 5 04:35:31 UTC 2012


commit 2076447cb6b86e02bf9bb0cfc666f7568a8f4d00
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Feb 4 18:03:27 2012 -0800

    Integration tests for the ControlSocket class
    
    The ControlSocket subclasses were indirectly tested via the ControlMessage
    tests, but we missed several interesting use cases. Adding more testing for the
    ControlSocket instances and correcting some of its behavior with respect to
    reporting socket closer while sending messages.
---
 run_tests.py                         |    2 +
 stem/connection.py                   |    3 +-
 stem/socket.py                       |   28 +++++++--
 test/integ/socket/__init__.py        |    2 +-
 test/integ/socket/control_message.py |   14 +----
 test/integ/socket/control_socket.py  |  117 ++++++++++++++++++++++++++++++++++
 6 files changed, 147 insertions(+), 19 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index 755eda3..832cd1f 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -25,6 +25,7 @@ import test.integ.connection.authentication
 import test.integ.connection.connect
 import test.integ.connection.protocolinfo
 import test.integ.socket.control_message
+import test.integ.socket.control_socket
 import test.integ.util.conf
 import test.integ.util.system
 import test.integ.version
@@ -87,6 +88,7 @@ INTEG_TESTS = (
   test.integ.util.conf.TestConf,
   test.integ.util.system.TestSystem,
   test.integ.version.TestVersion,
+  test.integ.socket.control_socket.TestControlSocket,
   test.integ.socket.control_message.TestControlMessage,
   test.integ.connection.protocolinfo.TestProtocolInfo,
   test.integ.connection.authentication.TestAuthenticate,
diff --git a/stem/connection.py b/stem/connection.py
index 392ad9b..76099bf 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -613,7 +613,8 @@ def authenticate_cookie(control_socket, cookie_path, suppress_ctl_errors = True)
 def get_protocolinfo(control_socket):
   """
   Issues a PROTOCOLINFO query to a control socket, getting information about
-  the tor process running on it.
+  the tor process running on it. If the socket is already closed then it is
+  first reconnected.
   
   Arguments:
     control_socket (stem.socket.ControlSocket) - connected tor control socket
diff --git a/stem/socket.py b/stem/socket.py
index 1b481ce..5b931a3 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -107,7 +107,7 @@ class ControlSocket:
     
     Raises:
       stem.socket.SocketError if a problem arises in using the socket
-      stem.socket.SocketClosed if the socket is shut down
+      stem.socket.SocketClosed if the socket is known to be shut down
     """
     
     self._send_cond.acquire()
@@ -155,6 +155,14 @@ class ControlSocket:
     Checks if the socket is known to be closed. We won't be aware if it is
     until we either use it or have explicitily shut it down.
     
+    In practice a socket derived from a port knows about its disconnection
+    after a failed recv() call. Socket file derived connections know after
+    either a send() or recv().
+    
+    This means that to have reliable detection for when we're disconnected
+    you need to continually pull from the socket (which is part of what the
+    BaseController does).
+    
     Returns:
       bool that's True if we're known to be shut down and False otherwise
     """
@@ -178,9 +186,8 @@ class ControlSocket:
     if self.is_alive(): self.close()
     
     try:
-      control_socket = self._make_socket()
-      self._socket = control_socket
-      self._socket_file = control_socket.makefile()
+      self._socket = self._make_socket()
+      self._socket_file = self._socket.makefile()
       self._is_alive = True
     finally:
       self._send_cond.release()
@@ -213,6 +220,8 @@ class ControlSocket:
       try: self._socket_file.close()
       except: pass
     
+    self._socket = None
+    self._socket_file = None
     self._is_alive = False
     
     self._send_cond.release()
@@ -673,6 +682,7 @@ def send_message(control_file, message, raw = False):
   
   Raises:
     stem.socket.SocketError if a problem arises in using the socket
+    stem.socket.SocketClosed if the socket is known to be shut down
   """
   
   if not raw: message = send_formatting(message)
@@ -689,7 +699,15 @@ def send_message(control_file, message, raw = False):
     log.trace("Sent to tor:\n" + log_message)
   except socket.error, exc:
     log.info("Failed to send message: %s" % exc)
-    raise SocketError(exc)
+    
+    # When sending there doesn't seem to be a reliable method for
+    # distinguishing between failures from a disconnect verses other things.
+    # Just accounting for known disconnection responses.
+    
+    if str(exc) == "[Errno 32] Broken pipe":
+      raise SocketClosed(exc)
+    else:
+      raise SocketError(exc)
   except AttributeError:
     # if the control_file has been closed then flush will receive:
     # AttributeError: 'NoneType' object has no attribute 'sendall'
diff --git a/test/integ/socket/__init__.py b/test/integ/socket/__init__.py
index d01630e..7479a48 100644
--- a/test/integ/socket/__init__.py
+++ b/test/integ/socket/__init__.py
@@ -2,5 +2,5 @@
 Integration tests for stem.socket.
 """
 
-__all__ = ["control_message"]
+__all__ = ["control_message", "control_socket"]
 
diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py
index 2eb2a6e..72f9174 100644
--- a/test/integ/socket/control_message.py
+++ b/test/integ/socket/control_message.py
@@ -31,20 +31,10 @@ class TestControlMessage(unittest.TestCase):
     self.assertEquals("514 Authentication required.\r\n", auth_required_response.raw_content())
     self.assertEquals([("514", " ", "Authentication required.")], auth_required_response.content())
     
-    # The socket's broken but doesn't realize it yet. Send another message and
-    # it should fail with a closed exception. With a control port we won't get
-    # an error until we read from the socket. However, with a control socket
-    # the write will cause a SocketError.
+    # The socket's broken but doesn't realize it yet. These use cases are
+    # checked in more depth by the ControlSocket integ tests.
     
     self.assertTrue(control_socket.is_alive())
-    
-    try:
-      control_socket.send("GETINFO version")
-    except: pass
-    
-    # at this point is_alive is True with a control port and False with a
-    # control socket
-    
     self.assertRaises(stem.socket.SocketClosed, control_socket.recv)
     self.assertFalse(control_socket.is_alive())
     
diff --git a/test/integ/socket/control_socket.py b/test/integ/socket/control_socket.py
new file mode 100644
index 0000000..920b161
--- /dev/null
+++ b/test/integ/socket/control_socket.py
@@ -0,0 +1,117 @@
+"""
+Integration tests for the stem.socket.ControlSocket subclasses. When ran these
+test basic functionality for a ControlPort *or* ControlSocketFile, depending on
+which can connect to our tor instance.
+
+These tests share a similar domain with those for the ControlMessage, but where
+those focus on parsing and correctness of the content these are more concerned
+with the behavior of the socket itself.
+"""
+
+import unittest
+
+import stem.connection
+import stem.socket
+import test.runner
+
+class TestControlSocket(unittest.TestCase):
+  def setUp(self):
+    test.runner.require_control(self)
+  
+  def test_send_buffered(self):
+    """
+    Sends multiple requests before receiving back any of the replies.
+    """
+    
+    runner = test.runner.get_runner()
+    tor_version = runner.get_tor_version()
+    
+    with runner.get_tor_socket() as control_socket:
+      for i in range(100):
+        control_socket.send("GETINFO version")
+      
+      for i in range(100):
+        response = control_socket.recv()
+        self.assertEquals("version=%s\nOK" % tor_version, str(response))
+  
+  def test_send_closed(self):
+    """
+    Sends a message after we've closed the connection.
+    """
+    
+    with test.runner.get_runner().get_tor_socket() as control_socket:
+      self.assertTrue(control_socket.is_alive())
+      control_socket.close()
+      self.assertFalse(control_socket.is_alive())
+      
+      self.assertRaises(stem.socket.SocketClosed, control_socket.send, "blarg")
+  
+  def test_send_disconnected(self):
+    """
+    Sends a message to a socket that has been disconnected by the other end.
+    
+    Our behavior upon disconnection slightly differs based on if we're a port
+    or socket file based connection. With a control port we won't notice the
+    disconnect (is_alive() will return True) until we've made a failed recv()
+    call. With a file socket, however, we'll also fail when calling send().
+    """
+    
+    with test.runner.get_runner().get_tor_socket() as control_socket:
+      control_socket.send("QUIT")
+      self.assertEquals("closing connection", str(control_socket.recv()))
+      self.assertTrue(control_socket.is_alive())
+      
+      # If we send another message to a port based socket then it will seem to
+      # succeed. However, a file based socket should report a failure.
+      
+      if isinstance(control_socket, stem.socket.ControlPort):
+        control_socket.send("blarg")
+        self.assertTrue(control_socket.is_alive())
+      else:
+        self.assertRaises(stem.socket.SocketClosed, control_socket.send, "blarg")
+        self.assertFalse(control_socket.is_alive())
+  
+  def test_recv_closed(self):
+    """
+    Receives a message after we've closed the connection.
+    """
+    
+    with test.runner.get_runner().get_tor_socket() as control_socket:
+      self.assertTrue(control_socket.is_alive())
+      control_socket.close()
+      self.assertFalse(control_socket.is_alive())
+      
+      self.assertRaises(stem.socket.SocketClosed, control_socket.recv)
+  
+  def test_recv_disconnected(self):
+    """
+    Receives a message from a socket that has been disconnected by the other
+    end.
+    """
+    
+    with test.runner.get_runner().get_tor_socket() as control_socket:
+      control_socket.send("QUIT")
+      self.assertEquals("closing connection", str(control_socket.recv()))
+      
+      # Neither a port or file based socket will know that tor has hung up on
+      # the connection at this point. We should know after calling recv(),
+      # however.
+      
+      self.assertTrue(control_socket.is_alive())
+      self.assertRaises(stem.socket.SocketClosed, control_socket.recv)
+      self.assertFalse(control_socket.is_alive())
+  
+  def test_connect_repeatedly(self):
+    """
+    Checks that we can reconnect, use, and disconnect a socket repeatedly.
+    """
+    
+    with test.runner.get_runner().get_tor_socket(False) as control_socket:
+      for i in range(10):
+        # this will raise if the PROTOCOLINFO query fails
+        stem.connection.get_protocolinfo(control_socket)
+        
+        control_socket.close()
+        self.assertRaises(stem.socket.SocketClosed, control_socket.send, "PROTOCOLINFO 1")
+        control_socket.connect()
+





More information about the tor-commits mailing list