added travis file and some more tests

This commit is contained in:
Brian Muller 2014-01-04 14:27:45 -05:00
parent 2a4802f56f
commit ee0c6b9bd5
12 changed files with 242 additions and 24 deletions

6
.travis.yml Normal file
View File

@ -0,0 +1,6 @@
language: python
python:
- "2.7"
- "pypy"
install: pip install . --use-mirrors
script: trial kademlia

View File

@ -1,5 +1,8 @@
PYDOCTOR=pydoctor PYDOCTOR=pydoctor
test:
trial kademlia
docs: docs:
$(PYDOCTOR) --make-html --html-output apidoc --add-package kademlia --project-name=kademlia --project-url=http://github.com/bmuller/kademlia --html-use-sorttable --html-use-splitlinks --html-shorten-lists $(PYDOCTOR) --make-html --html-output apidoc --add-package kademlia --project-name=kademlia --project-url=http://github.com/bmuller/kademlia --html-use-sorttable --html-use-splitlinks --html-shorten-lists
@ -9,6 +12,3 @@ lint:
install: install:
python setup.py install python setup.py install
test:
trial kademlia

View File

@ -1,18 +1,49 @@
# [Kademlia](http://en.wikipedia.org/wiki/Kademlia) in Python # [Kademlia](http://en.wikipedia.org/wiki/Kademlia) in Python
[![Build Status](https://secure.travis-ci.org/bmuller/kademlia.png?branch=master)](https://travis-ci.org/bmuller/kademlia)
## Installation ## Installation
``` ```
easy_install kademlia pip install kademlia
``` ```
## Usage ## Usage
*This assumes you have a working familiarity with Twisted.* *This assumes you have a working familiarity with Twisted.*
Assuming you want to connect to an existing network (run the standalone server example below if you don't have a network):
```python ```python
from twisted.internet import reactor from twisted.internet import reactor
from twisted.python import log
from kademlia.network import Server
import sys
... # log to std out
log.startLogging(sys.stdout)
def quit(result):
print "Key result:", result
reactor.stop()
def get(result, server):
reactor.stop()
#return server.get("a key").addCallback(quit)
def done(found, server):
log.msg("Found nodes: %s" % found)
return server.set("a key", "a value").addCallback(get, server)
server = Server()
# next line, or use reactor.listenUDP(5678, server)
server.listen(5678)
server.bootstrap([('127.0.0.1', 1234)]).addCallback(done, two)
reactor.run()
```
## Stand-alone Server
If all you want to do is run a local server, just start the example server:
```
twistd -noy server.tac
``` ```

View File

@ -1,8 +0,0 @@
from twisted.internet import reactor
from twisted.python import log
from kademlia.network import Server
import sys
log.startLogging(sys.stdout)
one = Server(1234)
reactor.run()

View File

@ -4,7 +4,7 @@ Package for interacting on the network at a high level.
import random import random
from twisted.internet.task import LoopingCall from twisted.internet.task import LoopingCall
from twisted.internet import defer from twisted.internet import defer, reactor
from kademlia.log import Logger from kademlia.log import Logger
from kademlia.protocol import KademliaProtocol from kademlia.protocol import KademliaProtocol
@ -113,7 +113,7 @@ class Server(object):
to start listening as an active node on the network. to start listening as an active node on the network.
""" """
def __init__(self, port, ksize=20, alpha=3): def __init__(self, ksize=20, alpha=3):
""" """
Create a server instance. This will start listening on the given port. Create a server instance. This will start listening on the given port.
@ -125,10 +125,13 @@ class Server(object):
self.alpha = alpha self.alpha = alpha
self.log = Logger(system=self) self.log = Logger(system=self)
storage = ForgetfulStorage() storage = ForgetfulStorage()
self.node = Node('127.0.0.1', port, digest(random.getrandbits(255))) self.node = Node(None, None, digest(random.getrandbits(255)))
self.protocol = KademliaProtocol(self.node, storage, ksize) self.protocol = KademliaProtocol(self.node.id, storage, ksize)
self.refreshLoop = LoopingCall(self.refreshTable).start(3600) self.refreshLoop = LoopingCall(self.refreshTable).start(3600)
def listen(self, port):
return reactor.listenUDP(port, self.protocol)
def refreshTable(self): def refreshTable(self):
""" """
Refresh buckets that haven't had any lookups in the last hour Refresh buckets that haven't had any lookups in the last hour

View File

@ -7,11 +7,11 @@ from kademlia.log import Logger
class KademliaProtocol(RPCProtocol): class KademliaProtocol(RPCProtocol):
def __init__(self, node, storage, ksize): def __init__(self, sourceID, storage, ksize):
RPCProtocol.__init__(self, node.port) RPCProtocol.__init__(self)
self.router = RoutingTable(self, ksize) self.router = RoutingTable(self, ksize)
self.storage = storage self.storage = storage
self.sourceID = node.id self.sourceID = sourceID
self.log = Logger(system=self) self.log = Logger(system=self)
def getRefreshIDs(self): def getRefreshIDs(self):

View File

@ -93,13 +93,15 @@ class RoutingTable(object):
def __init__(self, protocol, ksize): def __init__(self, protocol, ksize):
self.protocol = protocol self.protocol = protocol
self.ksize = ksize self.ksize = ksize
self.buckets = [KBucket(0, 2 ** 160, ksize)] self.flush()
def flush(self):
self.buckets = [KBucket(0, 2 ** 160, self.ksize)]
def splitBucket(self, index): def splitBucket(self, index):
one, two = self.buckets[index].split() one, two = self.buckets[index].split()
self.buckets[index] = one self.buckets[index] = one
self.buckets.insert(index + 1, two) self.buckets.insert(index + 1, two)
# todo split one/two if needed based on section 4.2 # todo split one/two if needed based on section 4.2
def getLonelyBuckets(self): def getLonelyBuckets(self):

View File

@ -3,7 +3,9 @@ import hashlib
from twisted.trial import unittest from twisted.trial import unittest
from kademlia.utils import digest
from kademlia.node import Node, NodeHeap from kademlia.node import Node, NodeHeap
from kademlia.tests.utils import mknode
class NodeTest(unittest.TestCase): class NodeTest(unittest.TestCase):
@ -26,3 +28,30 @@ class NodeHeapTest(unittest.TestCase):
def test_maxSize(self): def test_maxSize(self):
n = NodeHeap(3) n = NodeHeap(3)
self.assertEqual(0, len(n)) self.assertEqual(0, len(n))
for d in range(10):
n.push(d, mknode())
self.assertEqual(3, len(n))
self.assertEqual(3, len(list(n)))
def test_iteration(self):
heap = NodeHeap(5)
nodes = [mknode(id=digest(x)) for x in range(10)]
for index, node in enumerate(nodes):
heap.push(index, node)
for index, node in enumerate(heap):
self.assertEqual(digest(index), node.id)
self.assertTrue(index < 5)
def test_remove(self):
heap = NodeHeap(5)
nodes = [mknode(id=digest(x)) for x in range(10)]
for index, node in enumerate(nodes):
heap.push(index, node)
heap.remove([nodes[0].id, nodes[1].id])
self.assertEqual(len(list(heap)), 5)
for index, node in enumerate(heap):
self.assertEqual(digest(index + 2), node.id)
self.assertTrue(index < 5)

View File

@ -0,0 +1,52 @@
from twisted.trial import unittest
from kademlia.routing import KBucket, RoutingTable
from kademlia.protocol import KademliaProtocol
from kademlia.tests.utils import mknode, FakeProtocol
class KBucketTest(unittest.TestCase):
def test_split(self):
bucket = KBucket(0, 10, 5)
bucket.addNode(mknode(intid=5))
bucket.addNode(mknode(intid=6))
one, two = bucket.split()
self.assertEqual(len(one), 1)
self.assertEqual(one.range, (0, 5))
self.assertEqual(len(two), 1)
self.assertEqual(two.range, (6, 10))
def test_addNode(self):
# when full, return false
bucket = KBucket(0, 10, 2)
self.assertTrue(bucket.addNode(mknode()))
self.assertTrue(bucket.addNode(mknode()))
self.assertFalse(bucket.addNode(mknode()))
self.assertEqual(len(bucket), 2)
# make sure when a node is double added it's put at the end
bucket = KBucket(0, 10, 3)
nodes = [mknode(), mknode(), mknode()]
for node in nodes:
bucket.addNode(node)
for index, node in enumerate(bucket.getNodes()):
self.assertEqual(node, nodes[index])
def test_inRange(self):
bucket = KBucket(0, 10, 10)
self.assertTrue(bucket.hasInRange(mknode(intid=5)))
self.assertFalse(bucket.hasInRange(mknode(intid=11)))
self.assertTrue(bucket.hasInRange(mknode(intid=10)))
self.assertTrue(bucket.hasInRange(mknode(intid=0)))
class RoutingTableTest(unittest.TestCase):
def setUp(self):
self.id = mknode().id
self.protocol = FakeProtocol(self.id)
self.router = self.protocol.router
def test_addContact(self):
self.router.addContact(mknode())
self.assertTrue(len(self.router.buckets), 1)
self.assertTrue(len(self.router.buckets[0].nodes), 1)

94
kademlia/tests/utils.py Normal file
View File

@ -0,0 +1,94 @@
"""
Utility functions for tests.
"""
import random
import hashlib
from struct import pack
from kademlia.node import Node
from kademlia.routing import RoutingTable
def mknode(ip=None, port=None, id=None, intid=None):
"""
Make a node. Created a random id if not specified.
"""
if intid is not None:
id = pack('>l', intid)
id = id or hashlib.sha1(str(random.getrandbits(255))).digest()
return Node(ip, port, id)
class FakeProtocol(object):
def __init__(self, sourceID, ksize=20):
self.router = RoutingTable(self, ksize)
self.storage = {}
self.sourceID = sourceID
def getRefreshIDs(self):
"""
Get ids to search for to keep old buckets up to date.
"""
ids = []
for bucket in self.router.getLonelyBuckets():
ids.append(random.randint(*bucket.range))
return ids
def rpc_ping(self, sender, nodeid):
source = Node(sender[0], sender[1], nodeid)
self.router.addContact(source)
return self.sourceID
def rpc_store(self, sender, nodeid, key, value):
source = Node(sender[0], sender[1], nodeid)
self.router.addContact(source)
self.log.debug("got a store request from %s, storing value" % str(sender))
self.storage[key] = value
def rpc_find_node(self, sender, nodeid, key):
self.log.info("finding neighbors of %i in local table" % long(nodeid.encode('hex'), 16))
source = Node(sender[0], sender[1], nodeid)
self.router.addContact(source)
node = Node(None, None, key)
return map(tuple, self.router.findNeighbors(node, exclude=source))
def rpc_find_value(self, sender, nodeid, key):
source = Node(sender[0], sender[1], nodeid)
self.router.addContact(source)
value = self.storage.get(key, None)
if value is None:
return self.rpc_find_node(sender, nodeid, key)
return { 'value': value }
def callFindNode(self, nodeToAsk, nodeToFind):
address = (nodeToAsk.ip, nodeToAsk.port)
d = self.find_node(address, self.sourceID, nodeToFind.id)
return d.addCallback(self.handleCallResponse, nodeToAsk)
def callFindValue(self, nodeToAsk, nodeToFind):
address = (nodeToAsk.ip, nodeToAsk.port)
d = self.find_value(address, self.sourceID, nodeToFind.id)
return d.addCallback(self.handleCallResponse, nodeToAsk)
def callPing(self, nodeToAsk):
address = (nodeToAsk.ip, nodeToAsk.port)
d = self.ping(address, self.sourceID)
return d.addCallback(self.handleCallResponse, nodeToAsk)
def callStore(self, nodeToAsk, key, value):
address = (nodeToAsk.ip, nodeToAsk.port)
d = self.store(address, self.sourceID, key, value)
return d.addCallback(self.handleCallResponse, nodeToAsk)
def handleCallResponse(self, result, node):
"""
If we get a response, add the node to the routing table. If
we get no response, make sure it's removed from the routing table.
"""
if result[0]:
self.log.info("got response from %s, adding to router" % node)
self.router.addContact(node)
else:
self.log.debug("no response from %s, removing from router" % node)
self.router.removeContact(node)
return result

9
server.tac Normal file
View File

@ -0,0 +1,9 @@
from twisted.application import service, internet
import sys, os
sys.path.append(os.path.dirname(__file__))
from kademlia.network import Server
application = service.Application("kademlia")
kserver = Server()
server = internet.UDPServer(1234, kserver.protocol)
server.setServiceParent(application)

View File

@ -12,5 +12,5 @@ setup(
url="http://github.com/bmuller/kademlia", url="http://github.com/bmuller/kademlia",
packages=find_packages(), packages=find_packages(),
requires=["twisted", "rpcudp"], requires=["twisted", "rpcudp"],
install_requires=['twisted>=12.0', "rpcudp>=0.3"] install_requires=['twisted>=13.0', "rpcudp>=1.0"]
) )