Tearing up a bunch of stuff again. Trying to create decent tests.

Most of the santiago.py::Santiago stuff is irrelevant again.  Going test-first
is showing me a less terrible interface.
This commit is contained in:
Nick Daly 2012-03-17 13:29:54 -05:00
parent 650e7422fe
commit 7c5a8fcee0
3 changed files with 182 additions and 88 deletions

View File

@ -80,8 +80,18 @@ class SantiagoHttpListener(santiago.SantiagoListener):
class SantiagoHttpSender(santiago.SantiagoSender): class SantiagoHttpSender(santiago.SantiagoSender):
"""Responsible for answering HTTP requests.""" """Responsible for answering HTTP requests."""
import urllib, urllib2
@cherrypy.tools.json_out() @cherrypy.tools.json_out()
def proxy(self, key, service, server, hops=3): def proxy(self, key, service, server, hops=3):
"""Forwards on a request to another Santiago.""" """Forwards on a request to another Santiago."""
return super(SantiagoHttpSender, self).proxy(key, service, server, hops) return super(SantiagoHttpSender, self).proxy(key, service, server, hops)
@cherrypy.tools.json_out()
def send(self):
"""Send messages to other Santiagi."""
for message in super(SantiagoSender, self).send():
if message["location"].startswith("http"):
urllib2.Request(message["location"],urllib.urlencode(message))

View File

@ -67,10 +67,35 @@ class Santiago(object):
Santiago services. A Santiago has no idea of and is not responsible for Santiago services. A Santiago has no idea of and is not responsible for
anybody else's services. anybody else's services.
Serving-related variables:
:hosting: service to location mappings: This dictionary maps service
names to service locations.
:keys: key to service mappings: This dictionary maps keys to service
names.
Between the two, we can provide services for keys at particular
locations. These aren't necessarily services owned by this box, it
merely points the way and acts as a directory service.
Client-related variables:
:servers: This dual-key dictionary stores service: key: location
mappings, allowing for fast service-based lookup when I'm seeking
somebody to perform a specific service for me.
Both the client and server dictionaries can contain one another's data.
I'm not sure whether they should or not, yet. The data separation seems
valuable but perhaps highly over-engineered.
""" """
self.instance = instance self.instance = instance
# what I host where
self.hosting = self.load_dict("hosting") self.hosting = self.load_dict("hosting")
# who I host for
self.keys = self.load_dict("keys") self.keys = self.load_dict("keys")
# other folks
self.servers = self.load_dict("servers") self.servers = self.load_dict("servers")
self.listeners = list() self.listeners = list()
@ -101,30 +126,22 @@ class Santiago(object):
# Server-related tags # Server-related tags
# ------------------- # -------------------
def provide_at_location(self, service, location): def provide_service(self, key, service, location):
"""Serve service at location. """Serve a service for user at location.
post:: post::
location in self.hosting[service] location in self.hosting[service]
"""
self.hosting[service].append(location)
def provide_for_key(self, service, key):
"""Serve service for user.
post::
service in self.keys[key] service in self.keys[key]
""" """
self.keys[key].append(service) self.keys[key].append(service)
self.hosting[service].append(location)
# client-related tags # client-related tags
# ------------------- # -------------------
def learn_service(self, service, key, locations): def learn_service(self, key, service, locations):
"""Learn a service to use, as a client. """Learn a service to use, as a client.
post:: post::
@ -132,7 +149,7 @@ class Santiago(object):
forall(locations, lambda x: x in self.servers[service][key]) forall(locations, lambda x: x in self.servers[service][key])
""" """
self.servers[service][key] += locations self.servers[service][key].extend(locations)
def consume_service(self, service, key): def consume_service(self, service, key):
return self.servers[service][key] return self.servers[service][key]
@ -154,7 +171,7 @@ class Santiago(object):
"""Provide a requested service to a client.""" """Provide a requested service to a client."""
if santiagi is not None: if santiagi is not None:
self.learn_service("santiago", key, santiagi) self.learn_service(key, "santiago", santiagi)
if not self.am_i(server): if not self.am_i(server):
self.proxy(key, service, server, hops=hops) self.proxy(key, service, server, hops=hops)
@ -286,9 +303,6 @@ if __name__ == "__main__":
santiago.add_sender(http_sender) santiago.add_sender(http_sender)
# TODO move this into the table loading. # TODO move this into the table loading.
santiago.provide_at_location("wiki", "192.168.0.13") santiago.provide_service("james", "wiki", "192.168.0.13")
santiago.provide_for_key("wiki", "james")
santiago.max_hops = 3
santiago.proxy_list = ("tor")
cherrypy.quickstart(http_listener) cherrypy.quickstart(http_listener)

View File

@ -23,7 +23,25 @@ class SantiagoTest(unittest.TestCase):
"""The base class for tests.""" """The base class for tests."""
def setUp(self): def setUp(self):
self.santiago = santiago.Santiago("0x1") super(TestServing, self).setUp()
port_a = "localhost:9000"
port_b = "localhost:8000"
listeners_a = [santiago.SantiagoListener(port_a)]
senders_a = [santiago.SantiagoSender()]
listeners_b = [santiago.SantiagoListener(port_b)]
senders_b = [santiago.SantiagoSender()]
hosting_a = { "b": { "santiago": [ port_a ]}}
consuming_a = { "santiagao": { "b": [ port_b ]}}
hosting_b = { "a": { "santiago": [ port_b ],
"wiki": [ "localhost:8001" ]}}
consuming_b = { "santiagao": { "a": [ port_a ]}}
self.santiago_a = Santiago(listeners_a, senders_a, hosting_a, consuming_a)
self.santiago_b = Santiago(listeners_b, senders_b, hosting_b, consuming_b)
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
"""Add a poor man's forward compatibility.""" """Add a poor man's forward compatibility."""
@ -35,95 +53,147 @@ class SantiagoTest(unittest.TestCase):
if not a in b: if not a in b:
raise self.ContainsError("%s not in %s" % (a, b)) raise self.ContainsError("%s not in %s" % (a, b))
class TestDataModel(SantiagoTest): class TestServerInitialRequest(SantiagoTest):
"""Test adding and removing services and data.""" """Test how the Santiago server replies to initial service requests.
def test_add_listener(self): TODO: tests for:
"""Verify that we can add a listener."""
http_listener = SantiagoHttpListener(self.santiago, "localhost:8080")
self.santiago.add_listener(http_listener)
self.assertIn(http_listener, self.santiago.listeners)
def test_add_sender(self):
"""Verify that we can add a sender."""
http_sender = SantiagoHttpSender(self.santiago)
self.santiago.add_sender(http_sender)
self.assertIn(http_sender, self.santiago.senders)
def test_provide_at_location(self):
"""Verify that we can provide a service somewhere."""
service, location = ("something", "there")
self.santiago.provide_at_location(service, location)
self.assertIn(location, self.santiago.hosting[service])
def test_provide_for_key(self):
"""Verify we can provide a specific service to someone."""
service, key = ("something", "0x1")
self.santiago.provide_for_key(service, key)
self.assertIn(service, self.santiago.keys[key])
class TestServing(SantiagoTest):
"""TODO: tests for:
(normal serving + proxying) * (learning santiagi + not learning) (normal serving + proxying) * (learning santiagi + not learning)
""" """
def setUp(self): def test_acknowledgement(self):
super(TestServing, self).setUp() """If B receives an authorized request, then it replies with a location.
self.santiago.provide_at_location("wiki", "192.168.0.13") An "authorized request" in this case is for a service from a client that
self.santiago.provide_for_key("wiki", "0x2") B is willing to host that service for.
self.listener = santiago.SantiagoListener(self.santiago, "localhost:8080") In this case, B will answer with the wiki's location.
self.sender = santiago.SantiagoSender(self.santiago)
self.santiago.add_listener(self.listener)
self.santiago.add_sender(self.sender)
def test_successful_serve(self): """
"""Make sure we get an expected, successful serving message back.""" self.santiago_b.receive(from_=None, to=None,
client="a", host="b",
service="wiki", reply_to=None)
self.listener.serve("0x2", "wiki", "0x1", 0, None) self.assertEqual(self.santiago_b.outgoing_messages,
expected = [ [{"from": "b",
{ "to": "0x2", "to": "a",
"location": ["192.168.0.13"], "client": "a",
"reply-to": self.listener.location, }, ] "host": "b",
"service": "wiki",
"locations": ["192.168.0.13"],
"reply-to": "localhost:8000"}])
self.assertEqual(self.sender.send(), expected) def test_negative_acknowledgement_bad_service(self)
"""Does B reject requests for unsupported services?
In this case, B should reply with an empty list of locations.
"""
self.santiago_b.receive(from_=None, to=None,
client="a", host="b",
service="wiki", reply_to=None)
self.assertEqual(self.santiago_b.outgoing_messages,
[{"from": "b",
"to": "a",
"client": "a",
"host": "b",
"service": "wiki",
"locations": [],
"reply-to": "localhost:8000"}])
def test_deny_bad_key(self):
"""If B receives a request from an unauthorized key, it does not reply.
An "unauthorized request" in this case is for a service from a client
that B does not trust. This is different than clients B hosts no
services for.
In this case, B will never answer the request.
"""
self.santiago_b.receive(from_=None, to=None,
client="z", host="b",
service="wiki", reply_to=None)
self.assertEqual(self.santiago_b.outgoing_messages, [])
def test_learn_santaigo(self):
"""Does B learn new Santiago locations from trusted requests?
If A sends B a request with a new Santiago location, B should learn it.
"""
self.santiago_b.receive(from_=None, to=None,
client="a", host="b",
service="wiki", reply_to="localhost:9001")
self.assertEqual(self.santiago_b.consuming["santiago"]["a"],
["localhost:9000", "localhost:9001"])
def test_handle_requests_once(self): def test_handle_requests_once(self):
"""Verify that we send each request out only once.""" """Verify that we reply to each request only once."""
self.listener.serve("0x2", "wiki", "0x1", 0, None) self.santiago_b.receive(from_=None, to=None,
self.sender.send() client="a", host="b",
service="wiki", reply_to=None)
self.santiago_b.process()
self.assertEqual(self.sender.send(), list()) self.assertEqual(self.santiago_b.outgoing_messages, [])
class TestConsuming(SantiagoTest): class TestClientInitialRequest(SantiagoTest):
"""TODO: tests for: """Does the client send a correctly formed request?
(learning services) In these tests, we're sending requests to a mock listener which merely
records that the requests were well-formed.
""" """
def setUp(self):
super(SantiagoTest, self).setUp()
import cherrypy
class RequestReceiver(object):
def index(self, *args, **kwargs):
self.args = *args
self.kwargs = **kwargs
index.exposed = True
self.socket_port = 8000
self.receiver = RequestReceiver()
cherrypy.quickstart(self.receiver)
def test_request(self):
"""Verify that A queues a properly formatted initial request."""
self.santiago_a.request(from_="a", to="b",
client="a", host="b",
service="wiki", reply_to="localhost:9001")
self.assertEqual(self.santiago_a.outgoing_messages,
[{ "from": "a", "to": "b",
"client": "a", "host": "b",
"service": "wiki", "reply-to": "localhost:9001"})]
def test_request(self):
"""Verify that A sends out a properly formatted initial request."""
self.santiago_a.request(from_="a", to="b",
client="a", host="b",
service="wiki", reply_to="localhost:9001")
self.santiago_a.process()
self.assertEqual(self.receiver.kwargs,
[{ "from": "a", "to": "b",
"client": "a", "host": "b",
"service": "wiki", "reply-to": "localhost:9001"})]
class TestServerInitialResponse(SantiagoTest):
pass pass
class TestInitialRequest(SantiagoTest): class TestClientInitialResponse(SantiagoTest):
"""Testing the initial request.
Does Santiago produce well-formed output when creating a service request?
"""
pass
class TestInitialResponse(SantiagoTest):
pass pass
class TestForwardedRequest(SantiagoTest): class TestForwardedRequest(SantiagoTest):