test: expand coverage 64%→76%; add BUGS.md for Gemini migration issues
This commit is contained in:
@@ -10,12 +10,18 @@ from decnet.network import (
|
||||
HOST_IPVLAN_IFACE,
|
||||
HOST_MACVLAN_IFACE,
|
||||
MACVLAN_NETWORK_NAME,
|
||||
allocate_ips,
|
||||
create_ipvlan_network,
|
||||
create_macvlan_network,
|
||||
detect_interface,
|
||||
detect_subnet,
|
||||
get_host_ip,
|
||||
ips_to_range,
|
||||
remove_macvlan_network,
|
||||
setup_host_ipvlan,
|
||||
setup_host_macvlan,
|
||||
teardown_host_ipvlan,
|
||||
teardown_host_macvlan,
|
||||
)
|
||||
|
||||
|
||||
@@ -193,3 +199,154 @@ class TestSetupHostIpvlan:
|
||||
calls = [str(c) for c in mock_run.call_args_list]
|
||||
assert any(HOST_IPVLAN_IFACE in c for c in calls)
|
||||
assert not any(HOST_MACVLAN_IFACE in c for c in calls)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# allocate_ips (pure logic — no subprocess / Docker)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestAllocateIps:
|
||||
def test_basic_allocation(self):
|
||||
ips = allocate_ips("192.168.1.0/24", "192.168.1.1", "192.168.1.100", count=3)
|
||||
assert len(ips) == 3
|
||||
assert "192.168.1.1" not in ips # gateway skipped
|
||||
assert "192.168.1.100" not in ips # host IP skipped
|
||||
|
||||
def test_skips_network_and_broadcast(self):
|
||||
ips = allocate_ips("10.0.0.0/30", "10.0.0.1", "10.0.0.3", count=1)
|
||||
# /30 hosts: .1 (gateway), .2. .3 is host_ip → only .2 available
|
||||
assert ips == ["10.0.0.2"]
|
||||
|
||||
def test_respects_ip_start(self):
|
||||
ips = allocate_ips("192.168.1.0/24", "192.168.1.1", "192.168.1.1",
|
||||
count=2, ip_start="192.168.1.50")
|
||||
assert all(ip >= "192.168.1.50" for ip in ips)
|
||||
|
||||
def test_raises_when_not_enough_ips(self):
|
||||
# /30 only has 2 host addresses; reserving both leaves 0
|
||||
with pytest.raises(RuntimeError, match="Not enough free IPs"):
|
||||
allocate_ips("10.0.0.0/30", "10.0.0.1", "10.0.0.2", count=3)
|
||||
|
||||
def test_no_duplicates(self):
|
||||
ips = allocate_ips("10.0.0.0/24", "10.0.0.1", "10.0.0.2", count=10)
|
||||
assert len(ips) == len(set(ips))
|
||||
|
||||
def test_exact_count_returned(self):
|
||||
ips = allocate_ips("172.16.0.0/24", "172.16.0.1", "172.16.0.254", count=5)
|
||||
assert len(ips) == 5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# detect_interface
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDetectInterface:
|
||||
@patch("decnet.network._run")
|
||||
def test_parses_dev_from_route(self, mock_run):
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout="default via 192.168.1.1 dev eth0 proto dhcp\n"
|
||||
)
|
||||
assert detect_interface() == "eth0"
|
||||
|
||||
@patch("decnet.network._run")
|
||||
def test_raises_when_no_dev_found(self, mock_run):
|
||||
mock_run.return_value = MagicMock(stdout="")
|
||||
with pytest.raises(RuntimeError, match="Could not auto-detect"):
|
||||
detect_interface()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# detect_subnet
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDetectSubnet:
|
||||
def _make_run(self, addr_output, route_output):
|
||||
def side_effect(cmd, **kwargs):
|
||||
if "addr" in cmd:
|
||||
return MagicMock(stdout=addr_output)
|
||||
return MagicMock(stdout=route_output)
|
||||
return side_effect
|
||||
|
||||
@patch("decnet.network._run")
|
||||
def test_parses_subnet_and_gateway(self, mock_run):
|
||||
mock_run.side_effect = self._make_run(
|
||||
" inet 192.168.1.5/24 brd 192.168.1.255 scope global eth0\n",
|
||||
"default via 192.168.1.1 dev eth0\n",
|
||||
)
|
||||
subnet, gw = detect_subnet("eth0")
|
||||
assert subnet == "192.168.1.0/24"
|
||||
assert gw == "192.168.1.1"
|
||||
|
||||
@patch("decnet.network._run")
|
||||
def test_raises_when_no_inet(self, mock_run):
|
||||
mock_run.side_effect = self._make_run("", "default via 192.168.1.1 dev eth0\n")
|
||||
with pytest.raises(RuntimeError, match="Could not detect subnet"):
|
||||
detect_subnet("eth0")
|
||||
|
||||
@patch("decnet.network._run")
|
||||
def test_raises_when_no_gateway(self, mock_run):
|
||||
mock_run.side_effect = self._make_run(
|
||||
" inet 192.168.1.5/24 brd 192.168.1.255 scope global eth0\n", ""
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="Could not detect gateway"):
|
||||
detect_subnet("eth0")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_host_ip
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetHostIp:
|
||||
@patch("decnet.network._run")
|
||||
def test_returns_host_ip(self, mock_run):
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout=" inet 10.0.0.5/24 brd 10.0.0.255 scope global eth0\n"
|
||||
)
|
||||
assert get_host_ip("eth0") == "10.0.0.5"
|
||||
|
||||
@patch("decnet.network._run")
|
||||
def test_raises_when_no_inet(self, mock_run):
|
||||
mock_run.return_value = MagicMock(stdout="link/ether aa:bb:cc:dd:ee:ff\n")
|
||||
with pytest.raises(RuntimeError, match="Could not determine host IP"):
|
||||
get_host_ip("eth0")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# remove_macvlan_network
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRemoveMacvlanNetwork:
|
||||
def test_removes_matching_network(self):
|
||||
client = MagicMock()
|
||||
net = MagicMock()
|
||||
net.name = MACVLAN_NETWORK_NAME
|
||||
client.networks.list.return_value = [net]
|
||||
remove_macvlan_network(client)
|
||||
net.remove.assert_called_once()
|
||||
|
||||
def test_noop_when_no_matching_network(self):
|
||||
client = MagicMock()
|
||||
other = MagicMock()
|
||||
other.name = "some-other-network"
|
||||
client.networks.list.return_value = [other]
|
||||
remove_macvlan_network(client)
|
||||
other.remove.assert_not_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# teardown_host_macvlan
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestTeardownHostMacvlan:
|
||||
@patch("decnet.network.os.geteuid", return_value=0)
|
||||
@patch("decnet.network._run")
|
||||
def test_deletes_macvlan_iface(self, mock_run, _):
|
||||
mock_run.return_value = MagicMock(returncode=0)
|
||||
teardown_host_macvlan("192.168.1.96/27")
|
||||
calls = [str(c) for c in mock_run.call_args_list]
|
||||
assert any(HOST_MACVLAN_IFACE in c for c in calls)
|
||||
|
||||
@patch("decnet.network.os.geteuid", return_value=1)
|
||||
def test_requires_root(self, _):
|
||||
with pytest.raises(PermissionError):
|
||||
teardown_host_macvlan("192.168.1.96/27")
|
||||
|
||||
Reference in New Issue
Block a user