import { SystemResources } from "@umt/contracts"; import { describe, expect, it } from "vitest"; import { bucketSamples, computePoint, createProcessCpuMeter, createResourceSampler, parseCgroupBytes, parseCgroupCpuMax, parseCgroupCpuUsage, parseDiskstats, parseIoStat, parseLoadavg, parseMeminfo, parseNetDev, parseProcStat, type ReadText, readResources, } from "../src/modules/system/resources.ts"; // 资源监控采集:/proc 与 cgroup v2 的解析、速率计算、降级与采样器(不碰 PG;读文件用注入的假实现)。 const STAT = (user: number, idle: number) => `cpu ${user} 0 100 ${idle} 50 0 0 0 0 0\ncpu0 1 2 3 4 5 6 7 8 0 0\n`; const MEMINFO = `MemTotal: 16000000 kB MemFree: 2000000 kB MemAvailable: 8000000 kB SwapTotal: 1000000 kB SwapFree: 600000 kB `; const NETDEV = (rx: number, tx: number) => `Inter-| Receive | Transmit face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed lo: 999999 10 0 0 0 0 0 0 999999 10 0 0 0 0 0 0 eth0: ${rx} 100 0 0 0 0 0 0 ${tx} 80 0 0 0 0 0 0 `; const IOSTAT = (r: number, w: number) => `8:0 rbytes=${r} wbytes=${w} rios=10 wios=5 dbytes=0 dios=0\n259:0 rbytes=0 wbytes=0\n`; /** 假文件系统:按路径给内容,没给的读不到 */ function fakeFs(files: Record): ReadText { return (path) => Promise.resolve(files[path] ?? null); } describe("解析", () => { it("/proc 各文件", () => { expect(parseProcStat(STAT(1000, 8000))).toEqual({ total: 1000 + 100 + 8000 + 50, idle: 8050 }); expect(parseProcStat("garbage")).toBeNull(); expect(parseMeminfo(MEMINFO)).toEqual({ totalBytes: 16000000 * 1024, availableBytes: 8000000 * 1024, swapUsedBytes: 400000 * 1024, }); expect(parseMeminfo(null)).toBeNull(); expect(parseLoadavg("0.52 0.40 0.31 2/345 6789")).toEqual([0.52, 0.4, 0.31]); expect(parseLoadavg("")).toBeNull(); // lo 不计 expect(parseNetDev(NETDEV(5000, 3000))).toEqual({ rx: 5000, tx: 3000 }); // 整盘才计(分区、loop 不计) expect( parseDiskstats( " 8 0 sda 100 0 2000 0 50 0 4000 0 0 0 0\n 8 1 sda1 100 0 2000 0 50 0 4000 0 0 0 0\n 7 0 loop0 1 0 8 0 0 0 0 0 0 0 0\n", ), ).toEqual({ read: 2000 * 512, write: 4000 * 512 }); }); it("cgroup v2", () => { expect(parseCgroupCpuUsage("usage_usec 123456\nuser_usec 1\n")).toBe(123456); expect(parseCgroupCpuMax("max 100000")).toBeNull(); expect(parseCgroupCpuMax("200000 100000\n")).toBe(2); expect(parseCgroupBytes("max\n")).toBeNull(); expect(parseCgroupBytes("1048576\n")).toBe(1048576); expect(parseIoStat(IOSTAT(4096, 8192))).toEqual({ read: 4096, write: 8192 }); expect(parseIoStat("")).toBeNull(); }); }); describe("读数与速率", () => { it("容器:CPU 按 cgroup 用量 / 核数,内存扣掉 inactive_file,磁盘读写取 io.stat", async () => { const files = (usage: number, user: number, idle: number, rx: number, io: number) => fakeFs({ "/proc/stat": STAT(user, idle), "/sys/fs/cgroup/cpu.stat": `usage_usec ${usage}\n`, "/sys/fs/cgroup/cpu.max": "200000 100000", "/proc/loadavg": "1.00 0.50 0.25 1/2 3", "/proc/meminfo": MEMINFO, "/sys/fs/cgroup/memory.current": String(600 * 1024 * 1024), "/sys/fs/cgroup/memory.max": String(1024 * 1024 * 1024), "/sys/fs/cgroup/memory.stat": `anon 1\ninactive_file ${100 * 1024 * 1024}\n`, "/proc/net/dev": NETDEV(rx, rx / 2), "/sys/fs/cgroup/io.stat": IOSTAT(io, io * 2), "/proc/uptime": "3600.5 7000.1", }); const a = await readResources(files(0, 1000, 9000, 0, 0), 0); const b = await readResources(files(5_000_000, 2000, 10000, 10_000_000, 1_000_000), 5000); const first = computePoint(null, a); expect(first.sample.cpuPercent).toBeNull(); expect(first.sample.netRxBps).toBeNull(); const p = computePoint(a, b); // 5 秒用了 5 秒 CPU,限额 2 核 → 50% expect(p.current.cpu).toMatchObject({ scope: "cgroup", percent: 50, cores: 2, load: [1, 0.5, 0.25] }); // 整机:total 增 2000、idle 增 1000 → 50% expect(p.current.cpu.hostPercent).toBe(50); expect(p.current.memory).toMatchObject({ scope: "cgroup", usedBytes: 500 * 1024 * 1024, limitBytes: 1024 * 1024 * 1024, percent: 48.8, }); expect(p.sample).toMatchObject({ netRxBps: 2_000_000, netTxBps: 1_000_000, diskReadBps: 200_000 }); expect(p.current.diskIo).toMatchObject({ scope: "cgroup", writeBps: 400_000 }); expect(p.current.hostUptimeSec).toBe(3600.5); }); it("宿主(没有 cgroup 文件):退回 /proc 整机口径", async () => { const files = (user: number, idle: number) => fakeFs({ "/proc/stat": STAT(user, idle), "/proc/meminfo": MEMINFO, "/proc/net/dev": NETDEV(0, 0) }); const p = computePoint(await readResources(files(1000, 9000), 0), await readResources(files(1500, 9500), 1000)); expect(p.current.cpu).toMatchObject({ scope: "host", percent: 50 }); expect(p.current.memory).toMatchObject({ scope: "host", usedBytes: 8000000 * 1024, percent: 50 }); expect(p.current.diskIo).toMatchObject({ scope: "none", readBps: null }); expect(p.current.network.scope).toBe("host"); }); it("什么都读不到:全部为 null,不抛错", async () => { const none = fakeFs({}); const p = computePoint(await readResources(none, 0), await readResources(none, 1000)); expect(p.current.cpu).toMatchObject({ scope: "none", percent: null, hostPercent: null, load: null }); expect(p.current.memory).toMatchObject({ scope: "none", usedBytes: null, percent: null }); expect(p.current.network).toMatchObject({ scope: "none", rxBps: null }); }); it("计数器回绕 / 重置时速率为 null,不出负数", async () => { const f = (rx: number) => fakeFs({ "/proc/net/dev": NETDEV(rx, rx) }); const p = computePoint(await readResources(f(1000), 0), await readResources(f(10), 1000)); expect(p.sample.netRxBps).toBeNull(); }); }); describe("采样器", () => { it("环形保留、按桶平均、快照符合契约", async () => { let now = Date.parse("2026-10-06T00:00:00Z"); let usage = 0; const read: ReadText = (path) => { if (path === "/sys/fs/cgroup/cpu.stat") return Promise.resolve(`usage_usec ${usage}\n`); if (path === "/sys/fs/cgroup/cpu.max") return Promise.resolve("100000 100000"); if (path === "/proc/meminfo") return Promise.resolve(MEMINFO); return Promise.resolve(null); }; const s = createResourceSampler({ read, autoStart: false, intervalMs: 5000, retentionMs: 60_000, now: () => now }); for (let i = 0; i < 30; i++) { // 每 5 秒用 2.5 秒 CPU(1 核)→ 50% usage += 2_500_000; now += 5000; await s.tick(); } const snap = SystemResources.parse(await s.snapshot(5)); // 保留 60 秒(含边界)→ 13 个点;第一次读数没有速率,不进趋势 expect(snap.samples).toHaveLength(13); expect(snap.samples.every((x) => x.cpuPercent === 50)).toBe(true); expect(snap.cpu).toMatchObject({ scope: "cgroup", percent: 50, cores: 1 }); expect(snap.intervalSec).toBe(5); s.stop(); // 长窗口:按桶取平均,点数不超过 360 const many = Array.from({ length: 4000 }, (_, i) => ({ at: new Date(Date.parse("2026-10-06T00:00:00Z") + i * 5000).toISOString(), cpuPercent: i % 2 ? 40 : 60, memoryUsedBytes: 100, memoryPercent: 10, netRxBps: null, netTxBps: null, diskReadBps: null, diskWriteBps: null, })); const b = bucketSamples(many, 5, 360); expect(b.bucketSec).toBe(60); expect(b.points.length).toBeLessThanOrEqual(360); expect(b.points[0]).toMatchObject({ cpuPercent: 50, memoryUsedBytes: 100, netRxBps: null }); }); it("刚启动没有趋势时,快照现采两次给出当前值", async () => { let usage = 0; const read: ReadText = (path) => { if (path === "/sys/fs/cgroup/cpu.stat") { usage += 100_000; return Promise.resolve(`usage_usec ${usage}\n`); } return Promise.resolve(null); }; const s = createResourceSampler({ read, autoStart: false }); const snap = await s.snapshot(60); expect(snap.cpu.scope).toBe("cgroup"); expect(snap.cpu.percent).not.toBeNull(); expect(snap.samples.length).toBeGreaterThanOrEqual(1); }); it("进程 CPU:首次为 null,之后为非负数", () => { const m = createProcessCpuMeter(); expect(m()).toBeNull(); let x = 0; for (let i = 0; i < 1e5; i++) x += Math.sqrt(i); expect(x).toBeGreaterThan(0); const v = m(); expect(v).not.toBeNull(); expect(v!).toBeGreaterThanOrEqual(0); }); });