- Agent: started_at comes from inspect's State.StartedAt (nanoseconds
trimmed; Docker's zero time is "never started"), not the list's Created,
so a restarted container's uptime counts from its restart.
- A rising restart_count between two snapshots emits a "restart" event
("restart #3", or "3 restarts, now #5"), said instead of a start; a
restart then a death gives both. A count that falls (recreated under the
same name) says nothing. The timeline draws it as ↻.
- Network and disk I/O count what the kernel calls physical: interfaces
not under /sys/devices/virtual/net (bridges, docker0, veth, vxlan, tun),
block devices with a device behind them (not dm, md, zram), so a byte is
not counted twice. Where /sys calls everything virtual (an agent in LXC)
the name-filtered set is kept rather than reporting no traffic.
- Agent tests updated for #5152's (status, detail, containers) and
(is_manager, topology) shapes; the no-Docker disk test now states the
case it was passing for by accident.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
6.1 KiB
Python
149 lines
6.1 KiB
Python
"""Unit tests for the host agent's new metric collectors / parsers.
|
|
|
|
Pure parsers and the rate helper are tested directly; the file-reading
|
|
collectors are tested by monkeypatching agent._read_file with fixtures.
|
|
"""
|
|
import plugins.host_agent.agent as agent
|
|
|
|
|
|
def test_parse_cpu_lines_aggregate_and_per_core():
|
|
text = (
|
|
"cpu 100 0 100 800 0 0 0\n"
|
|
"cpu0 50 0 50 400 0\n"
|
|
"cpu1 50 0 50 400 0\n"
|
|
"intr 12345\n"
|
|
)
|
|
parsed = agent._parse_cpu_lines(text)
|
|
assert set(parsed) == {"cpu", "cpu0", "cpu1"}
|
|
assert parsed["cpu"] == (1000, 800) # total, idle(=idle+iowait)
|
|
|
|
|
|
def test_collect_cpu_returns_aggregate_and_cores(monkeypatch):
|
|
snapshots = iter([
|
|
"cpu 100 0 100 800\ncpu0 50 0 50 400\ncpu1 50 0 50 400\n",
|
|
"cpu 200 0 200 1200\ncpu0 100 0 100 600\ncpu1 100 0 100 600\n",
|
|
])
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: next(snapshots))
|
|
monkeypatch.setattr(agent.time, "sleep", lambda s: None)
|
|
agg, cores = agent.collect_cpu()
|
|
assert isinstance(agg, float)
|
|
assert len(cores) == 2
|
|
|
|
|
|
def test_parse_psi():
|
|
text = (
|
|
"some avg10=1.50 avg60=2.00 avg300=3.00 total=12345\n"
|
|
"full avg10=0.50 avg60=0.10 avg300=0.00 total=678\n"
|
|
)
|
|
r = agent._parse_psi(text)
|
|
assert r["some_avg10"] == 1.5
|
|
assert r["some_avg60"] == 2.0
|
|
assert r["full_avg10"] == 0.5
|
|
|
|
|
|
def test_rates_basic_and_counter_reset():
|
|
cur = {"eth0": (2000, 1000), "sda": (5000, 0)}
|
|
prev = {"eth0": (1000, 500), "sda": (10000, 0)} # sda counter went backwards
|
|
out = agent._rates(cur, prev, dt=10.0)
|
|
assert out["eth0"] == (100.0, 50.0)
|
|
assert "sda" not in out # negative delta dropped
|
|
assert agent._rates(cur, prev, dt=0.0) == {} # no elapsed time → no rates
|
|
|
|
|
|
def test_rates_skips_names_missing_from_prev():
|
|
assert agent._rates({"new": (5, 5)}, {}, dt=1.0) == {}
|
|
|
|
|
|
def test_collect_net_raw_skips_lo_and_veth(monkeypatch, tmp_path):
|
|
# /sys can't say (no such tree): the name filter alone decides.
|
|
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR", str(tmp_path / "absent"))
|
|
fixture = (
|
|
"Inter-| Receive\n"
|
|
" face |bytes\n"
|
|
" lo: 100 1 0 0 0 0 0 0 200 2 0 0 0 0 0 0\n"
|
|
" eth0: 1000 5 0 0 0 0 0 0 500 4 0 0 0 0 0 0\n"
|
|
" veth9: 9 0 0 0 0 0 0 0 9 0 0 0 0 0 0 0\n"
|
|
)
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: fixture)
|
|
assert agent.collect_net_raw() == {"eth0": (1000, 500)}
|
|
|
|
|
|
def test_collect_diskio_raw_whole_disks_only(monkeypatch, tmp_path):
|
|
monkeypatch.setattr(agent, "SYS_BLOCK_DIR", str(tmp_path / "absent"))
|
|
# fields: major minor name reads rd_merged sectors_read ms ... sectors_written ...
|
|
fixture = (
|
|
" 8 0 sda 1 0 10 0 1 0 20 0 0 0 0\n"
|
|
" 8 1 sda1 1 0 5 0 1 0 5 0 0 0 0\n"
|
|
" 259 0 nvme0n1 1 0 100 0 1 0 200 0 0 0 0\n"
|
|
" 7 0 loop0 1 0 1 0 1 0 1 0 0 0 0\n"
|
|
)
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: fixture)
|
|
out = agent.collect_diskio_raw()
|
|
assert out == {"sda": (10 * 512, 20 * 512), "nvme0n1": (100 * 512, 200 * 512)}
|
|
|
|
|
|
_NETDEV = (
|
|
"Inter-| Receive\n"
|
|
" face |bytes\n"
|
|
" lo: 100 1 0 0 0 0 0 0 200 2 0 0 0 0 0 0\n"
|
|
" eno1: 1000 5 0 0 0 0 0 0 500 4 0 0 0 0 0 0\n"
|
|
"docker0: 70 1 0 0 0 0 0 0 80 1 0 0 0 0 0 0\n"
|
|
"br-1a2b: 30 1 0 0 0 0 0 0 40 1 0 0 0 0 0 0\n"
|
|
"vxlan0: 10 1 0 0 0 0 0 0 20 1 0 0 0 0 0 0\n"
|
|
)
|
|
|
|
|
|
def _sys_net(tmp_path, virtual):
|
|
d = tmp_path / "virtual-net"
|
|
for name in virtual:
|
|
(d / name).mkdir(parents=True)
|
|
return str(d)
|
|
|
|
|
|
def test_collect_net_raw_counts_physical_interfaces_only(monkeypatch, tmp_path):
|
|
# Bridge, docker0 and vxlan traffic also crosses eno1: counting them too
|
|
# would count those bytes twice in the host total (#5153).
|
|
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR",
|
|
_sys_net(tmp_path, ["lo", "docker0", "br-1a2b", "vxlan0"]))
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: _NETDEV)
|
|
assert agent.collect_net_raw() == {"eno1": (1000, 500)}
|
|
|
|
|
|
def test_collect_net_raw_keeps_everything_when_sys_calls_it_all_virtual(monkeypatch, tmp_path):
|
|
# Inside LXC eth0 is a veth peer: all virtual. No traffic at all would be wrong.
|
|
netdev = "h\nh\n eth0: 1000 5 0 0 0 0 0 0 500 4 0 0 0 0 0 0\n"
|
|
monkeypatch.setattr(agent, "SYS_VIRTUAL_NET_DIR", _sys_net(tmp_path, ["lo", "eth0"]))
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: netdev)
|
|
assert agent.collect_net_raw() == {"eth0": (1000, 500)}
|
|
|
|
|
|
def test_collect_diskio_raw_counts_physical_disks_only(monkeypatch, tmp_path):
|
|
# dm-0 (LVM on sda) and md0 (RAID over sdb, sdc) repeat their members' I/O.
|
|
block = tmp_path / "block"
|
|
for name in ("sda", "sdb", "nvme0n1"):
|
|
(block / name / "device").mkdir(parents=True)
|
|
for name in ("dm-0", "md0", "zram0"):
|
|
(block / name).mkdir(parents=True)
|
|
diskstats = "".join(f" 8 0 {n} 1 0 10 0 1 0 20 0 0 0 0\n"
|
|
for n in ("sda", "sdb", "nvme0n1", "dm-0", "md0", "zram0"))
|
|
monkeypatch.setattr(agent, "SYS_BLOCK_DIR", str(block))
|
|
monkeypatch.setattr(agent, "_read_file", lambda p: diskstats)
|
|
assert sorted(agent.collect_diskio_raw()) == ["nvme0n1", "sda", "sdb"]
|
|
|
|
|
|
def test_build_sample_first_call_has_no_rates_but_keeps_state(monkeypatch):
|
|
monkeypatch.setattr(agent, "collect_cpu", lambda: (5.0, [5.0]))
|
|
monkeypatch.setattr(agent, "collect_memory", lambda: {"total_bytes": 10, "available_bytes": 5})
|
|
monkeypatch.setattr(agent, "collect_load", lambda: {"1m": 0.0, "5m": 0.0, "15m": 0.0})
|
|
monkeypatch.setattr(agent, "collect_uptime", lambda: 1)
|
|
monkeypatch.setattr(agent, "collect_storage", lambda m: [])
|
|
monkeypatch.setattr(agent, "collect_temps", lambda: [])
|
|
monkeypatch.setattr(agent, "collect_psi", lambda: {})
|
|
monkeypatch.setattr(agent, "collect_net_raw", lambda: {"eth0": (1000, 500)})
|
|
monkeypatch.setattr(agent, "collect_diskio_raw", lambda: {"sda": (10, 20)})
|
|
state: dict = {}
|
|
s1 = agent.build_sample(["/"], state)
|
|
assert s1["net"] == [] and s1["diskio"] == [] # no prior counters yet
|
|
assert state["mono"] is not None
|
|
assert state["net"] == {"eth0": (1000, 500)}
|