Skip to content

Commit 9a7de93

Browse files
committed
3E - 100% Test coverage!
1 parent 29ba22f commit 9a7de93

File tree

5 files changed

+313
-7
lines changed

5 files changed

+313
-7
lines changed

coverage.xml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
<?xml version="1.0" ?>
2-
<coverage version="7.10.5" timestamp="1755979866629" lines-valid="195" lines-covered="191" line-rate="0.9795" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
2+
<coverage version="7.10.5" timestamp="1755981078598" lines-valid="195" lines-covered="195" line-rate="1" branches-covered="0" branches-valid="0" branch-rate="0" complexity="0">
33
<!-- Generated by coverage.py: https://coverage.readthedocs.io/en/7.10.5 -->
44
<!-- Based on https://raw.githubusercontent.com/cobertura/web/master/htdocs/xml/coverage-04.dtd -->
55
<sources>
66
<source>D:\GitHub\NetSplit</source>
77
</sources>
88
<packages>
9-
<package name="." line-rate="0.9795" branch-rate="0" complexity="0">
9+
<package name="." line-rate="1" branch-rate="0" complexity="0">
1010
<classes>
11-
<class name="netSplit.py" filename="netSplit.py" complexity="0" line-rate="0.9795" branch-rate="0">
11+
<class name="netSplit.py" filename="netSplit.py" complexity="0" line-rate="1" branch-rate="0">
1212
<methods/>
1313
<lines>
1414
<line number="13" hits="1"/>
@@ -90,7 +90,7 @@
9090
<line number="181" hits="1"/>
9191
<line number="182" hits="1"/>
9292
<line number="183" hits="1"/>
93-
<line number="184" hits="0"/>
93+
<line number="184" hits="1"/>
9494
<line number="186" hits="1"/>
9595
<line number="188" hits="1"/>
9696
<line number="189" hits="1"/>
@@ -125,7 +125,7 @@
125125
<line number="227" hits="1"/>
126126
<line number="228" hits="1"/>
127127
<line number="229" hits="1"/>
128-
<line number="230" hits="0"/>
128+
<line number="230" hits="1"/>
129129
<line number="232" hits="1"/>
130130
<line number="235" hits="1"/>
131131
<line number="236" hits="1"/>
@@ -134,7 +134,7 @@
134134
<line number="239" hits="1"/>
135135
<line number="240" hits="1"/>
136136
<line number="241" hits="1"/>
137-
<line number="242" hits="0"/>
137+
<line number="242" hits="1"/>
138138
<line number="244" hits="1"/>
139139
<line number="245" hits="1"/>
140140
<line number="246" hits="1"/>
@@ -160,7 +160,7 @@
160160
<line number="284" hits="1"/>
161161
<line number="285" hits="1"/>
162162
<line number="286" hits="1"/>
163-
<line number="287" hits="0"/>
163+
<line number="287" hits="1"/>
164164
<line number="288" hits="1"/>
165165
<line number="289" hits="1"/>
166166
<line number="290" hits="1"/>

scripts/print_with_lines.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from pathlib import Path
2+
p = Path('d:/GitHub/NetSplit/netSplit.py')
3+
text = p.read_text()
4+
for i, l in enumerate(text.splitlines(), start=1):
5+
print(f"{i:03}: {l}")
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import os
2+
import sys
3+
import socket as _socket
4+
5+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
6+
7+
import netSplit
8+
from netSplit import socket as NetSocket
9+
10+
11+
def test_legacy_marker_with_dict_key(monkeypatch):
12+
# legacy marker: first byte not 'i' or 's' and config is a dict with integer key
13+
sid = 0
14+
raw = sid.to_bytes(4, 'big')
15+
seq = [netSplit.VERSION.encode(), b'OK', raw[:1], raw[1:]]
16+
client = type('C', (), {
17+
'seq': list(seq),
18+
'sent': [],
19+
'closed': False,
20+
'recv': lambda self, n: self.seq.pop(0) if self.seq else b'',
21+
'sendall': lambda self, data: self.sent.append(data),
22+
'close': lambda self: setattr(self, 'closed', True),
23+
'settimeout': lambda self, t: None,
24+
'setblocking': lambda self, b: None,
25+
'getpeername': lambda self: ('127.0.0.1', 1),
26+
})()
27+
28+
# config is a dict with integer key -> should hit the 'sid in config' branch
29+
netSplit.config = {0: {'host': '127.0.0.1', 'port': 1}}
30+
31+
# Prevent real network connects and proxying
32+
class FakeSock:
33+
def __init__(self, *a, **k):
34+
pass
35+
def settimeout(self, t):
36+
pass
37+
def connect(self, addr):
38+
pass
39+
def setblocking(self, b):
40+
pass
41+
42+
monkeypatch.setattr(netSplit._socket, 'socket', FakeSock)
43+
monkeypatch.setattr(netSplit.socket, '_proxy_between', lambda self, cs, sock: None)
44+
45+
listener = NetSocket(_socket.AF_INET, _socket.SOCK_STREAM)
46+
res = listener.handle_client(client, ('127.0.0.1', 0))
47+
assert res is None or isinstance(res, str)
48+
49+
50+
def test_proxy_select_empty_then_shutdown(monkeypatch):
51+
# Cover the 'if not events: continue' path by making the selector
52+
# return an empty list once, then return an event that triggers
53+
# ConnectionResetError to end the loop.
54+
55+
class FakeKey:
56+
def __init__(self, fileobj, data):
57+
self.fileobj = fileobj
58+
self.data = data
59+
60+
class FakeSelector:
61+
def __init__(self):
62+
self.calls = 0
63+
def register(self, sock, ev, data=None):
64+
# noop
65+
pass
66+
def unregister(self, sock):
67+
pass
68+
def select(self, timeout=None):
69+
self.calls += 1
70+
if self.calls == 1:
71+
return []
72+
# second call: return an event whose fileobj.recv raises
73+
class BadSock:
74+
def getpeername(self):
75+
return ('1.2.3.4', 9999)
76+
def recv(self, n):
77+
raise ConnectionResetError
78+
return [ (FakeKey(BadSock(), 'client'), None) ]
79+
80+
# Fake sockets passed to _proxy_between
81+
class CS:
82+
def getpeername(self):
83+
return ('127.0.0.1', 1234)
84+
def setblocking(self, b):
85+
pass
86+
87+
class BACK:
88+
def getpeername(self):
89+
return ('9.9.9.9', 4321)
90+
def setblocking(self, b):
91+
pass
92+
93+
monkeypatch.setattr(netSplit.selectors, 'DefaultSelector', lambda: FakeSelector())
94+
95+
ns = NetSocket(_socket.AF_INET, _socket.SOCK_STREAM)
96+
cs = CS()
97+
back = BACK()
98+
# Should return None (clean shutdown on ConnectionResetError)
99+
res = ns._proxy_between(cs, back)
100+
assert res is None
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import sys
2+
import os
3+
4+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
5+
6+
import netSplit
7+
from netSplit import socket as NetSocket
8+
9+
10+
class FakeClient:
11+
def __init__(self, recv_sequence):
12+
# recv_sequence: list of bytes or Exceptions to be returned/raised
13+
self._recv = list(recv_sequence)
14+
self.sent = []
15+
self.closed = False
16+
17+
def recv(self, n):
18+
if not self._recv:
19+
return b''
20+
v = self._recv.pop(0)
21+
if isinstance(v, Exception):
22+
raise v
23+
return v
24+
25+
def sendall(self, data):
26+
self.sent.append(data)
27+
28+
def close(self):
29+
self.closed = True
30+
31+
def settimeout(self, t):
32+
pass
33+
34+
def setblocking(self, b):
35+
pass
36+
37+
def getpeername(self):
38+
return ('127.0.0.1', 1234)
39+
40+
41+
def test_short_version_missing_rest():
42+
# initial shorter than VERSION and recv_exact returns b'' -> no_support
43+
prefix = netSplit.VERSION[:-1] if len(netSplit.VERSION) > 1 else b''
44+
seq = [prefix.encode(), b'']
45+
fake = FakeClient(seq)
46+
listener = NetSocket()
47+
res = listener.handle_client(fake, ('127.0.0.1', 0))
48+
assert isinstance(res, str)
49+
assert 'no_support' in res
50+
# ensure fake got a protocol error payload and was closed
51+
assert fake.closed or any(b'E' in chunk for chunk in fake.sent)
52+
53+
54+
def test_invalid_server_name_returns_protocol_error():
55+
# full VERSION, client ack, then 's' marker with a missing name
56+
name = b'missing'
57+
seq = [netSplit.VERSION.encode(), b'OK', b's', len(name).to_bytes(4, 'big'), name]
58+
fake = FakeClient(seq)
59+
# ensure no matching name in config
60+
netSplit.config = {}
61+
listener = NetSocket()
62+
res = listener.handle_client(fake, ('127.0.0.1', 0))
63+
assert isinstance(res, str)
64+
assert 'invalid_server' in res
65+
# ensure protocol error was sent and socket closed
66+
assert fake.closed or any(netSplit.PROTOCOL_ERRORS['invalid_server'].startswith(chunk) for chunk in fake.sent)
67+
68+
69+
def test_legacy_index_out_of_range_triggers_invalid_server():
70+
# full VERSION, client ack, legacy marker (first byte of id) + 3 bytes rest
71+
# craft a large sid that is out of range for a small config
72+
sid = 9999
73+
raw_id = sid.to_bytes(4, 'big')
74+
seq = [netSplit.VERSION.encode(), b'OK', raw_id[:1], raw_id[1:]]
75+
fake = FakeClient(seq)
76+
netSplit.config = {0: {'host': '127.0.0.1', 'port': 1}}
77+
listener = NetSocket()
78+
res = listener.handle_client(fake, ('127.0.0.1', 0))
79+
assert isinstance(res, str)
80+
assert 'invalid_server' in res
81+
assert fake.closed or any(netSplit.PROTOCOL_ERRORS['invalid_server'].startswith(chunk) for chunk in fake.sent)
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import os
2+
import sys
3+
import socket as _socket
4+
5+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
6+
7+
import netSplit
8+
from netSplit import socket as NetSocket
9+
10+
11+
class SeqClient:
12+
def __init__(self, seq):
13+
self.seq = list(seq)
14+
self.sent = []
15+
self.closed = False
16+
17+
def recv(self, n):
18+
if not self.seq:
19+
return b''
20+
return self.seq.pop(0)
21+
22+
def sendall(self, data):
23+
self.sent.append(data)
24+
25+
def close(self):
26+
self.closed = True
27+
28+
def settimeout(self, t):
29+
pass
30+
31+
def setblocking(self, b):
32+
pass
33+
34+
def getpeername(self):
35+
return ('127.0.0.1', 1)
36+
37+
38+
def _stub_proxy(self, cs, sock):
39+
# mark that backend was connected and return success
40+
return None
41+
42+
43+
class FakeSock:
44+
"""A minimal fake socket used to avoid real network connects in tests."""
45+
def __init__(self, *a, **k):
46+
self.connected = False
47+
48+
def settimeout(self, t):
49+
pass
50+
51+
def connect(self, addr):
52+
# pretend we connected; don't actually touch the network
53+
self.connected = True
54+
55+
def sendall(self, data):
56+
pass
57+
58+
def close(self):
59+
pass
60+
61+
def setblocking(self, b):
62+
pass
63+
64+
65+
def test_version_rest_combines_and_proceeds(monkeypatch):
66+
# split VERSION into prefix + rest so the code executes 'ver = initial + rest'
67+
v = netSplit.VERSION
68+
prefix = v[:1].encode()
69+
rest = v[1:].encode()
70+
71+
# sequence of what client will send to the server via recv()
72+
# initial (prefix), then rest for recv_exact, then ack 'OK', then marker 'i' + 4-byte id
73+
seq = [prefix, rest, b'OK', b'i', (0).to_bytes(4, 'big')]
74+
client = SeqClient(seq)
75+
76+
# set a config so len(config) is available and target server exists
77+
netSplit.config = {0: {'host': '127.0.0.1', 'port': 1}}
78+
79+
# stub the backend proxying so no network is used
80+
monkeypatch.setattr(netSplit.socket, '_proxy_between', _stub_proxy)
81+
# prevent real socket.connect() from being called
82+
monkeypatch.setattr(netSplit._socket, 'socket', FakeSock)
83+
84+
listener = NetSocket(_socket.AF_INET, _socket.SOCK_STREAM)
85+
res = listener.handle_client(client, ('127.0.0.1', 0))
86+
assert res is None or isinstance(res, str)
87+
88+
89+
def test_selection_by_name_success(monkeypatch):
90+
name = 'myserver'
91+
# client sends full VERSION, then 'OK' ack, then 's' marker + name length + name
92+
seq = [netSplit.VERSION.encode(), b'OK', b's', len(name.encode()).to_bytes(4, 'big'), name.encode()]
93+
client = SeqClient(seq)
94+
95+
netSplit.config = {name: {'host': '127.0.0.1', 'port': 1}}
96+
97+
# stub proxy to avoid real sockets
98+
monkeypatch.setattr(netSplit.socket, '_proxy_between', _stub_proxy)
99+
monkeypatch.setattr(netSplit._socket, 'socket', FakeSock)
100+
101+
listener = NetSocket(_socket.AF_INET, _socket.SOCK_STREAM)
102+
res = listener.handle_client(client, ('127.0.0.1', 0))
103+
assert res is None or isinstance(res, str)
104+
105+
106+
def test_legacy_list_index_success(monkeypatch):
107+
# legacy marker: send a first byte that's not 'i' or 's', then 3 bytes follow
108+
sid = 0
109+
raw = sid.to_bytes(4, 'big')
110+
seq = [netSplit.VERSION.encode(), b'OK', raw[:1], raw[1:]]
111+
client = SeqClient(seq)
112+
113+
netSplit.config = [{'host': '127.0.0.1', 'port': 1}]
114+
115+
monkeypatch.setattr(netSplit.socket, '_proxy_between', _stub_proxy)
116+
monkeypatch.setattr(netSplit._socket, 'socket', FakeSock)
117+
118+
listener = NetSocket(_socket.AF_INET, _socket.SOCK_STREAM)
119+
res = listener.handle_client(client, ('127.0.0.1', 0))
120+
assert res is None or isinstance(res, str)

0 commit comments

Comments
 (0)