Initial push

This commit is contained in:
Ano-sys
2024-12-30 02:06:40 +01:00
commit 817ba850fb
63 changed files with 6775 additions and 0 deletions
+343
View File
@@ -0,0 +1,343 @@
"""
Tests for RN Praxis 2
"""
import contextlib
import errno
import struct
import time
import urllib.request as req
from urllib.parse import urlparse
from http.client import HTTPConnection
import pytest
import util
import dht
# We assume that all tests from the previous sheet(s) are working!
# -i dumps header
# -L follows redirects
# curl -iL localhost:12345/static/baz
def _iter_with_neighbors(xs):
"""Return iterator to list that includes each elements neighbors
For each element in the original list a triple of its neighbors
is generated: `(xs[i - 1], xs[i], xs[i + 1])`
"""
return zip(xs[-1:] + xs[:-1], xs, xs[1:] + xs[:1])
@pytest.fixture
def static_peer(request):
"""Return a function for spawning DHT peers
"""
def runner(peer, predecessor=None, successor=None):
"""Spawn a static DHT peer
The peer is passed its local neighborhood via environment variables.
"""
return util.KillOnExit(
[request.config.getoption('executable'), peer.ip, f'{peer.port}'] + ([f'{peer.id}'] if peer.id is not None else []),
env={
**({'PRED_ID': f'{predecessor.id}', 'PRED_IP': predecessor.ip, 'PRED_PORT': f'{predecessor.port}'} if predecessor is not None else {}),
**({'SUCC_ID': f'{successor.id}', 'SUCC_IP': successor.ip, 'SUCC_PORT': f'{successor.port}'} if successor is not None else {}),
'NO_STABILIZE': '1', # Forward compatibility with P3.
},
)
@contextlib.contextmanager
def empty_context(*args, **kwargs):
yield # No peer initialization here, just an empty context that never fails
if request.config.getoption('debug_own'):
return empty_context
return runner
@pytest.mark.timeout(1)
def test_listen(static_peer):
"""
Tests chord part of the system (1.1).
Listens on UDP port.
"""
self = dht.Peer(None, '127.0.0.1', 4711)
with static_peer(self), pytest.raises(OSError) as exception_info:
time.sleep(.1)
# Attempt to open the port that the implementation should have opened
with dht.peer_socket(self):
pass
assert exception_info.value.errno == errno.EADDRINUSE, "UDP port not open"
@pytest.mark.timeout(1)
@pytest.mark.parametrize("uri", ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'])
def test_immediate_dht(static_peer, uri):
"""Test hashing of request (1.2)
Run peer in minimal (non-trivial) DHT
- two nodes, equal split of namespace
- first node real, second mock sockets
1. make request - internally requires hashing of location part in URL
2. check that request yields either 404 (if peer is responsible) or 303 (if it isn't)
3. no packet should be received by the second peer
"""
predecessor = dht.Peer(0xc000, '127.0.0.1', 4712)
self = dht.Peer(0x4000, '127.0.0.1', 4711)
successor = predecessor
with dht.peer_socket(
successor
) as mock, static_peer(
self, predecessor, successor,
), contextlib.closing(
HTTPConnection(self.ip, self.port) # Use `http.client` instead of `urllib` to avoid redirection
) as conn:
conn.connect()
conn.request('GET', f'/{uri}')
time.sleep(.1)
reply = conn.getresponse()
_ = reply.read()
uri_hash = dht.hash(f'/{uri}'.encode('latin1'))
implementation_responsible = not self.id < uri_hash <= successor.id
if implementation_responsible:
assert reply.status == 404, "Server should've indicated missing data"
else:
assert reply.status == 303, "Server should've delegated response"
assert reply.headers['Location'] == f'http://{successor.ip}:{successor.port}/{uri}', "Server should've delegated to its successor"
assert util.bytes_available(mock) == 0, "Data received on successor socket"
@pytest.mark.timeout(1)
@pytest.mark.parametrize("uri", ['a', 'b'])
def test_lookup_sent(static_peer, uri):
"""Test for lookup to correct peer (1.3)
Node is running with minimal assigned address space, should send lookup messages
for the correct hash to its successors and reply with 503 & Retry-After header.
"""
predecessor = dht.Peer(0xffff, '127.0.0.1', 4710)
self = dht.Peer(0x0000, '127.0.0.1', 4711)
successor = dht.Peer(0x0001, '127.0.0.1', 4712)
with dht.peer_socket(successor) as mock: # Can't differentiate predecessor & successor, reusing ports.
with static_peer(
self, predecessor, successor
), pytest.raises(req.HTTPError) as exception_info:
url = f'http://{self.ip}:{self.port}/{uri}'
req.urlopen(url)
assert exception_info.value.status == 503, "Server should reply with 503"
assert exception_info.value.headers.get("Retry-After", None) == "1", "Server should set 'Retry-After' header to 1s"
time.sleep(.1)
assert util.bytes_available(mock) > 0, "No data received on successor socket"
data = mock.recv(1024)
assert len(data) == struct.calcsize(dht.message_format), "Received message has invalid length for DHT message"
msg = dht.deserialize(data)
assert dht.Flags(msg.flags) == dht.Flags.lookup, "Received message should be a lookup"
uri_hash = dht.hash(urlparse(url).path.encode('latin1'))
assert msg.id == uri_hash, "Received lookup should query the requested datum's hash"
assert msg.peer == self, "Received lookup should indicate its originator"
@pytest.mark.timeout(1)
def test_lookup_reply(static_peer):
"""Test whether peer replies to lookup correctly (1.4)"""
predecessor = dht.Peer(0x0000, '127.0.0.1', 4710)
self = dht.Peer(0x1000, '127.0.0.1', 4711)
successor = dht.Peer(0x2000, '127.0.0.1', 4712)
with dht.peer_socket(
predecessor
) as pred_mock, static_peer(
self, predecessor, successor
), dht.peer_socket(
successor
) as succ_mock:
lookup = dht.Message(dht.Flags.lookup, 0x1800, predecessor)
pred_mock.sendto(dht.serialize(lookup), (self.ip, self.port))
time.sleep(.1)
assert util.bytes_available(succ_mock) == 0, "Data received on successor socket"
assert util.bytes_available(pred_mock) > 0, "No data received on predecessor socket"
data = pred_mock.recv(1024)
assert len(data) == struct.calcsize(dht.message_format), "Received message has invalid length for DHT message"
reply = dht.deserialize(data)
assert dht.Flags(reply.flags) == dht.Flags.reply, "Received message should be a reply"
assert reply.peer == successor, "Reply does not indicate successor as responsible"
assert reply.id == self.id, "Reply does not indicate implementation as previous ID"
@pytest.mark.timeout(1)
def test_lookup_forward(static_peer):
"""Test whether peer forwards lookup correctly (1.5)"""
predecessor = dht.Peer(0x0000, '127.0.0.1', 4710)
self = dht.Peer(0x1000, '127.0.0.1', 4711)
successor = dht.Peer(0x2000, '127.0.0.1', 4712)
with dht.peer_socket(
predecessor
) as pred_mock, static_peer(
self, predecessor, successor
), dht.peer_socket(
successor
) as succ_mock:
lookup = dht.Message(dht.Flags.lookup, 0x2800, predecessor)
pred_mock.sendto(dht.serialize(lookup), (self.ip, self.port))
time.sleep(.1)
assert util.bytes_available(pred_mock) == 0, "Data received on predecessor socket"
assert util.bytes_available(succ_mock) > 0, "No data received on successor socket"
data = succ_mock.recv(1024)
assert len(data) == struct.calcsize(dht.message_format), "Received message has invalid length for DHT message"
received = dht.deserialize(data)
assert received == lookup, "Received message should be equal to original lookup"
@pytest.mark.timeout(1)
@pytest.mark.parametrize("uri", ['a', 'b'])
def test_lookup_complete(static_peer, uri):
"""Test for correct lookup use (1.6)
Node is running with minimal assigned address space, should send lookup messages
for the correct hash to its successors and reply with 503 & Retry-After header.
"""
predecessor = dht.Peer(0xffff, '127.0.0.1', 4710)
self = dht.Peer(0x0000, '127.0.0.1', 4711)
successor = dht.Peer(0x0001, '127.0.0.1', 4712)
with dht.peer_socket(
predecessor
) as pred_mock, static_peer(
self, predecessor, successor
), dht.peer_socket(
successor
) as succ_mock, contextlib.closing(
HTTPConnection(self.ip, self.port) # We have to use the more low-level `http.client` instead of `urllib` to avoid it interpreting the `Retry-After` header
) as conn:
conn.connect()
time.sleep(.1)
# Check HTTP reply
conn.request('GET', f'/{uri}')
time.sleep(.1)
response = conn.getresponse()
_ = response.read()
assert response.status == 503, "Server should reply with 503"
assert response.headers.get("Retry-After", None) == "1", "Server should set 'Retry-After' header to 1s"
time.sleep(.1)
# Check lookup message
time.sleep(.1)
assert util.bytes_available(pred_mock) == 0, "Data received on predecessor socket"
assert util.bytes_available(succ_mock) > 0, "No data received on successor socket"
data = succ_mock.recv(1024)
assert len(data) == struct.calcsize(dht.message_format), "Received message has invalid length for DHT message"
msg = dht.deserialize(data)
time.sleep(.1)
assert dht.Flags(msg.flags) == dht.Flags.lookup, "Received message should be a lookup"
uri_hash = dht.hash(f'/{uri}'.encode('latin1'))
assert msg.id == uri_hash, "Received lookup should query the requested datum's hash"
assert msg.peer == self, "Received lookup should indicate its originator"
time.sleep(.1)
# Send reply
reply = dht.Message(dht.Flags.reply, successor.id, predecessor)
pred_mock.sendto(dht.serialize(reply), (self.ip, self.port))
time.sleep(.1)
# Check HTTP reply again
conn.request('GET', f'/{uri}')
response = conn.getresponse()
_ = response.read()
assert response.status == 303, "Server should've delegated response"
assert response.headers['Location'] == f'http://{predecessor.ip}:{predecessor.port}/{uri}', "Server should've delegated to its predecessor"
@pytest.mark.timeout(10) # five requests, two seconds each
def test_dht(static_peer):
"""Test a complete DHT (1.7)
At this point, a DHT consisting only of the implementation should work as expected.
We will repeat the dynamic content test, but will contact a different peer for each request.
"""
# chosen by fair dice roll.
# guaranteed to be random.
dht_ids = [10927, 18804, 40809, 54536, 63901]
contact_order = [1, 0, 3, 2, 4]
datum = '191b023eb6e0090d'
content = b'8392cb0f8991fb706b8d80b898fd7bdc888e8fc4b40858e9eb136743ba1ac290'
peers = [
dht.Peer(id_, '127.0.0.1', 4710 + i)
for i, id_
in enumerate(dht_ids)
]
with contextlib.ExitStack() as contexts:
for predecessor, peer, successor in _iter_with_neighbors(peers):
contexts.enter_context(static_peer(
peer, predecessor, successor
))
# Ensure datum is missing
contact = peers[contact_order[0]]
with pytest.raises(req.HTTPError) as exception_info:
util.urlopen(f'http://{contact.ip}:{contact.port}/dynamic/{datum}')
assert exception_info.value.status == 404, f"'/dynamic/{datum}' should be missing, but GET was not answered with '404'"
# Create datum
contact = peers[contact_order[1]]
reply = util.urlopen(req.Request(f'http://{contact.ip}:{contact.port}/dynamic/{datum}', data=content, method='PUT'))
assert reply.status == 201, f"Creation of '/dynamic/{datum}' did not yield '201'"
# Ensure datum exists
contact = peers[contact_order[2]]
reply = util.urlopen(f'http://{contact.ip}:{contact.port}/dynamic/{datum}')
assert reply.status == 200
assert reply.read() == content, f"Content of '/dynamic/{datum}' does not match what was passed"
# Delete datum
contact = peers[contact_order[3]]
real_url = util.urlopen(f'http://{contact.ip}:{contact.port}/dynamic/{datum}').geturl() # Determine correct URL
reply = util.urlopen(req.Request(real_url, method='DELETE'))
assert reply.status in {200, 202, 204}, f"Deletion of '/dynamic/{datum}' did not succeed"
# Ensure datum is missing
contact = peers[contact_order[4]]
with pytest.raises(req.HTTPError) as exception_info:
util.urlopen(f'http://{contact.ip}:{contact.port}/dynamic/{datum}')
assert exception_info.value.status == 404, f"'/dynamic/{datum}' should be missing, but GET was not answered with '404'"