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.
This commit is contained in:
Damian Johnson 2011-10-17 09:45:43 -07:00
parent 24d4881025
commit 1b44b967e7
6 changed files with 314 additions and 25 deletions

View File

@ -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

View File

@ -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):

View File

@ -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.

234
test/integ/message.py Normal file
View File

@ -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)

View File

@ -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"))

View File

@ -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