mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
feat(docker): expose netem-extension packet filters on kernel links
Surface the netem extensions delivered in uBridge feature/tc-netem-ext as GNS3 filter types: rate (tc-style bandwidth string), reorder (needs delay), gemodel (Gilbert-Elliot bursty loss, exclusive with packet_loss), duplicate, seed and limit, plus the delay jitter distribution and the loss/dup correlation parameters. All are kernel-datapath only — the relay's packet-filter registry has no equivalent, so available_filters hides them on relay links, setting one on a created relay link returns 409, and the project-load path drops them with a warning (update_filters runs before the datapath is decided, so it cannot reject there). Validation mirrors the tc grammar: reorder requires delay, gemodel and packet_loss are mutually exclusive, a distribution requires jitter > 0, rate is integer+unit capped at 100gbit. The translation in DockerVM._netem_command_parts emits the frozen grammar order and gates extension keywords on the 'tc capabilities' netem token list (probed once per uBridge process; a plain delay/loss/corrupt/dup filter never probes, keeping old uBridge builds untouched; the correl suffix has no token and gates on 'rate' as the extension-build marker). Reconcile became reset-before-set: the kernel's netem change merges optional attributes (rate, correlation, reorder, corrupt, gemodel, distribution, seed keep their previous value when the new message omits them), so removing a parameter needs an explicit 'tc reset' — the bpf_drop filters are re-added right after in the same flow, so the clsact churn is invisible. The capability check runs before the reset so a failed update leaves the previous qdisc in place. e2e (isolated instance, kernel link): 24/24 — rate 512kbit adds exactly 2x the per-packet serialization to the RTT (45.5 ms for 1400-byte pings), paretonormal jitter + rate-cleared leak check, reorder gap 5, gemodel 50% bursty loss, duplicate, loss 30% correl 50%, seed 42 + limit 5000 visible in the qdisc dump, suspend/resume and node restart keep the filters, relay link rejects rate with 409, teardown leaves no netem. Full unit suite: 1916 passed.
This commit is contained in:
13 files changed
+1012
-108
No files matched your search
@@ -846,3 +846,184 @@ def test_veth_names_deterministic_and_bounded(vm):
|
||||
# multi-port adapters can reach two-digit adapter/port numbers
|
||||
long_host, long_guest = vm._veth_names(12, 8)
|
||||
assert len(long_host) <= 15 and len(long_guest) <= 15
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P6a: netem extensions (rate / reorder / gemodel / duplicate / seed / limit /
|
||||
# distribution / correlation) — translation and capability gating
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _caps_hypervisor_netem(tokens="delay,jitter,loss,dup,corrupt,rate,reorder,gemodel,dist,seed,limit"):
|
||||
"""A hypervisor mock whose direct send() answers `tc capabilities` with
|
||||
the full netem-extension surface."""
|
||||
hyp = MagicMock()
|
||||
hyp.is_running.return_value = True
|
||||
hyp.send = AsyncioMagicMock(return_value=[f"netem={tokens};ebpf=1;cbpf=1"])
|
||||
return hyp
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_applies_netem_extensions(vm):
|
||||
"""
|
||||
The netem-extension filters merge into the single netem qdisc command,
|
||||
in the frozen grammar order.
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = _caps_hypervisor_netem()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
nio.filters = {
|
||||
"delay": [100, 20, "normal"],
|
||||
"packet_loss": [5, 25],
|
||||
"duplicate": [10, 50],
|
||||
"corrupt": [2],
|
||||
"reorder": [25, 50, 5],
|
||||
"rate": ["512kbit"],
|
||||
"limit": [5000],
|
||||
"seed": [42],
|
||||
}
|
||||
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
vm._ubridge_send.assert_any_call(
|
||||
f'tc netem set "{host_ifc}" delay 100 jitter 20 loss 5 correl 25 dup 10 correl 50 corrupt 2 '
|
||||
"reorder 25 correl 50 gap 5 rate 512kbit limit 5000 distribution normal seed 42"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_nio_kernel_applies_gemodel(vm):
|
||||
"""
|
||||
gemodel replaces the plain loss keyword and is mutually exclusive with
|
||||
packet_loss (the controller validates; the translation just prefers
|
||||
gemodel when both somehow arrive).
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = _caps_hypervisor_netem()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
vm.status = "started"
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
vm._kernel_veths[(0, 0)] = host_ifc
|
||||
nio = vm.manager.create_nio({"type": "nio_bridge", "bridge": BRIDGE})
|
||||
vm._ethernet_adapters[0].add_nio(0, nio)
|
||||
nio.filters = {"delay": [50, 5], "gemodel": [100, 0, 30]}
|
||||
|
||||
await vm.adapter_update_nio_binding(0, nio)
|
||||
vm._ubridge_send.assert_any_call(f'tc netem set "{host_ifc}" delay 50 jitter 5 loss gemodel 100 0 30')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_netem_extension_needs_capability(vm):
|
||||
"""
|
||||
Extension keywords on an old uBridge (no tc capabilities / no netem-ext
|
||||
tokens) raise a clear upgrade error instead of a raw parse failure.
|
||||
"""
|
||||
|
||||
# old uBridge: no tc module at all -> empty capabilities
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_hypervisor.is_running.return_value = True
|
||||
vm._ubridge_hypervisor.send = AsyncioMagicMock(side_effect=UbridgeError("201-Unknown command"))
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
|
||||
with pytest.raises(DockerError, match="netem extensions"):
|
||||
await vm._ubridge_apply_netem(host_ifc, {"rate": ["512kbit"]})
|
||||
vm._ubridge_send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_netem_extension_partial_capability(vm):
|
||||
"""
|
||||
A uBridge that reports some but not all needed tokens fails on the
|
||||
missing ones (per-token check, not just build detection).
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = _caps_hypervisor_netem(tokens="delay,jitter,loss,dup,corrupt")
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
|
||||
with pytest.raises(DockerError, match="rate, reorder"):
|
||||
await vm._ubridge_apply_netem(host_ifc, {"delay": [50], "reorder": [25], "rate": ["1mbit"]})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_netem_plain_surface_never_probes(vm):
|
||||
"""
|
||||
Original-surface filters (delay/loss/corrupt, and plain dup) must not
|
||||
touch the capability probe at all — an old uBridge serves them as-is.
|
||||
The reset-before-set is unconditional (kernel netem replace merges
|
||||
optional attrs, so a removed parameter would otherwise leak).
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = MagicMock()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
|
||||
await vm._ubridge_apply_netem(host_ifc, {"delay": [50, 5], "packet_loss": [10], "duplicate": [10], "corrupt": [2]})
|
||||
assert vm._ubridge_send.call_args_list == [
|
||||
call(f'tc reset "{host_ifc}"'),
|
||||
call(f'tc netem set "{host_ifc}" delay 50 jitter 5 loss 10 dup 10 corrupt 2'),
|
||||
]
|
||||
vm._ubridge_hypervisor.send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_netem_reset_leak_on_removal(vm):
|
||||
"""
|
||||
A re-apply with a parameter REMOVED (rate gone, delay kept) must reset
|
||||
first: the kernel's netem change merges optional attributes, so a bare
|
||||
replace would keep the old rate shaping forever.
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = _caps_hypervisor_netem()
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
|
||||
await vm._ubridge_apply_netem(host_ifc, {"delay": [50], "rate": ["1mbit"]})
|
||||
vm._ubridge_send.reset_mock()
|
||||
await vm._ubridge_apply_netem(host_ifc, {"delay": [50]})
|
||||
|
||||
assert vm._ubridge_send.call_args_list == [
|
||||
call(f'tc reset "{host_ifc}"'),
|
||||
call(f'tc netem set "{host_ifc}" delay 50'),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_apply_netem_correl_gates_on_extension_build(vm):
|
||||
"""
|
||||
Loss/dup correlation is an extension (the capabilities string has no
|
||||
correl token) — it gates on the netem-extension build marker "rate".
|
||||
"""
|
||||
|
||||
vm._ubridge_hypervisor = _caps_hypervisor_netem(tokens="delay,jitter,loss,dup,corrupt")
|
||||
vm._ubridge_send = AsyncioMagicMock()
|
||||
host_ifc, _ = vm._veth_names(0, 0)
|
||||
|
||||
with pytest.raises(DockerError, match="netem extensions"):
|
||||
await vm._ubridge_apply_netem(host_ifc, {"packet_loss": [5, 25]})
|
||||
|
||||
|
||||
def test_create_nio_bridge_accepts_netem_extension_filters(vm):
|
||||
|
||||
nio = vm.manager.create_nio(
|
||||
{"type": "nio_bridge", "bridge": BRIDGE, "filters": {"rate": ["512kbit"], "reorder": [25, 0, 5]}}
|
||||
)
|
||||
assert isinstance(nio, NIOBridge)
|
||||
assert nio.filters == {"rate": ["512kbit"], "reorder": [25, 0, 5]}
|
||||
|
||||
|
||||
def test_create_nio_udp_rejects_kernel_only_filters(vm):
|
||||
"""
|
||||
The relay NIO rejects netem-extension filter types (second guard behind
|
||||
the controller).
|
||||
"""
|
||||
|
||||
with pytest.raises(ComputeError, match="kernel-datapath"):
|
||||
vm.manager.create_nio(
|
||||
{"type": "nio_udp", "lport": 4242, "rport": 4343, "rhost": "127.0.0.1", "filters": {"gemodel": [100, 0, 30]}}
|
||||
)
|
||||
@@ -169,6 +169,20 @@ async def test_ubridge_apply_filters(node):
|
||||
node._ubridge_send.assert_any_call("bridge add_packet_filter VPCS-10 filter0 latency 10")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ubridge_apply_filters_rejects_kernel_only(node):
|
||||
"""
|
||||
Netem-extension filters have no relay equivalent: applying them on a
|
||||
relay bridge must fail loudly instead of pushing an unknown filter type
|
||||
to uBridge.
|
||||
"""
|
||||
|
||||
node._ubridge_send = AsyncioMagicMock()
|
||||
with pytest.raises(NodeError, match="kernel-datapath"):
|
||||
await node._ubridge_apply_filters("VPCS-10", {"delay": [100, 10], "rate": ["512kbit"]})
|
||||
node._ubridge_send.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ubridge_apply_bpf_filters(node):
|
||||
|
||||
|
||||
@@ -52,6 +52,22 @@ async def _kernel_link(project, **kwargs):
|
||||
return link, node1, node2
|
||||
|
||||
|
||||
def _mock_compute_http(compute):
|
||||
"""Wire the async compute HTTP callbacks the relay _prepare path needs."""
|
||||
|
||||
async def subnet_callback(other):
|
||||
return ("192.168.1.1", "192.168.1.2")
|
||||
|
||||
compute.get_ip_on_same_subnet.side_effect = subnet_callback
|
||||
|
||||
async def port_callback(path, data=None, **kwargs):
|
||||
response = MagicMock()
|
||||
response.json = {"udp_port": 1024}
|
||||
return response
|
||||
|
||||
compute.post.side_effect = port_callback
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_eligible(project):
|
||||
|
||||
@@ -380,3 +396,108 @@ async def test_update_suspend_kernel_link_sends_no_synthetic_filter(project):
|
||||
for body in bodies:
|
||||
assert body["filters"] == {}
|
||||
assert body["suspend"] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# P6a: netem-extension filters (rate / reorder / gemodel / duplicate / seed /
|
||||
# limit / distribution / correlation)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_kernel_datapath_eligible_with_netem_extension_filters(project):
|
||||
"""
|
||||
All netem-extension types have a tc netem equivalent — they keep the
|
||||
link kernel-eligible (only frequency_drop still forces the relay).
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
link._filters = {
|
||||
"delay": [100, 20, "normal"],
|
||||
"packet_loss": [5, 25],
|
||||
"duplicate": [10],
|
||||
"reorder": [25, 0, 5],
|
||||
"rate": ["512kbit"],
|
||||
"gemodel": [100, 0, 30],
|
||||
"seed": [42],
|
||||
"limit": [5000],
|
||||
}
|
||||
# gemodel/packet_loss are mutually exclusive — drop packet_loss for the
|
||||
# eligibility check (validation rejects the mix elsewhere)
|
||||
link._filters.pop("packet_loss")
|
||||
assert link._kernel_datapath_eligible(node1, node2) is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_netem_extension_filters_accepted_on_kernel_link(project):
|
||||
"""
|
||||
The extension filters ride the kernel NIO to both endpoints like the
|
||||
base netem surface.
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
await link._prepare()
|
||||
|
||||
filters = {"delay": [100, 20, "normal"], "rate": ["512kbit"], "reorder": [25, 0, 5]}
|
||||
await link.update_filters(filters)
|
||||
assert link.filters == filters
|
||||
by_node = {entry[0].id: entry[3] for entry in (await link._prepare())}
|
||||
assert by_node[node1.id]["filters"] == filters
|
||||
assert by_node[node2.id]["filters"] == filters
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_kernel_only_filters_rejected_on_relay_link(project):
|
||||
"""
|
||||
Netem-extension filters have no relay equivalent: setting them on a
|
||||
created relay link is a 409. (frequency_drop in the stored filters
|
||||
forces this link onto the relay datapath.)
|
||||
"""
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
_mock_compute_http(link._nodes[0]["node"].compute)
|
||||
link._filters = {"frequency_drop": [7]}
|
||||
await link._prepare()
|
||||
assert link.kernel_datapath is False
|
||||
link._created = True # the computes hold the UDP NIOs
|
||||
|
||||
with pytest.raises(ControllerError, match="kernel-datapath"):
|
||||
await link.update_filters({"rate": ["512kbit"]})
|
||||
# the stored filters are untouched
|
||||
assert link.filters == {"frequency_drop": [7]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_kernel_only_filters_accepted_before_creation(project):
|
||||
"""
|
||||
While loading a project the datapath is not decided yet (update_filters
|
||||
runs before _prepare) — a link that will be kernel-wired must accept the
|
||||
extension filters. Only _prepare decides, dropping what the relay cannot
|
||||
run.
|
||||
"""
|
||||
|
||||
link, _n1, _n2 = await _kernel_link(project)
|
||||
await link.update_filters({"rate": ["512kbit"], "reorder": [25, 0, 5], "delay": [100, 10]})
|
||||
assert link.filters["rate"] == ["512kbit"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prepare_relay_link_strips_kernel_only_filters(project):
|
||||
"""
|
||||
A relay-wired link (here: frequency_drop in the stored filters) built
|
||||
from a topology carrying extension filters drops them with a warning
|
||||
instead of pushing an unknown filter type to uBridge.
|
||||
"""
|
||||
|
||||
link, node1, node2 = await _kernel_link(project)
|
||||
_mock_compute_http(link._nodes[0]["node"].compute)
|
||||
link._filters = {"frequency_drop": [7], "rate": ["512kbit"], "delay": [100, 20, "normal"], "packet_loss": [5, 25]}
|
||||
entries = await link._prepare()
|
||||
assert link.kernel_datapath is False
|
||||
by_node = {entry[0].id: entry[3] for entry in entries}
|
||||
assert by_node[node1.id]["type"] == "nio_udp"
|
||||
# the relay's filter node keeps what it can run; rate, the distribution
|
||||
# and the loss correlation are kernel-only and stripped (the far side
|
||||
# carries no filters — the relay filters one bridge)
|
||||
assert by_node[node1.id]["filters"] == {"frequency_drop": [7], "delay": [100, 20], "packet_loss": [5]}
|
||||
assert by_node[node2.id]["filters"] == {}
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from gns3server.utils.packet_filter_validation import (
|
||||
FilterValidationError,
|
||||
filter_inactive_filters,
|
||||
split_kernel_only_features,
|
||||
validate_all_filters,
|
||||
validate_filter_parameters,
|
||||
)
|
||||
@@ -131,9 +132,11 @@ class TestPacketFilterValidation:
|
||||
with pytest.raises(FilterValidationError, match="expects 1 parameter"):
|
||||
validate_filter_parameters("frequency_drop", [1, 2])
|
||||
|
||||
# delay expects 2 parameters
|
||||
with pytest.raises(FilterValidationError, match="expects 2 parameter"):
|
||||
validate_filter_parameters("delay", [100])
|
||||
# delay takes 1 to 3 parameters (latency, optional jitter,
|
||||
# optional distribution) — latency alone is valid, a 4th is not
|
||||
validate_filter_parameters("delay", [100])
|
||||
with pytest.raises(FilterValidationError, match="expects 1 to 3 parameter"):
|
||||
validate_filter_parameters("delay", [100, 50, "normal", "extra"])
|
||||
|
||||
def test_string_to_int_conversion(self):
|
||||
"""Test string to integer conversion."""
|
||||
@@ -258,3 +261,108 @@ class TestFilterInactiveFilters:
|
||||
"""Test None filters."""
|
||||
result = filter_inactive_filters(None)
|
||||
assert result == {}
|
||||
|
||||
|
||||
class TestNetemExtensionFilters:
|
||||
"""P6a: the netem-extension filter types (rate, reorder, gemodel,
|
||||
duplicate, seed, limit) and the kernel-only parameter extensions."""
|
||||
|
||||
def test_rate_valid(self):
|
||||
for value in ("512kbit", "10mbit", "1gbit", "64000bps", "8000kbps", "100mbps", "1544bit"):
|
||||
validate_filter_parameters("rate", [value])
|
||||
|
||||
def test_rate_invalid(self):
|
||||
# bare number, unknown unit, float, over 100gbit
|
||||
for value in ("512", "512kbits", "0.5mbit", "101gbit", "", "mbit"):
|
||||
with pytest.raises(FilterValidationError, match="Rate"):
|
||||
validate_filter_parameters("rate", [value])
|
||||
|
||||
def test_rate_rejects_non_string(self):
|
||||
with pytest.raises(FilterValidationError, match="must be a string"):
|
||||
validate_filter_parameters("rate", [512000])
|
||||
|
||||
def test_reorder_valid(self):
|
||||
validate_filter_parameters("reorder", [25])
|
||||
validate_filter_parameters("reorder", [25, 50])
|
||||
validate_filter_parameters("reorder", [25, 50, 5])
|
||||
|
||||
def test_reorder_invalid_ranges(self):
|
||||
with pytest.raises(FilterValidationError, match="Reorder"):
|
||||
validate_filter_parameters("reorder", [101])
|
||||
with pytest.raises(FilterValidationError, match="Gap"):
|
||||
validate_filter_parameters("reorder", [25, 0, 0]) # gap < 1
|
||||
with pytest.raises(FilterValidationError, match="Gap"):
|
||||
validate_filter_parameters("reorder", [25, 0, 1001])
|
||||
|
||||
def test_reorder_requires_delay(self):
|
||||
with pytest.raises(FilterValidationError, match="reorder requires delay"):
|
||||
validate_all_filters({"reorder": [25]})
|
||||
# with delay present it passes
|
||||
validate_all_filters({"reorder": [25, 0, 5], "delay": [100, 10]})
|
||||
|
||||
def test_gemodel_valid_and_exclusive(self):
|
||||
validate_filter_parameters("gemodel", [100])
|
||||
validate_filter_parameters("gemodel", [100, 0])
|
||||
validate_filter_parameters("gemodel", [100, 0, 30])
|
||||
with pytest.raises(FilterValidationError, match="mutually exclusive"):
|
||||
validate_all_filters({"gemodel": [100, 0, 30], "packet_loss": [10]})
|
||||
|
||||
def test_gemodel_invalid_range(self):
|
||||
with pytest.raises(FilterValidationError, match="bad-state"):
|
||||
validate_filter_parameters("gemodel", [101, 0, 30])
|
||||
|
||||
def test_duplicate_valid(self):
|
||||
validate_filter_parameters("duplicate", [10])
|
||||
validate_filter_parameters("duplicate", [10, 25])
|
||||
|
||||
def test_seed_and_limit(self):
|
||||
validate_filter_parameters("seed", [42])
|
||||
validate_filter_parameters("seed", [4294967295])
|
||||
with pytest.raises(FilterValidationError, match="Seed"):
|
||||
validate_filter_parameters("seed", [4294967296])
|
||||
validate_filter_parameters("limit", [5000])
|
||||
with pytest.raises(FilterValidationError, match="Limit"):
|
||||
validate_filter_parameters("limit", [0])
|
||||
with pytest.raises(FilterValidationError, match="Limit"):
|
||||
validate_filter_parameters("limit", [1000001])
|
||||
|
||||
def test_delay_distribution(self):
|
||||
for dist in ("uniform", "normal", "pareto", "paretonormal"):
|
||||
validate_filter_parameters("delay", [100, 20, dist])
|
||||
with pytest.raises(FilterValidationError, match="Distribution"):
|
||||
validate_filter_parameters("delay", [100, 20, "poisson"])
|
||||
# distribution without jitter does nothing -> rejected
|
||||
with pytest.raises(FilterValidationError, match="requires a Jitter"):
|
||||
validate_filter_parameters("delay", [100, 0, "normal"])
|
||||
# empty distribution = not set
|
||||
validate_filter_parameters("delay", [100, 20, ""])
|
||||
|
||||
def test_packet_loss_correlation(self):
|
||||
validate_filter_parameters("packet_loss", [10])
|
||||
validate_filter_parameters("packet_loss", [10, 25])
|
||||
with pytest.raises(FilterValidationError, match="Correlation"):
|
||||
validate_filter_parameters("packet_loss", [10, 101])
|
||||
|
||||
def test_kernel_only_features(self):
|
||||
clean, dropped = split_kernel_only_features(
|
||||
{
|
||||
"delay": [100, 20, "normal"],
|
||||
"packet_loss": [10, 25],
|
||||
"rate": ["512kbit"],
|
||||
"corrupt": [2],
|
||||
}
|
||||
)
|
||||
assert dropped == {"rate", "delay distribution", "packet_loss correlation"}
|
||||
assert clean == {"delay": [100, 20], "packet_loss": [10], "corrupt": [2]}
|
||||
|
||||
def test_kernel_only_features_all_relay_safe(self):
|
||||
clean, dropped = split_kernel_only_features(
|
||||
{"delay": [100, 20, ""], "packet_loss": [10, 0], "corrupt": [2], "bpf": ["icmp"]}
|
||||
)
|
||||
assert dropped == set()
|
||||
# zero correlation and empty distribution are kept as-is (no-ops)
|
||||
assert clean == {"delay": [100, 20, ""], "packet_loss": [10, 0], "corrupt": [2], "bpf": ["icmp"]}
|
||||
|
||||
def test_kernel_only_features_empty(self):
|
||||
assert split_kernel_only_features({}) == ({}, set())
|
||||
assert split_kernel_only_features(None) == ({}, set())
|
||||
Reference in new issue
Block a user