[tor-commits] [stem/master] Revisions for prior mocking and controller changes

atagar at torproject.org atagar at torproject.org
Sun Dec 30 07:39:49 UTC 2012


commit 79fc8cd7514cda072abf855b6a1908f0dd10bb17
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Dec 29 23:27:06 2012 -0800

    Revisions for prior mocking and controller changes
    
    Changes include...
    
    * Rewrite of get_socks_ports() with the following changes...
    
      * Renaming to get_socks_listeners() since it provides (address, port) tuples,
        not just ports.
    
      * Better error handling. Previously malformed content such as invalid ports
        would cause us to raise ValueErrors.
    
      * The method wasn't documented as raising any exceptions, but if the
        get_conf() calls fail then a ControllerError will be raised.
    
    * Minor corrections to the return_for_args() examples.
    
    * Renamed return_for_args()'s "method" arg to "is_method" (it's nice when the
      an arg name can imply the type).
    
    * I'm not sure what was meant by "Also, remove the the space in the join so
      that there is only one parameter passed to the string substitution.".
      Personally I'd find a message like...
    
      "Unrecognized argument sent for return_for_args(). Got 'foo, bar' but we only
      recognize 'foo, blarg'."
    
      ... to be a bit nicer than...
    
      "Unrecognized argument sent for return_for_args(). Got 'foo, bar' but we only
      recognize 'foo;blarg'."
---
 stem/control.py                  |   53 ++++++++++++++++++---------
 test/integ/control/controller.py |    2 +-
 test/mocking.py                  |   68 +++++++++++++++++------------------
 test/unit/control/controller.py  |   72 +++++++++++++++++++++++++++++--------
 test/unit/tutorial.py            |    2 +-
 5 files changed, 126 insertions(+), 71 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index 8a36285..704bb5e 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -26,6 +26,7 @@ providing its own for interacting at a higher level.
     |- set_options - sets or resets the values of multiple configuration options
     |- load_conf - loads configuration information as if it was in the torrc
     |- save_conf - saves configuration information to the torrc
+    |- get_socks_listeners - provides (address, port) where tor is listening for SOCKS connections
     |- is_feature_enabled - checks if a given controller feature is enabled
     |- enable_feature - enables a controller feature that has been disabled by default
     |- signal - sends a signal to the tor client
@@ -1320,32 +1321,48 @@ class Controller(BaseController):
     else:
       raise stem.ProtocolError("SAVECONF returned unexpected response code")
   
-  def get_socks_ports(self):
+  def get_socks_listeners(self):
     """
-    Returns a list of SOCKS proxy ports open on the controlled tor instance.
+    Provides the SOCKS **(address, port)** tuples that tor has open.
     
-    :returns: list of **(host, port)** tuples or an empty list if there are no
-      SOCKS listeners
+    :returns: list of **(address, port)** tuples for the available SOCKS
+      listeners
+    
+    :raises: :class:`stem.ControllerError` if unable to determine the listeners
     """
     
+    proxy_addrs = []
+    
     try:
-      raw_addrs = self.get_info("net/listeners/socks").split()
-      # remove the surrounding quotes from each listener
-      raw_addrs = [x.replace("\"", "") for x in raw_addrs]
+      for listener in self.get_info("net/listeners/socks").split():
+        if not (listener.startswith('"') and listener.endswith('"')):
+          raise stem.ProtocolError("'GETINFO net/listeners/socks' responses are expected to be quoted: %s" % listener)
+        elif not ':' in listener:
+          raise stem.ProtocolError("'GETINFO net/listeners/socks' had a listener without a colon: %s" % listener)
+        
+        listener = listener[1:-1] # strip quotes
+        addr, port = listener.split(':')
+        proxy_addrs.append((addr, port))
     except stem.InvalidArguments:
-      # tor version is old (pre-tor-0.2.2.26-beta); use get_conf()
+      # tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead
       socks_port = self.get_conf('SocksPort')
-      raw_addrs = []
+      
       for listener in self.get_conf('SocksListenAddress', multiple = True):
-        if listener.count(':') == 0:
-          listener = listener + ":" + socks_port
-        raw_addrs.append(listener)
-    # both processes above give a list of strings of the form host:port
-    proxy_addrs = []
-    for proxy in raw_addrs:
-      proxy_pair = proxy.split(":")
-      proxy_addrs.append(tuple((proxy_pair[0], int(proxy_pair[1]))))
-    return proxy_addrs
+        if ':' in listener:
+          addr, port = listener.split(':')
+          proxy_addrs.append((addr, port))
+        else:
+          proxy_addrs.append((listener, socks_port))
+    
+    # validate that address/ports are valid, and convert ports to ints
+    
+    for addr, port in proxy_addrs:
+      if not stem.util.connection.is_valid_ip_address(addr):
+        raise stem.ProtocolError("Invalid address for a SOCKS listener: %s" % addr)
+      elif not stem.util.connection.is_valid_port(port):
+        raise stem.ProtocolError("Invalid port for a SOCKS listener: %s" % port)
+    
+    return [(addr, int(port)) for (addr, port) in proxy_addrs]
   
   def is_feature_enabled(self, feature):
     """
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index 5c146e0..86056aa 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -467,7 +467,7 @@ class TestController(unittest.TestCase):
     runner = test.runner.get_runner()
     
     with runner.get_tor_controller() as controller:
-      self.assertEqual([('127.0.0.1', 1112)], controller.get_socks_ports())
+      self.assertEqual([('127.0.0.1', 1112)], controller.get_socks_listeners())
   
   def test_enable_feature(self):
     """
diff --git a/test/mocking.py b/test/mocking.py
index 074c977..a86b958 100644
--- a/test/mocking.py
+++ b/test/mocking.py
@@ -204,63 +204,61 @@ def return_true(): return return_value(True)
 def return_false(): return return_value(False)
 def return_none(): return return_value(None)
 
-def return_for_args(args_to_return_value, default = None, method = False):
+def return_for_args(args_to_return_value, default = None, is_method = False):
   """
   Returns a value if the arguments to it match something in a given
   'argument => return value' mapping. Otherwise, a default function
   is called with the arguments.
   
-  The mapped argument is a tuple (not a list) of parameters to a function (or
-  method). Positional arguments must be in the order used to call the mocked
-  function and keyword arguments must be strings of the form 'k=v' in
-  alphabetical order.  Some examples:
+  The mapped argument is a tuple (not a list) of parameters to a function or
+  method. Positional arguments must be in the order used to call the mocked
+  function, and keyword arguments must be strings of the form 'k=v'. Keyword
+  arguments **must** appear in alphabetical order. For example...
   
   ::
   
-    Mocked functions can return different types depending on input:
+    mocking.mock("get_answer", mocking.return_for_args({
+      ("breakfast_menu",): "spam",
+      ("lunch_menu",): "eggs and spam",
+      (42,): ["life", "universe", "everything"],
+    }))
     
-      mocking.mock("get_answer", mocking.return_for_args({
-        ("breakfast_menu",): "spam",
-        ("lunch_menu",): "eggs and spam",
-        (42,): ["life", "universe", "everything"],
-      })
-      
-      mocking.mock("align_text", {
-        ("Stem", "alignment=left", "size=10"):   "Stem      ",
-        ("Stem", "alignment=center", "size=10"): "   Stem   ",
-        ("Stem", "alignment=right", "size=10"):  "      Stem",
-      })
-    
-    The mocked method returns one of three circuit ids depending on the input:
-    
-    ::
+    mocking.mock("align_text", mocking.return_for_args({
+      ("Stem", "alignment=left", "size=10"):   "Stem      ",
+      ("Stem", "alignment=center", "size=10"): "   Stem   ",
+      ("Stem", "alignment=right", "size=10"):  "      Stem",
+    }))
     
-      mocking.mock_method(Controller, "new_circuit", mocking.return_for_args({
-        (): "1",
-        ("path=['718BCEA286B531757ACAFF93AE04910EA73DE617', " + \
-          "'30BAB8EE7606CBD12F3CC269AE976E0153E7A58D', " + \
-          "'2765D8A8C4BBA3F89585A9FFE0E8575615880BEB']",): "2"
-        ("path=['1A', '2B', '3C']", "purpose=controller"): "3"
-      }, method = True)
+    mocking.mock_method(Controller, "new_circuit", mocking.return_for_args({
+      (): "1",
+      ("path=['718BCEA286B531757ACAFF93AE04910EA73DE617', " + \
+        "'30BAB8EE7606CBD12F3CC269AE976E0153E7A58D', " + \
+        "'2765D8A8C4BBA3F89585A9FFE0E8575615880BEB']",): "2"
+      ("path=['1A', '2B', '3C']", "purpose=controller"): "3"
+    }, is_method = True))
   
-  :param dict,tuple args_to_return_value: mapping of arguments to the value we should provide
-  :param functor default: returns the value of this function if the args don't match something that we have, we raise a ValueError by default
-  :param bool method: removes the 'self' reference before processing the remainder of the parameters
+  :param dict args_to_return_value: mapping of arguments to the value we should provide
+  :param functor default: returns the value of this function if the args don't
+    match something that we have, we raise a ValueError by default
+  :param bool is_method: handles this like a method, removing the 'self'
+    reference
   """
   
   def _return_value(*args, **kwargs):
-    # strip off the 'self' for mock classes
-    if args and method:
+    # strip off the 'self' if we're mocking a method
+    if args and is_method:
       args = args[1:] if len(args) > 2 else [args[1]]
     
     if kwargs:
-      args.extend(['='.join((str(k),str(kwargs[k]))) for k in sorted(kwargs.keys())])
+      args.extend(["%s=%s" % (k, kwargs[k]) for k in sorted(kwargs.keys())])
+    
     args = tuple(args)
+    
     if args in args_to_return_value:
       return args_to_return_value[args]
     elif default is None:
       arg_label = ", ".join([str(v) for v in args])
-      arg_keys = ";".join([str(v) for v in args_to_return_value.keys()])
+      arg_keys = ", ".join([str(v) for v in args_to_return_value.keys()])
       raise ValueError("Unrecognized argument sent for return_for_args(). Got '%s' but we only recognize '%s'." % (arg_label, arg_keys))
     else:
       return default(args)
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index f3849eb..695c2de 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -77,54 +77,94 @@ class TestControl(unittest.TestCase):
     # EventType.SIGNAL was added in tor version 0.2.3.1-alpha
     self.assertRaises(InvalidRequest, self.controller.add_event_listener, mocking.no_op(), EventType.SIGNAL)
   
-  def test_socks_port_old_tor(self):
+  def test_get_socks_listeners_old(self):
     """
-    Exercises the get_socks_ports method as if talking to an old tor process.
+    Exercises the get_socks_listeners() method as though talking to an old tor
+    instance.
     """
     
     # An old tor raises stem.InvalidArguments for get_info about socks, but
-    #  get_socks_ports returns the socks information, anyway.
+    # get_socks_listeners should work anyway.
+    
     mocking.mock_method(Controller, "get_info", mocking.raise_exception(InvalidArguments))
+    
     mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
       ("SocksPort",): "9050",
       ("SocksListenAddress", "multiple=True"): ["127.0.0.1"]
-    }, method = True))
-    self.assertEqual([('127.0.0.1', 9050)], self.controller.get_socks_ports())
+    }, is_method = True))
+    self.assertEqual([('127.0.0.1', 9050)], self.controller.get_socks_listeners())
     
     # Again, an old tor, but SocksListenAddress overrides the port number.
+    
     mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
       ("SocksPort",): "9050",
       ("SocksListenAddress", "multiple=True"): ["127.0.0.1:1112"]
-    }, method = True))
-    self.assertEqual([('127.0.0.1', 1112)], self.controller.get_socks_ports())
+    }, is_method = True))
+    self.assertEqual([('127.0.0.1', 1112)], self.controller.get_socks_listeners())
     
     # Again, an old tor, but multiple listeners
+    
     mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
       ("SocksPort",): "9050",
       ("SocksListenAddress", "multiple=True"): ["127.0.0.1:1112", "127.0.0.1:1114"]
-    }, method = True))
-    self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)], self.controller.get_socks_ports())
+    }, is_method = True))
+    self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)], self.controller.get_socks_listeners())
     
     # Again, an old tor, but no SOCKS listeners
+    
     mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
       ("SocksPort",): "0",
       ("SocksListenAddress", "multiple=True"): []
-    }, method = True))
-    self.assertEqual([], self.controller.get_socks_ports())
+    }, is_method = True))
+    self.assertEqual([], self.controller.get_socks_listeners())
+    
+    # Where tor provides invalid ports or addresses
+    
+    mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
+      ("SocksPort",): "blarg",
+      ("SocksListenAddress", "multiple=True"): ["127.0.0.1"]
+    }, is_method = True))
+    self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
+    
+    mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
+      ("SocksPort",): "0",
+      ("SocksListenAddress", "multiple=True"): ["127.0.0.1:abc"]
+    }, is_method = True))
+    self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
+    
+    mocking.mock_method(Controller, "get_conf", mocking.return_for_args({
+      ("SocksPort",): "40",
+      ("SocksListenAddress", "multiple=True"): ["500.0.0.1"]
+    }, is_method = True))
+    self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
   
-  def test_socks_port_new_tor(self):
+  def test_get_socks_listeners_new(self):
     """
-    Exercises the get_socks_ports method as if talking to a newer tor process.
+    Exercises the get_socks_listeners() method as if talking to a newer tor
+    instance.
     """
     
     # multiple SOCKS listeners
     mocking.mock_method(Controller, "get_info", mocking.return_value(
-      "\"127.0.0.1:1112\" \"127.0.0.1:1114\""
+      '"127.0.0.1:1112" "127.0.0.1:1114"'
     ))
     self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)],
-        self.controller.get_socks_ports())
+        self.controller.get_socks_listeners())
     
     # no SOCKS listeners
     mocking.mock_method(Controller, "get_info", mocking.return_value(""))
-    self.assertEqual([], self.controller.get_socks_ports())
+    self.assertEqual([], self.controller.get_socks_listeners())
+    
+    # check where GETINFO provides malformed content
+    
+    invalid_responses = (
+      '"127.0.0.1"',        # address only
+      '"1112"',             # port only
+      '"5127.0.0.1:1112"',  # invlaid address
+      '"127.0.0.1:991112"', # invalid port
+    )
+    
+    for response in invalid_responses:
+      mocking.mock_method(Controller, "get_info", mocking.return_value(response))
+      self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
 
diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py
index b586549..79baa8c 100644
--- a/test/unit/tutorial.py
+++ b/test/unit/tutorial.py
@@ -21,7 +21,7 @@ class TestTutorial(unittest.TestCase):
       'get_info': mocking.return_for_args({
         ('traffic/read',): '1234',
         ('traffic/written',): '5678',
-      }, method = True),
+      }, is_method = True),
     })
     
     controller.authenticate()





More information about the tor-commits mailing list