From 7c5a8fcee06773b680a4cbf30ccbef13e27692dc Mon Sep 17 00:00:00 2001 From: Nick Daly Date: Sat, 17 Mar 2012 13:29:54 -0500 Subject: [PATCH] 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. --- ugly_hacks/santiago/protocols/http.py | 10 ++ ugly_hacks/santiago/santiago.py | 50 +++--- ugly_hacks/santiago/test_santiago.py | 210 +++++++++++++++++--------- 3 files changed, 182 insertions(+), 88 deletions(-) diff --git a/ugly_hacks/santiago/protocols/http.py b/ugly_hacks/santiago/protocols/http.py index 783f11e01..df9b3a4a0 100644 --- a/ugly_hacks/santiago/protocols/http.py +++ b/ugly_hacks/santiago/protocols/http.py @@ -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)) diff --git a/ugly_hacks/santiago/santiago.py b/ugly_hacks/santiago/santiago.py index ee684ad84..b97e62bca 100644 --- a/ugly_hacks/santiago/santiago.py +++ b/ugly_hacks/santiago/santiago.py @@ -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) diff --git a/ugly_hacks/santiago/test_santiago.py b/ugly_hacks/santiago/test_santiago.py index c409df02c..20a298b9f 100644 --- a/ugly_hacks/santiago/test_santiago.py +++ b/ugly_hacks/santiago/test_santiago.py @@ -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):