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):
"""Responsible for answering HTTP requests."""
import urllib, urllib2
@cherrypy.tools.json_out()
def proxy(self, key, service, server, hops=3):
"""Forwards on a request to another Santiago."""
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
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
# what I host where
self.hosting = self.load_dict("hosting")
# who I host for
self.keys = self.load_dict("keys")
# other folks
self.servers = self.load_dict("servers")
self.listeners = list()
@ -101,30 +126,22 @@ class Santiago(object):
# Server-related tags
# -------------------
def provide_at_location(self, service, location):
"""Serve service at location.
def provide_service(self, key, service, location):
"""Serve a service for user at location.
post::
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]
"""
self.keys[key].append(service)
self.hosting[service].append(location)
# 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.
post::
@ -132,7 +149,7 @@ class Santiago(object):
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):
return self.servers[service][key]
@ -154,7 +171,7 @@ class Santiago(object):
"""Provide a requested service to a client."""
if santiagi is not None:
self.learn_service("santiago", key, santiagi)
self.learn_service(key, "santiago", santiagi)
if not self.am_i(server):
self.proxy(key, service, server, hops=hops)
@ -286,9 +303,6 @@ if __name__ == "__main__":
santiago.add_sender(http_sender)
# TODO move this into the table loading.
santiago.provide_at_location("wiki", "192.168.0.13")
santiago.provide_for_key("wiki", "james")
santiago.max_hops = 3
santiago.proxy_list = ("tor")
santiago.provide_service("james", "wiki", "192.168.0.13")
cherrypy.quickstart(http_listener)

View File

@ -23,7 +23,25 @@ class SantiagoTest(unittest.TestCase):
"""The base class for tests."""
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):
"""Add a poor man's forward compatibility."""
@ -35,95 +53,147 @@ class SantiagoTest(unittest.TestCase):
if not a in b:
raise self.ContainsError("%s not in %s" % (a, b))
class TestDataModel(SantiagoTest):
"""Test adding and removing services and data."""
class TestServerInitialRequest(SantiagoTest):
"""Test how the Santiago server replies to initial service requests.
def test_add_listener(self):
"""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:
TODO: tests for:
(normal serving + proxying) * (learning santiagi + not learning)
"""
def setUp(self):
super(TestServing, self).setUp()
def test_acknowledgement(self):
"""If B receives an authorized request, then it replies with a location.
self.santiago.provide_at_location("wiki", "192.168.0.13")
self.santiago.provide_for_key("wiki", "0x2")
An "authorized request" in this case is for a service from a client that
B is willing to host that service for.
self.listener = santiago.SantiagoListener(self.santiago, "localhost:8080")
self.sender = santiago.SantiagoSender(self.santiago)
self.santiago.add_listener(self.listener)
self.santiago.add_sender(self.sender)
In this case, B will answer with the wiki's location.
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)
expected = [
{ "to": "0x2",
"location": ["192.168.0.13"],
"reply-to": self.listener.location, }, ]
self.assertEqual(self.santiago_b.outgoing_messages,
[{"from": "b",
"to": "a",
"client": "a",
"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):
"""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.sender.send()
self.santiago_b.receive(from_=None, to=None,
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):
"""TODO: tests for:
class TestClientInitialRequest(SantiagoTest):
"""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
class TestInitialRequest(SantiagoTest):
"""Testing the initial request.
Does Santiago produce well-formed output when creating a service request?
"""
pass
class TestInitialResponse(SantiagoTest):
class TestClientInitialResponse(SantiagoTest):
pass
class TestForwardedRequest(SantiagoTest):