1
0

fix: 强化 CPU/memory checker 错误处理、timeout 遵守和快照校验

- Memory checker: reader 与 ctx.signal race,abort 返回 memory/timeout,reject 保持 memory/snapshot
- CPU checker: 第二次快照异常返回 cpu/snapshot,计算前校验空数组/核心数不一致/非有限值/负 delta
- CPU 计算: 零 delta 安全处理,observation 不含 NaN/Infinity
- 文档: CPU 互补描述修正,Memory timeout 约束说明
- 测试: +18 覆盖 timeout、异常和边界输入
This commit is contained in:
2026-05-27 16:33:39 +08:00
parent 145bb8fd04
commit 3390eb5e8d
8 changed files with 394 additions and 24 deletions

View File

@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
import type { CpuCoreSnapshot } from "../../../../../src/server/checker/runner/cpu/types";
import { calculateCpuStats } from "../../../../../src/server/checker/runner/cpu/calculate";
import { calculateCpuStats, validateCpuSnapshots } from "../../../../../src/server/checker/runner/cpu/calculate";
function makeCore(user: number, nice: number, sys: number, idle: number, irq: number): CpuCoreSnapshot {
return { times: { idle, irq, nice, sys, user } };
@@ -110,3 +110,68 @@ describe("calculateCpuStats", () => {
expect(stats.usagePercent).toBe(60);
});
});
describe("validateCpuSnapshots", () => {
test("合法 snapshot 返回 null", () => {
const before = [makeCore(100, 0, 0, 900, 0)];
const after = [makeCore(200, 0, 0, 800, 0)];
expect(validateCpuSnapshots(before, after)).toBeNull();
});
test("空 before snapshot", () => {
const after = [makeCore(0, 0, 0, 0, 0)];
expect(validateCpuSnapshots([], after)).toBe("CPU 快照为空");
});
test("空 after snapshot", () => {
const before = [makeCore(0, 0, 0, 0, 0)];
expect(validateCpuSnapshots(before, [])).toBe("CPU 快照为空");
});
test("核心数不一致", () => {
const before = [makeCore(0, 0, 0, 0, 0)];
const after = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
expect(validateCpuSnapshots(before, after)).toBe("CPU 快照核心数不一致: before=1, after=2");
});
test("before 包含 NaN time 值", () => {
const before = [{ times: { idle: NaN, irq: 0, nice: 0, sys: 0, user: 0 } }];
const after = [makeCore(0, 0, 0, 0, 0)];
const error = validateCpuSnapshots(before, after);
expect(error).toContain("非有限值");
expect(error).toContain("before[0]");
});
test("after 包含 Infinity time 值", () => {
const before = [makeCore(0, 0, 0, 0, 0)];
const after = [{ times: { idle: Infinity, irq: 0, nice: 0, sys: 0, user: 0 } }];
const error = validateCpuSnapshots(before, after);
expect(error).toContain("非有限值");
expect(error).toContain("after[0]");
});
test("负数 total delta", () => {
const before = [makeCore(1000, 0, 0, 0, 0)];
const after = [makeCore(100, 0, 0, 0, 0)];
const error = validateCpuSnapshots(before, after);
expect(error).toContain("负数 delta");
});
test("零 delta 合法", () => {
const before = [makeCore(100, 0, 0, 100, 0)];
const after = [makeCore(100, 0, 0, 100, 0)];
expect(validateCpuSnapshots(before, after)).toBeNull();
});
test("零 delta 不产生除零错误", () => {
const before = [makeCore(100, 0, 0, 100, 0)];
const after = [makeCore(100, 0, 0, 100, 0)];
const stats = calculateCpuStats(before, after);
expect(Number.isFinite(stats.usagePercent)).toBe(true);
expect(Number.isFinite(stats.idlePercent)).toBe(true);
expect(Number.isFinite(stats.maxCoreUsagePercent)).toBe(true);
expect(Number.isFinite(stats.minCoreUsagePercent)).toBe(true);
expect(stats.usagePercent).toBe(0);
expect(stats.idlePercent).toBe(0);
});
});

View File

@@ -210,6 +210,130 @@ describe("CpuChecker execute", () => {
expect(result.failure?.path).toBe("timeout");
});
test("第二次 snapshot 抛错返回 cpu/snapshot failure", async () => {
const before = [makeCore(0, 0, 0, 10000, 0)];
let callCount = 0;
const reader: SnapshotReader = () => {
callCount++;
if (callCount === 1) return before;
throw new Error("second snapshot failed");
};
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("snapshot");
expect(result.observation).toBeNull();
});
test("空 snapshot pair 返回 cpu/snapshot failure", async () => {
const reader: SnapshotReader = () => [];
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("snapshot");
});
test("核心数不一致返回 cpu/snapshot failure", async () => {
let callCount = 0;
const snapshots = [[makeCore(0, 0, 0, 100, 0)], [makeCore(0, 0, 0, 100, 0), makeCore(0, 0, 0, 100, 0)]];
const reader: SnapshotReader = () => {
const result = snapshots[Math.min(callCount, snapshots.length - 1)]!;
callCount++;
return result;
};
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("snapshot");
expect(result.failure?.message).toContain("核心数不一致");
});
test("非有限 CPU time 值返回 cpu/snapshot failure", async () => {
let callCount = 0;
const snapshots: CpuCoreSnapshot[][] = [
[makeCore(0, 0, 0, 100, 0)],
[{ times: { idle: NaN, irq: 0, nice: 0, sys: 0, user: 100 } }],
];
const reader: SnapshotReader = () => {
const result = snapshots[Math.min(callCount, snapshots.length - 1)]!;
callCount++;
return result;
};
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("snapshot");
expect(result.failure?.message).toContain("非有限值");
});
test("负数 CPU time delta 返回 cpu/snapshot failure", async () => {
const before = [makeCore(1000, 0, 0, 0, 0)];
const after = [makeCore(100, 0, 0, 0, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("snapshot");
expect(result.failure?.message).toContain("负数 delta");
});
test("零 delta snapshot 返回稳定安全值", async () => {
const before = [makeCore(100, 0, 0, 100, 0)];
const after = [makeCore(100, 0, 0, 100, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(true);
expect(result.failure).toBeNull();
expect(result.observation).toMatchObject({
idlePercent: 0,
maxCoreUsagePercent: 0,
minCoreUsagePercent: 0,
usagePercent: 0,
});
});
test("includePerCore=true 时输出 perCoreUsagePercent", async () => {
const before = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
const after = [makeCore(8000, 0, 0, 2000, 0), makeCore(2000, 0, 0, 8000, 0)];

View File

@@ -153,6 +153,66 @@ describe("MemoryChecker execute", () => {
expect(result.observation).toBeNull();
});
test("signal 已 abort 时返回 timeout failure", async () => {
const reader = () => Promise.resolve(makeMemData());
const checker = new MemoryChecker(reader);
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
const resolved = checker.resolve(target, makeResolveContext());
const controller = new AbortController();
controller.abort();
const result = await checker.execute(resolved, { signal: controller.signal });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("memory");
expect(result.failure?.path).toBe("timeout");
expect(result.observation).toBeNull();
});
test("pending reader 被 signal abort 后返回 timeout failure", async () => {
const reader = () =>
new Promise<Systeminformation.MemData>(() => {
// 故意永不 resolve模拟悬挂的 reader
});
const checker = new MemoryChecker(reader);
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
const resolved = checker.resolve(target, makeResolveContext());
const controller = new AbortController();
const executePromise = checker.execute(resolved, { signal: controller.signal });
controller.abort();
const result = await executePromise;
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("memory");
expect(result.failure?.path).toBe("timeout");
expect(result.observation).toBeNull();
});
test("reader 在 abort 前 resolve 时返回正常结果", async () => {
const data = makeMemData({ active: 4294967296, total: 8589934592 });
const reader = () => Promise.resolve(data);
const checker = new MemoryChecker(reader);
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
const resolved = checker.resolve(target, makeResolveContext());
resolved.expect = { usagePercent: { lte: 85 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(true);
expect(result.failure).toBeNull();
expect(result.observation).toMatchObject({
totalBytes: 8589934592,
usagePercent: 50,
});
});
test("detail 格式", async () => {
const data = makeMemData({ active: 4294967296, total: 8589934592 });
const reader = () => Promise.resolve(data);