Skip to content

Commit 789a44d

Browse files
authored
Add regression tests and discovered-bug fixes
* add network tests * add default None value for socket/thread * add tests for node class, and fix some bugs there
1 parent 2dd8ca0 commit 789a44d

File tree

4 files changed

+880
-7
lines changed

4 files changed

+880
-7
lines changed

src/chordnet/net.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ def __init__(self, ip, port, request_handler):
1010
self._request_handler = request_handler
1111
self._running = False
1212
self._network_thread = None
13+
self.server_socket = None
14+
self.network_thread = None
1315

1416
def start(self):
1517
"""

src/chordnet/node.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,8 @@ def stabilize(self):
264264
if not self.successor():
265265
return
266266

267+
x = None
268+
267269
try:
268270
# Get the predecessor of the current successor
269271
#print(f"stabilize: checking successor {self.successor().key} for predecessor", file=sys.stderr)
@@ -277,11 +279,12 @@ def stabilize(self):
277279
#print(f"stabilize: updated successor to {self.successor().key}", file=sys.stderr)
278280
# otherwise, we just notify them that we exist. This is usually for the first joiner to a ring.
279281

280-
self.notify(self.successor())
281282
#print(f"Node {self.address} - Updated Successor: {self.successor()}, Predecessor: {self.predecessor}", file=sys.stderr)
282283

283284
except Exception as e:
284285
print(f"Stabilize failed: {e}", file=sys.stderr)
286+
finally:
287+
self.notify(self.successor())
285288

286289

287290
def notify(self, potential_successor):
@@ -311,6 +314,7 @@ def notify(self, potential_successor):
311314
return False
312315
except Exception as e:
313316
print(f"Notify failed: {e}", file=sys.stderr)
317+
return False
314318

315319

316320
def start(self):
@@ -368,6 +372,8 @@ def _is_between(self, start, end, key):
368372
Returns:
369373
bool: True if the node is between start and end, False otherwise.
370374
"""
375+
if start == end: # this shouldn't happen
376+
return False
371377
if start < end:
372378
return start < key < end
373379
else: # Wrap around case
@@ -477,6 +483,9 @@ def _process_request(self, method, args):
477483
elif method == 'NOTIFY':
478484
# Parse the notifying node's details
479485
try:
486+
if len(args) < 3:
487+
return "INVALID_NODE"
488+
480489
notifier = self._parse_address(':'.join([args[0], args[1], args[2]]))
481490
return "OK" if self._be_notified(notifier) else "IGNORED"
482491

tests/test_net.py

Lines changed: 169 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# test_net.py
22
import socket
3+
import sys
34
from unittest.mock import Mock, patch
45

6+
import pytest
7+
58
from chordnet import _Net
69

710

@@ -12,6 +15,10 @@ def test_net_initialization():
1215
assert net._ip == "localhost"
1316
assert net._port == 8000
1417
assert net._request_handler == mock_handler
18+
assert net._running is False
19+
assert net._network_thread is None
20+
assert net.server_socket is None # Now initialized in __init__
21+
assert net.network_thread is None # Now initialized in __init__
1522

1623

1724
@patch("socket.socket")
@@ -22,12 +29,17 @@ def test_net_start(mock_thread, mock_socket):
2229

2330
net.start()
2431

32+
assert net._running is True
33+
assert net.server_socket == mock_socket.return_value
34+
assert net.network_thread == mock_thread.return_value
35+
2536
# Verify socket was created and bound
2637
mock_socket.return_value.bind.assert_called_once_with(("localhost", 8000))
2738
mock_socket.return_value.listen.assert_called_once_with(5)
2839

2940
# Verify thread was started
3041
mock_thread.assert_called_once_with(target=net._listen_for_connections, daemon=True)
42+
mock_thread.return_value.start.assert_called_once()
3143

3244

3345
def test_net_handle_connection():
@@ -59,9 +71,12 @@ def test_net_stop():
5971
# Mock the thread and socket
6072
net.network_thread = Mock()
6173
net.server_socket = Mock()
74+
net._running = True # Simulate that it was running
6275

6376
net.stop()
6477

78+
assert net._running is False # Should be set to False
79+
6580
# Verify socket was closed
6681
net.server_socket.close.assert_called_once()
6782

@@ -92,12 +107,15 @@ def test_send_request_success():
92107
assert response == "RESPONSE"
93108

94109
# Verify socket methods were called correctly
110+
mock_socket_instance.settimeout.assert_called_once_with(5)
95111
mock_socket_instance.connect.assert_called_once_with(("localhost", 8001))
96-
mock_socket_instance.send.assert_called_once()
97-
mock_socket_instance.recv.assert_called_once()
112+
mock_socket_instance.send.assert_called_once_with(b"TEST:arg1:arg2")
113+
mock_socket_instance.recv.assert_called_once_with(1024)
98114

99115

100-
def test_send_request_timeout():
116+
@patch.object(sys.stderr, 'write')
117+
@patch.object(sys.stderr, 'flush')
118+
def test_send_request_timeout(mock_stderr_flush, mock_stderr_write):
101119
# Create a mock network instance
102120
mock_handler = Mock()
103121
net = _Net("localhost", 8000, mock_handler)
@@ -110,16 +128,22 @@ def test_send_request_timeout():
110128
# Simulate a timeout
111129
mock_socket_instance.connect.side_effect = socket.timeout
112130

113-
# Call send_request and check for None return
114131
response = net.send_request(
115132
Mock(ip="localhost", port=8001), "TEST", "arg1", "arg2"
116133
)
117134

118135
# Assertions
119136
assert response is None
120137

138+
# Assert that 'write' was called and contained the message
139+
assert mock_stderr_write.call_count >= 1
140+
messages_written = [call_args[0][0] for call_args in mock_stderr_write.call_args_list]
141+
assert any("Request timed out" in msg for msg in messages_written)
121142

122-
def test_send_request_connection_refused():
143+
144+
@patch.object(sys.stderr, 'write')
145+
@patch.object(sys.stderr, 'flush')
146+
def test_send_request_connection_refused(mock_stderr_flush, mock_stderr_write):
123147
# Create a mock network instance
124148
mock_handler = Mock()
125149
net = _Net("localhost", 8000, mock_handler)
@@ -132,10 +156,149 @@ def test_send_request_connection_refused():
132156
# Simulate connection refused
133157
mock_socket_instance.connect.side_effect = ConnectionRefusedError
134158

135-
# Call send_request and check for None return
136159
response = net.send_request(
137160
Mock(ip="localhost", port=8001), "TEST", "arg1", "arg2"
138161
)
139162

140163
# Assertions
141164
assert response is None
165+
# Assert that 'write' was called and contained the message
166+
assert mock_stderr_write.call_count >= 1
167+
messages_written = [call_args[0][0] for call_args in mock_stderr_write.call_args_list]
168+
assert any("Connection refused" in msg for msg in messages_written)
169+
170+
171+
# tests/test_net.py (only the relevant test function is shown)
172+
173+
@patch.object(sys.stderr, 'write')
174+
@patch.object(sys.stderr, 'flush')
175+
def test_listen_for_connections_accept_exception(mock_stderr_flush, mock_stderr_write):
176+
mock_handler = Mock()
177+
net = _Net("localhost", 8000, mock_handler)
178+
179+
mock_server_socket = Mock()
180+
net.server_socket = mock_server_socket # Manually set the mock server_socket
181+
net._running = True # Ensure the loop runs at least once
182+
183+
# Configure side_effect using a generator.
184+
# First call to accept(): raises exception, but net._running is still True.
185+
# Second call to accept() (if loop continued): sets net._running to False, allowing loop to exit.
186+
def controlled_accept_side_effect_generator():
187+
yield Exception("Simulated accept error") # First time accept is called, it raises this.
188+
# If _listen_for_connections tried to accept again (loop continues after logging error),
189+
# this next line would execute, making the loop terminate.
190+
net._running = False
191+
yield Mock(), Mock() # Return a valid connection, so it doesn't raise another error if loop tried once more.
192+
193+
mock_server_socket.accept.side_effect = controlled_accept_side_effect_generator()
194+
195+
# Call _listen_for_connections directly to control its execution for testing this error path
196+
net._listen_for_connections()
197+
198+
# Assertions for stderr output
199+
assert mock_stderr_write.call_count >= 1
200+
messages_written = [call_args[0][0] for call_args in mock_stderr_write.call_args_list]
201+
assert any("Error accepting connection: Simulated accept error" in msg for msg in messages_written)
202+
assert net._running is False # Verify the loop has stopped
203+
204+
205+
def test_net_handle_connection_no_args():
206+
mock_socket = Mock()
207+
mock_handler = Mock(return_value="test_response_no_args")
208+
209+
net = _Net("localhost", 8000, mock_handler)
210+
211+
# Simulate receiving a request with no args
212+
mock_socket.recv.return_value = b"TEST"
213+
214+
net._handle_connection(mock_socket)
215+
216+
# Verify handler was called correctly with an empty list for args
217+
mock_handler.assert_called_once_with("TEST", [])
218+
219+
# Verify response was sent
220+
mock_socket.send.assert_called_once_with(b"test_response_no_args")
221+
222+
# Verify socket was closed
223+
mock_socket.close.assert_called_once()
224+
225+
226+
@patch.object(sys.stderr, 'write')
227+
@patch.object(sys.stderr, 'flush')
228+
def test_net_handle_connection_handler_exception(mock_stderr_flush, mock_stderr_write):
229+
mock_socket = Mock()
230+
mock_handler = Mock(side_effect=Exception("Handler error"))
231+
232+
net = _Net("localhost", 8000, mock_handler)
233+
234+
mock_socket.recv.return_value = b"TEST:arg1"
235+
236+
net._handle_connection(mock_socket)
237+
238+
mock_handler.assert_called_once_with("TEST", ["arg1"])
239+
mock_socket.send.assert_not_called() # Response should not be sent
240+
241+
mock_socket.close.assert_called_once() # Socket should still be closed
242+
243+
assert mock_stderr_write.call_count >= 1
244+
messages_written = [call_args[0][0] for call_args in mock_stderr_write.call_args_list]
245+
assert any("Error handling connection: Handler error" in msg for msg in messages_written)
246+
mock_stderr_flush.assert_called_once()
247+
248+
249+
def test_send_request_no_args():
250+
mock_handler = Mock()
251+
net = _Net("localhost", 8000, mock_handler)
252+
253+
with patch("socket.socket") as mock_socket:
254+
mock_socket_instance = Mock()
255+
mock_socket.return_value.__enter__.return_value = mock_socket_instance
256+
mock_socket_instance.recv.return_value = b"RESPONSE_NO_ARGS"
257+
258+
response = net.send_request(
259+
Mock(ip="localhost", port=8001), "NO_ARGS_METHOD"
260+
)
261+
262+
assert response == "RESPONSE_NO_ARGS"
263+
mock_socket_instance.send.assert_called_once_with(b"NO_ARGS_METHOD:")
264+
265+
266+
@patch.object(sys.stderr, 'write')
267+
@patch.object(sys.stderr, 'flush')
268+
def test_send_request_general_exception(mock_stderr_flush, mock_stderr_write):
269+
mock_handler = Mock()
270+
net = _Net("localhost", 8000, mock_handler)
271+
272+
with patch("socket.socket") as mock_socket:
273+
mock_socket_instance = Mock()
274+
mock_socket.return_value.__enter__.return_value = mock_socket_instance
275+
276+
# Simulate a generic exception during connect
277+
mock_socket_instance.connect.side_effect = Exception("Generic network error")
278+
279+
response = net.send_request(
280+
Mock(ip="localhost", port=8001), "TEST", "arg1"
281+
)
282+
283+
assert response is None
284+
assert mock_stderr_write.call_count >= 1
285+
messages_written = [call_args[0][0] for call_args in mock_stderr_write.call_args_list]
286+
assert any("Network request error: Generic network error" in msg for msg in messages_written)
287+
288+
289+
def test_net_stop_before_start():
290+
mock_handler = Mock()
291+
net = _Net("localhost", 8000, mock_handler)
292+
293+
# Initially, server_socket and network_thread are None due to __init__ changes
294+
assert net._running is False
295+
assert net.server_socket is None
296+
assert net.network_thread is None
297+
298+
# Calling stop should not raise an error
299+
try:
300+
net.stop()
301+
except Exception as e:
302+
pytest.fail(f"Calling stop before start raised an exception: {e}")
303+
304+
# No assertions on mocks needed as server_socket and network_thread are None

0 commit comments

Comments
 (0)