[tor-commits] [stem/master] Pass-through BaseController class

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


commit a500dbc23d7b3f012a2040d104c3d009b36b3694
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Feb 4 20:23:02 2012 -0800

    Pass-through BaseController class
    
    Controller will be a ControlSocket subclass so making it a pass-through to
    the socket they're constructed from to start with. Testing this turned out to
    be a pita for a couple reasons...
    
    - The mock() function can't handle class methods, nor is it possable to add
      method handling to it.
    
    - Ideally the controller would dynamically pick its parent class based on the
      socket it's constructed from. However, this is impossable (actually, I don't
      know of any language where instances can dynamically define their class
      hierarchy). This mostly just becomes an issue for isinstance checks.
    
    Dropping the old, scrap controller implementation for now. Some of its useful
    bits will come back later.
---
 run_tests.py                          |    2 +
 stem/__init__.py                      |    2 +-
 stem/control.py                       |  169 +++++----------------------------
 test/integ/control/__init__.py        |    6 +
 test/integ/control/base_controller.py |   44 +++++++++
 test/integ/socket/control_socket.py   |    8 ++-
 test/mocking.py                       |   39 +++++++-
 7 files changed, 123 insertions(+), 147 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index 832cd1f..25cd0f2 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -24,6 +24,7 @@ import test.unit.version
 import test.integ.connection.authentication
 import test.integ.connection.connect
 import test.integ.connection.protocolinfo
+import test.integ.control.base_controller
 import test.integ.socket.control_message
 import test.integ.socket.control_socket
 import test.integ.util.conf
@@ -93,6 +94,7 @@ INTEG_TESTS = (
   test.integ.connection.protocolinfo.TestProtocolInfo,
   test.integ.connection.authentication.TestAuthenticate,
   test.integ.connection.connect.TestConnect,
+  test.integ.control.base_controller.TestBaseController,
 )
 
 def load_user_configuration(test_config):
diff --git a/stem/__init__.py b/stem/__init__.py
index 542c82d..8a914fa 100644
--- a/stem/__init__.py
+++ b/stem/__init__.py
@@ -2,5 +2,5 @@
 Library for working with the tor process.
 """
 
-__all__ = ["connection", "process", "socket", "version"]
+__all__ = ["connection", "control", "process", "socket", "version"]
 
diff --git a/stem/control.py b/stem/control.py
index 3d55b62..d1f30ee 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -1,158 +1,41 @@
-# The following is very much a work in progress and mostly scratch (I just
-# wanted to make sure other work would nicely do the async event handling).
+"""
+Classes for interacting with the tor control socket.
 
-import Queue
-import threading
+Controllers are a wrapper around a ControlSocket, retaining its low-level
+connection methods (send, recv, is_alive, etc) in addition to providing methods
+for interacting at a higher level.
+"""
 
 import stem.socket
 
-class ControlConnection:
+class BaseController(stem.socket.ControlSocket):
   """
-  Connection to a Tor control port. This is a very lightweight wrapper around
-  the socket, providing basic process communication and event listening. Don't
-  use this directly - subclasses provide friendlier controller access.
+  Controller for the tor process. This is a minimal base class for other
+  controllers, providing basic process communication and event listing. Don't
+  use this directly - subclasses provide higher level functionality.
+  
+  Attributes:
+    socket - ControlSocket from which this was constructed
   """
   
   def __init__(self, control_socket):
-    self._is_running = True
-    self._control_socket = control_socket
-    
-    # File accessor for far better sending and receiving functionality. This
-    # uses a duplicate file descriptor so both this and the socket need to be
-    # closed when done.
-    
-    self._control_socket_file = self._control_socket.makefile()
-    
-    # queues where messages from the control socket are directed
-    self._event_queue = Queue.Queue()
-    self._reply_queue = Queue.Queue()
-    
-    # prevents concurrent writing to the socket
-    self._socket_write_cond = threading.Condition()
-    
-    # thread to pull from the _event_queue and call handle_event
-    self._event_cond = threading.Condition()
-    self._event_thread = threading.Thread(target = self._event_loop)
-    self._event_thread.setDaemon(True)
-    self._event_thread.start()
-    
-    # thread to continually pull from the control socket
-    self._reader_thread = threading.Thread(target = self._reader_loop)
-    self._reader_thread.setDaemon(True)
-    self._reader_thread.start()
-  
-  def is_running(self):
-    """
-    True if we still have an open connection to the control socket, false
-    otherwise.
-    """
-    
-    return self._is_running
+    self.socket = control_socket
   
-  def handle_event(self, event_message):
-    """
-    Overwritten by subclasses to provide event listening. This is notified
-    whenever we receive an event from the control socket.
-    
-    Arguments:
-      event_message (stem.socket.ControlMessage) -
-          message received from the control socket
-    """
-    
-    pass
+  def send(self, message, raw = False):
+    self.socket.send(message, raw)
   
-  def send(self, message):
-    """
-    Sends a message to the control socket and waits for a reply.
-    
-    Arguments:
-      message (str) - message to be sent to the control socket
-    
-    Returns:
-      stem.socket.ControlMessage with the response from the control socket
-    """
-    
-    # makes sure that the message ends with a CRLF
-    message = message.rstrip("\r\n") + "\r\n"
-    
-    self._socket_write_cond.acquire()
-    self._control_socket_file.write(message)
-    self._control_socket_file.flush()
-    self._socket_write_cond.release()
-    
-    return self._reply_queue.get()
+  def recv(self):
+    return self.socket.recv()
   
-  def _event_loop(self):
-    """
-    Continually pulls messages from the _event_thread and sends them to
-    handle_event. This is done via its own thread so subclasses with a lengthy
-    handle_event implementation don't block further reading from the socket.
-    """
-    
-    while self.is_running():
-      try:
-        event_message = self._event_queue.get_nowait()
-        self.handle_event(event_message)
-      except Queue.Empty:
-        self._event_cond.acquire()
-        self._event_cond.wait()
-        self._event_cond.release()
+  def is_alive(self):
+    return self.socket.is_alive()
   
-  def _reader_loop(self):
-    """
-    Continually pulls from the control socket, directing the messages into
-    queues based on their type. Controller messages come in two varieties...
-    
-    - Responses to messages we've sent (GETINFO, SETCONF, etc).
-    - Asynchronous events, identified by a status code of 650.
-    """
-    
-    while self.is_running():
-      try:
-        # TODO: this raises a SocketClosed when... well, the socket is closed
-        control_message = stem.socket.recv_message(self._control_socket_file)
-        
-        if control_message.content()[-1][0] == "650":
-          # adds this to the event queue and wakes up the handler
-          
-          self._event_cond.acquire()
-          self._event_queue.put(control_message)
-          self._event_cond.notifyAll()
-          self._event_cond.release()
-        else:
-          # TODO: figure out a good method for terminating the socket thread
-          self._reply_queue.put(control_message)
-      except stem.socket.ProtocolError, exc:
-        LOGGER.error("Error reading control socket message: %s" % exc)
-        # TODO: terminate?
+  def connect(self):
+    self.socket.connect()
   
   def close(self):
-    """
-    Terminates the control connection.
-    """
-    
-    self._is_running = False
-    
-    # if we haven't yet established a connection then this raises an error
-    # socket.error: [Errno 107] Transport endpoint is not connected
-    try: self._control_socket.shutdown(socket.SHUT_RDWR)
-    except socket.error: pass
-    
-    self._control_socket.close()
-    self._control_socket_file.close()
-    
-    # wake up the event thread so it can terminate
-    self._event_cond.acquire()
-    self._event_cond.notifyAll()
-    self._event_cond.release()
-    
-    self._event_thread.join()
-    self._reader_thread.join()
-
-# temporary function for getting a connection
-def test_connection():
-  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-  s.connect(("127.0.0.1", 9051))
-  return ControlConnection(s)
-
+    self.socket.close()
+  
+  def _make_socket(self):
+    self._control_socket._make_socket()
 
diff --git a/test/integ/control/__init__.py b/test/integ/control/__init__.py
new file mode 100644
index 0000000..715082f
--- /dev/null
+++ b/test/integ/control/__init__.py
@@ -0,0 +1,6 @@
+"""
+Integration tests for stem.control.
+"""
+
+__all__ = ["base_controller"]
+
diff --git a/test/integ/control/base_controller.py b/test/integ/control/base_controller.py
new file mode 100644
index 0000000..467c057
--- /dev/null
+++ b/test/integ/control/base_controller.py
@@ -0,0 +1,44 @@
+"""
+Integration tests for the stem.control.BaseController class.
+"""
+
+import unittest
+
+import stem.control
+import test.runner
+import test.mocking as mocking
+import test.integ.socket.control_socket
+
+class TestBaseController(unittest.TestCase):
+  def setUp(self):
+    test.runner.require_control(self)
+  
+  def tearDown(self):
+    mocking.revert_mocking()
+  
+  def test_socket_passthrough(self):
+    """
+    The BaseController is a passthrough for the socket it is built from, so
+    runs the ControlSocket integ tests again against it.
+    """
+    
+    # overwrites the Runner's get_tor_socket() to provide back a ControlSocket
+    # wrapped by a BaseContorller
+    
+    def mock_get_tor_socket(self, authenticate = True):
+      real_get_tor_socket = mocking.get_real_function(test.runner.Runner.get_tor_socket)
+      control_socket = real_get_tor_socket(self, authenticate)
+      return stem.control.BaseController(control_socket)
+    
+    mocking.mock_method(test.runner.Runner, "get_tor_socket", mock_get_tor_socket)
+    
+    # sanity check that the mocking is working
+    example_socket = test.runner.get_runner().get_tor_socket()
+    self.assertTrue(isinstance(example_socket, stem.control.BaseController))
+    
+    # re-runs all of the control_socket tests
+    socket_test_class = test.integ.socket.control_socket.TestControlSocket
+    for method in socket_test_class.__dict__:
+      if method.startswith("test_"):
+        socket_test_class.__dict__[method](self)
+
diff --git a/test/integ/socket/control_socket.py b/test/integ/socket/control_socket.py
index 920b161..3a4aaeb 100644
--- a/test/integ/socket/control_socket.py
+++ b/test/integ/socket/control_socket.py
@@ -11,6 +11,7 @@ with the behavior of the socket itself.
 import unittest
 
 import stem.connection
+import stem.control
 import stem.socket
 import test.runner
 
@@ -64,7 +65,12 @@ class TestControlSocket(unittest.TestCase):
       # 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):
+      if isinstance(control_socket, stem.control.BaseController):
+        base_socket = control_socket.socket
+      else:
+        base_socket = control_socket
+      
+      if isinstance(base_socket, stem.socket.ControlPort):
         control_socket.send("blarg")
         self.assertTrue(control_socket.is_alive())
       else:
diff --git a/test/mocking.py b/test/mocking.py
index 1e98cef..e5f46be 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -81,6 +81,41 @@ def mock(target, mock_call):
   # mocks the function with this wrapper
   target_module.__dict__[target_function] = mock_wrapper
 
+def mock_method(target_class, method_name, mock_call):
+  """
+  Mocks the given class method in a similar fasion as what mock() does for
+  functions.
+  
+  Arguments:
+    target_class (class) - class with the method we want to mock
+    method_name (str)    - name of the method to be mocked
+    mock_call (functor)  - mocking to replace the method with
+  """
+  
+  # Ideally callers could call us with just the method, for instance like...
+  #   mock_method(MyClass.foo, mocking.return_true())
+  #
+  # However, while classes reference the methods they have the methods
+  # themselves don't reference the class. This is unfortunate because it means
+  # that we need to know both the class and method we're replacing.
+  
+  target_method = target_class.__dict__[method_name]
+  
+  if "mock_id" in target_method.__dict__:
+    # we're overriding an already mocked method
+    mocking_id = target_method.mock_id
+    _, target_method, _ = MOCK_STATE[mocking_id]
+  else:
+    # this is a new mocking, save the original state
+    mocking_id = MOCK_ID.next()
+    MOCK_STATE[mocking_id] = (target_class, method_name, target_method)
+  
+  mock_wrapper = lambda *args: mock_call(*args)
+  mock_wrapper.__dict__["mock_id"] = mocking_id
+  
+  # mocks the function with this wrapper
+  target_class.__dict__[method_name] = mock_wrapper
+
 def revert_mocking():
   """
   Reverts any mocking done by this function.
@@ -102,8 +137,8 @@ def revert_mocking():
 
 def get_real_function(function):
   """
-  Provides the original, non-mocked implementation for a function. This simply
-  returns the current implementation if it isn't being mocked.
+  Provides the original, non-mocked implementation for a function or method.
+  This simply returns the current implementation if it isn't being mocked.
   
   Arguments:
     function (function) - function to look up the original implementation of



More information about the tor-commits mailing list