[tor-commits] [stem/master] Integration tests / fixes for types.ControlMessage

atagar at torproject.org atagar at torproject.org
Mon Oct 17 16:48:31 UTC 2011


commit 1b44b967e75b08d6f702ffa1507d8ad8c4980bda
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Oct 17 09:45:43 2011 -0700

    Integration tests / fixes for types.ControlMessage
    
    Adding integration tests for basic control port communication, exercising...
    - connection failure
    - bad commands
    - bad getinfo queries
    - general getinfo queries
    - setevent/basic event parsing
    
    This also includes fixes for a variety of issues found while testing.
---
 run_tests.py          |    4 +-
 stem/types.py         |   18 ++++-
 stem/util/system.py   |   53 ++++++++----
 test/integ/message.py |  234 +++++++++++++++++++++++++++++++++++++++++++++++++
 test/integ/system.py  |    1 -
 test/runner.py        |   29 ++++++-
 6 files changed, 314 insertions(+), 25 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index 6d62ab3..1741892 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -11,6 +11,7 @@ import unittest
 import test.runner
 import test.unit.message
 import test.unit.version
+import test.integ.message
 import test.integ.system
 
 from stem.util import enum, term
@@ -24,7 +25,8 @@ UNIT_TESTS = (("stem.types.ControlMessage", test.unit.message.TestMessageFunctio
               ("stem.types.Version", test.unit.version.TestVerionFunctions),
              )
 
-INTEG_TESTS = (("stem.util.system", test.integ.system.TestSystemFunctions),
+INTEG_TESTS = (("stem.types.ControlMessage", test.integ.message.TestMessageFunctions),
+               ("stem.util.system", test.integ.system.TestSystemFunctions),
               )
 
 # Configurations that the intergration tests can be ran with. Attributs are
diff --git a/stem/types.py b/stem/types.py
index 2339007..818b6d3 100644
--- a/stem/types.py
+++ b/stem/types.py
@@ -53,14 +53,28 @@ def read_message(control_file):
   
   while True:
     try: line = control_file.readline()
-    except socket.error, exc: raise ControlSocketClosed(exc)
+    except AttributeError, exc:
+      # if the control_file has been closed then we will receive:
+      # AttributeError: 'NoneType' object has no attribute 'recv'
+      
+      log.log(log.WARN, "ControlSocketClosed: socket file has been closed")
+      raise ControlSocketClosed("socket file has been closed")
+    except socket.error, exc:
+      log.log(log.WARN, "ControlSocketClosed: received an exception (%s)" % exc)
+      raise ControlSocketClosed(exc)
     
     raw_content += line
     
     # Parses the tor control lines. These are of the form...
     # <status code><divider><content>\r\n
     
-    if len(line) < 4:
+    if len(line) == 0:
+      # if the socket is disconnected then the readline() method will provide
+      # empty content
+      
+      log.log(log.WARN, "ControlSocketClosed: empty socket content")
+      raise ControlSocketClosed("Received empty socket content.")
+    elif len(line) < 4:
       log.log(log.WARN, "ProtocolError: line too short (%s)" % line)
       raise ProtocolError("Badly formatted reply line: too short")
     elif not re.match(r'^[a-zA-Z0-9]{3}[-+ ]', line):
diff --git a/stem/util/system.py b/stem/util/system.py
index 746eed4..5f7e9ca 100644
--- a/stem/util/system.py
+++ b/stem/util/system.py
@@ -3,6 +3,7 @@ Helper functions for working with the underlying system. These are mostly os
 dependent, only working on linux, osx, and bsd.
 """
 
+import re
 import os
 import time
 import subprocess
@@ -123,7 +124,7 @@ def get_pid(process_name, process_port = None):
   try:
     results = call("pgrep -x %s" % process_name)
     
-    if len(results) == 1 and len(results[0].split()) == 1:
+    if results and len(results) == 1 and len(results[0].split()) == 1:
       pid = results[0].strip()
       if pid.isdigit(): return int(pid)
   except IOError: pass
@@ -135,7 +136,7 @@ def get_pid(process_name, process_port = None):
   try:
     results = call("pidof %s" % process_name)
     
-    if len(results) == 1 and len(results[0].split()) == 1:
+    if results and len(results) == 1 and len(results[0].split()) == 1:
       pid = results[0].strip()
       if pid.isdigit(): return int(pid)
   except IOError: pass
@@ -145,12 +146,16 @@ def get_pid(process_name, process_port = None):
   
   if process_port:
     try:
-      results = call("netstat -npl | grep 127.0.0.1:%i" % process_port)
+      results = call("netstat -npl")
       
-      if len(results) == 1:
-        results = results[0].split()[6] # process field (ex. "7184/tor")
-        pid = results[:results.find("/")]
-        if pid.isdigit(): return int(pid)
+      # filters to results with our port (same as "grep 127.0.0.1:<port>")
+      if results:
+        results = [r for r in results if "127.0.0.1:%i" % process_port in r]
+        
+        if len(results) == 1:
+          results = results[0].split()[6] # process field (ex. "7184/tor")
+          pid = results[:results.find("/")]
+          if pid.isdigit(): return int(pid)
     except IOError: pass
   
   # attempts to resolve using ps, failing if:
@@ -160,7 +165,7 @@ def get_pid(process_name, process_port = None):
   try:
     results = call("ps -o pid -C %s" % process_name)
     
-    if len(results) == 2:
+    if results and len(results) == 2:
       pid = results[1].strip()
       if pid.isdigit(): return int(pid)
   except IOError: pass
@@ -175,11 +180,15 @@ def get_pid(process_name, process_port = None):
   
   if process_port:
     try:
-      results = call("sockstat -4l -P tcp -p %i | grep %s" % (process_port, process_name))
+      results = call("sockstat -4l -P tcp -p %i" % process_port)
       
-      if len(results) == 1 and len(results[0].split()) == 7:
-        pid = results[0].split()[2]
-        if pid.isdigit(): return int(pid)
+      # filters to results with our port (same as "grep <name>")
+      if results:
+        results = [r for r in results if process_name in r]
+        
+        if len(results) == 1 and len(results[0].split()) == 7:
+          pid = results[0].split()[2]
+          if pid.isdigit(): return int(pid)
     except IOError: pass
   
   # attempts to resolve via a ps command that works on mac/bsd (this and lsof
@@ -188,11 +197,15 @@ def get_pid(process_name, process_port = None):
   # - there are multiple instances
   
   try:
-    results = call("ps axc | egrep \" %s$\"" % process_name)
+    results = call("ps axc")
     
-    if len(results) == 1 and len(results[0].split()) > 0:
-      pid = results[0].split()[0]
-      if pid.isdigit(): return int(pid)
+    # filters to results with our port (same as "egrep ' <name>$'")
+    if results:
+      results = [r for r in results if r.endswith(" %s" % process_name)]
+      
+      if len(results) == 1 and len(results[0].split()) > 0:
+        pid = results[0].split()[0]
+        if pid.isdigit(): return int(pid)
   except IOError: pass
   
   # attempts to resolve via lsof, this should work on linux, mac, and bsd
@@ -202,8 +215,12 @@ def get_pid(process_name, process_port = None):
   # - there are multiple instances using the same port on different addresses
   
   try:
-    port_comp = str(process_port) if process_port else ""
-    results = call("lsof -wnPi | egrep \"^%s.*:%s\"" % (process_name, port_comp))
+    results = call("lsof -wnPi")
+    
+    # filters to results with our port (same as "egrep '^<name>.*:<port>'")
+    if results:
+      port_comp = str(process_port) if process_port else ""
+      results = [r for r in results if re.match("^%s.*:%s" % (process_name, port_comp), r)]
     
     # This can result in multiple entries with the same pid (from the query
     # itself). Checking all lines to see if they're in agreement about the pid.
diff --git a/test/integ/message.py b/test/integ/message.py
new file mode 100644
index 0000000..46f20d2
--- /dev/null
+++ b/test/integ/message.py
@@ -0,0 +1,234 @@
+"""
+Integration tests for the types.ControlMessage class.
+"""
+
+import re
+import socket
+import unittest
+
+import stem.types
+import test.runner
+
+class TestMessageFunctions(unittest.TestCase):
+  """
+  Exercises the 'types.ControlMessage' class with an actual tor instance.
+  """
+  
+  def test_unestablished_socket(self):
+    """
+    Checks message parsing when we have a valid but unauthenticated socket.
+    """
+    
+    control_socket, control_socket_file = self._get_control_socket(False)
+    
+    # If an unauthenticated connection gets a message besides AUTHENTICATE or
+    # PROTOCOLINFO then tor will give an 'Authentication required.' message and
+    # hang up.
+    
+    control_socket_file.write("GETINFO version\r\n")
+    control_socket_file.flush()
+    
+    auth_required_response = stem.types.read_message(control_socket_file)
+    self.assertEquals("Authentication required.", str(auth_required_response))
+    self.assertEquals(["Authentication required."], list(auth_required_response))
+    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.
+    
+    control_socket_file.write("GETINFO version\r\n")
+    control_socket_file.flush()
+    
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+    
+    # Additional socket usage should fail, and pulling more responses will fail
+    # with more closed exceptions.
+    
+    control_socket_file.write("GETINFO version\r\n")
+    self.assertRaises(socket.error, control_socket_file.flush)
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+    
+    # The socket connection is already broken so calling close shouldn't have
+    # an impact.
+    
+    control_socket.close()
+    control_socket_file.write("GETINFO version\r\n")
+    self.assertRaises(socket.error, control_socket_file.flush)
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+    
+    # Closing the file handler, however, will cause a different type of error.
+    
+    control_socket_file.close()
+    control_socket_file.write("GETINFO version\r\n")
+    
+    # receives: AttributeError: 'NoneType' object has no attribute 'sendall'
+    self.assertRaises(AttributeError, control_socket_file.flush)
+    
+    # receives: stem.types.ControlSocketClosed: socket file has been closed
+    self.assertRaises(stem.types.ControlSocketClosed, stem.types.read_message, control_socket_file)
+  
+  def test_invalid_command(self):
+    """
+    Parses the response for a command which doesn't exist.
+    """
+    
+    control_socket, control_socket_file = self._get_control_socket()
+    
+    control_socket_file.write("blarg\r\n")
+    control_socket_file.flush()
+    
+    unrecognized_command_response = stem.types.read_message(control_socket_file)
+    self.assertEquals('Unrecognized command "blarg"', str(unrecognized_command_response))
+    self.assertEquals(['Unrecognized command "blarg"'], list(unrecognized_command_response))
+    self.assertEquals('510 Unrecognized command "blarg"\r\n', unrecognized_command_response.raw_content())
+    self.assertEquals([('510', ' ', 'Unrecognized command "blarg"')], unrecognized_command_response.content())
+    
+    control_socket.close()
+    control_socket_file.close()
+  
+  def test_invalid_getinfo(self):
+    """
+    Parses the response for a GETINFO query which doesn't exist.
+    """
+    
+    control_socket, control_socket_file = self._get_control_socket()
+    
+    control_socket_file.write("GETINFO blarg\r\n")
+    control_socket_file.flush()
+    
+    unrecognized_key_response = stem.types.read_message(control_socket_file)
+    self.assertEquals('Unrecognized key "blarg"', str(unrecognized_key_response))
+    self.assertEquals(['Unrecognized key "blarg"'], list(unrecognized_key_response))
+    self.assertEquals('552 Unrecognized key "blarg"\r\n', unrecognized_key_response.raw_content())
+    self.assertEquals([('552', ' ', 'Unrecognized key "blarg"')], unrecognized_key_response.content())
+    
+    control_socket.close()
+    control_socket_file.close()
+  
+  def test_getinfo_config_file(self):
+    """
+    Parses the 'GETINFO config-file' response.
+    """
+    
+    runner = test.runner.get_runner()
+    torrc_dst = runner.get_torrc_path()
+    
+    control_socket, control_socket_file = self._get_control_socket()
+    
+    control_socket_file.write("GETINFO config-file\r\n")
+    control_socket_file.flush()
+    
+    config_file_response = stem.types.read_message(control_socket_file)
+    self.assertEquals("config-file=%s\nOK" % torrc_dst, str(config_file_response))
+    self.assertEquals(["config-file=%s" % torrc_dst, "OK"], list(config_file_response))
+    self.assertEquals("250-config-file=%s\r\n250 OK\r\n" % torrc_dst, config_file_response.raw_content())
+    self.assertEquals([("250", "-", "config-file=%s" % torrc_dst), ("250", " ", "OK")], config_file_response.content())
+    
+    control_socket.close()
+    control_socket_file.close()
+  
+  def test_getinfo_config_text(self):
+    """
+    Parses the 'GETINFO config-text' response.
+    """
+    
+    # We can't be certain of the order, and there may be extra config-text
+    # entries as per...
+    # https://trac.torproject.org/projects/tor/ticket/2362
+    #
+    # so we'll just check that the response is a superset of our config
+    
+    runner = test.runner.get_runner()
+    torrc_contents = []
+    
+    for line in runner.get_torrc_contents().split("\n"):
+      line = line.strip()
+      
+      if line and not line.startswith("#"):
+        torrc_contents.append(line)
+    
+    control_socket, control_socket_file = self._get_control_socket()
+    
+    control_socket_file.write("GETINFO config-text\r\n")
+    control_socket_file.flush()
+    
+    config_text_response = stem.types.read_message(control_socket_file)
+    
+    # the response should contain two entries, the first being a data response
+    self.assertEqual(2, len(list(config_text_response)))
+    self.assertEqual("OK", list(config_text_response)[1])
+    self.assertEqual(("250", " ", "OK"), config_text_response.content()[1])
+    self.assertTrue(config_text_response.raw_content().startswith("250+config-text=\r\n"))
+    self.assertTrue(config_text_response.raw_content().endswith("\r\n.\r\n250 OK\r\n"))
+    self.assertTrue(str(config_text_response).startswith("config-text=\n"))
+    self.assertTrue(str(config_text_response).endswith("\nOK"))
+    
+    for torrc_entry in torrc_contents:
+      self.assertTrue("\n%s\n" % torrc_entry in str(config_text_response))
+      self.assertTrue(torrc_entry in list(config_text_response)[0])
+      self.assertTrue("%s\r\n" % torrc_entry in config_text_response.raw_content())
+      self.assertTrue("%s" % torrc_entry in config_text_response.content()[0][2])
+    
+    control_socket.close()
+    control_socket_file.close()
+  
+  def test_bw_event(self):
+    """
+    Issues 'SETEVENTS BW' and parses a few events.
+    """
+    
+    control_socket, control_socket_file = self._get_control_socket()
+    
+    control_socket_file.write("SETEVENTS BW\r\n")
+    control_socket_file.flush()
+    
+    setevents_response = stem.types.read_message(control_socket_file)
+    self.assertEquals("OK", str(setevents_response))
+    self.assertEquals(["OK"], list(setevents_response))
+    self.assertEquals("250 OK\r\n", setevents_response.raw_content())
+    self.assertEquals([("250", " ", "OK")], setevents_response.content())
+    
+    # Tor will emit a BW event once per second. Parsing three of them.
+    
+    for _ in range(3):
+      bw_event = stem.types.read_message(control_socket_file)
+      self.assertTrue(re.match("BW [0-9]+ [0-9]+", str(bw_event)))
+      self.assertTrue(re.match("650 BW [0-9]+ [0-9]+\r\n", bw_event.raw_content()))
+      self.assertEquals(("650", " "), bw_event.content()[0][:2])
+    
+    control_socket.close()
+    control_socket_file.close()
+  
+  def _get_control_socket(self, authenticate = True):
+    """
+    Provides a socket connected to the tor test instance's control port.
+    
+    Arguments:
+      authenticate (bool) - if True then the socket is authenticated
+    
+    Returns:
+      (socket.socket, file) tuple with the control socket and its file
+    """
+    
+    runner = test.runner.get_runner()
+    
+    control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    control_socket.connect(("127.0.0.1", runner.get_control_port()))
+    control_socket_file = control_socket.makefile()
+    
+    if authenticate:
+      control_socket_file.write("AUTHENTICATE\r\n")
+      control_socket_file.flush()
+      
+      authenticate_response = stem.types.read_message(control_socket_file)
+      
+      self.assertEquals("OK", str(authenticate_response))
+      self.assertEquals(["OK"], list(authenticate_response))
+      self.assertEquals("250 OK\r\n", authenticate_response.raw_content())
+      self.assertEquals([("250", " ", "OK")], authenticate_response.content())
+    
+    return (control_socket, control_socket_file)
+
diff --git a/test/integ/system.py b/test/integ/system.py
index 8661660..1fcb3d2 100644
--- a/test/integ/system.py
+++ b/test/integ/system.py
@@ -37,7 +37,6 @@ class TestSystemFunctions(unittest.TestCase):
     """
     
     runner = test.runner.get_runner()
-    self.assertEquals(runner.get_pid(), system.get_pid("tor"))
     self.assertEquals(runner.get_pid(), system.get_pid("tor", runner.get_control_port()))
     self.assertEquals(None, system.get_pid("blarg_and_stuff"))
   
diff --git a/test/runner.py b/test/runner.py
index b3abdfa..6f76d01 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -5,6 +5,7 @@ Runtime context for the integration tests.
 import os
 import sys
 import time
+import shutil
 import signal
 import tempfile
 import subprocess
@@ -12,10 +13,11 @@ import subprocess
 from stem.util import term
 
 # number of seconds before we time out our attempt to start a tor instance
-TOR_INIT_TIMEOUT = 60
+TOR_INIT_TIMEOUT = 90
 
 BASIC_TORRC = """# configuration for stem integration tests
 DataDirectory %s
+SocksPort 0
 ControlPort 1111
 """
 
@@ -57,7 +59,7 @@ class Runner:
       raise exc
     
     # writes our testing torrc
-    torrc_dst = os.path.join(self._test_dir, "torrc")
+    torrc_dst = self.get_torrc_path()
     try:
       sys.stdout.write(term.format("  writing torrc (%s)... " % torrc_dst, term.Color.BLUE, term.Attr.BOLD))
       
@@ -93,7 +95,7 @@ class Runner:
       if self._tor_process: self._tor_process.kill()
       
       # double check that we have a torrc to work with
-      torrc_dst = os.path.join(self._test_dir, "torrc")
+      torrc_dst = self.get_torrc_path()
       if not os.path.exists(torrc_dst):
         raise OSError("torrc doesn't exist (%s)" % torrc_dst)
       
@@ -139,6 +141,7 @@ class Runner:
       self._tor_process.kill()
       self._tor_process.communicate() # blocks until the process is done
       self._tor_process = None
+      shutil.rmtree(self._test_dir, ignore_errors=True)
       sys.stdout.write(term.format("done\n", term.Color.BLUE, term.Attr.BOLD))
   
   def get_pid(self):
@@ -164,4 +167,24 @@ class Runner:
     
     # TODO: this will be fetched from torrc contents when we use custom configs
     return 1111
+  
+  def get_torrc_path(self):
+    """
+    Provides the absolute path for where our testing torrc resides.
+    
+    Returns:
+      str with our torrc path
+    """
+    
+    return os.path.join(self._test_dir, "torrc")
+  
+  def get_torrc_contents(self):
+    """
+    Provides the contents of our torrc.
+    
+    Returns:
+      str with the contents of our torrc, lines are newline separated
+    """
+    
+    return self._torrc_contents
 



More information about the tor-commits mailing list