- 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、异常和边界输入
380 lines
14 KiB
TypeScript
380 lines
14 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import type { SnapshotReader } from "../../../../../src/server/checker/runner/cpu/execute";
|
||
import type { CpuCoreSnapshot } from "../../../../../src/server/checker/runner/cpu/types";
|
||
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||
|
||
import { CpuChecker } from "../../../../../src/server/checker/runner/cpu/execute";
|
||
|
||
function makeCore(user: number, nice: number, sys: number, idle: number, irq: number): CpuCoreSnapshot {
|
||
return { times: { idle, irq, nice, sys, user } };
|
||
}
|
||
|
||
function makeResolveContext(
|
||
overrides: Partial<{ configDir: string; defaultIntervalMs: number; defaultTimeoutMs: number }> = {},
|
||
) {
|
||
return {
|
||
configDir: "/test",
|
||
defaultIntervalMs: 30000,
|
||
defaultTimeoutMs: 10000,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe("CpuChecker resolve", () => {
|
||
const checker = new CpuChecker();
|
||
|
||
test("默认值:sampleDurationMs=1000, includePerCore=false", () => {
|
||
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.cpu.sampleDurationMs).toBe(1000);
|
||
expect(resolved.cpu.includePerCore).toBe(false);
|
||
});
|
||
|
||
test("显式配置覆盖默认值", () => {
|
||
const target: RawTargetConfig = {
|
||
cpu: { includePerCore: true, sampleDuration: "2s" },
|
||
id: "cpu-test",
|
||
type: "cpu",
|
||
};
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.cpu.sampleDurationMs).toBe(2000);
|
||
expect(resolved.cpu.includePerCore).toBe(true);
|
||
});
|
||
|
||
test("无 expect 时 expect 为 undefined", () => {
|
||
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.expect).toBeUndefined();
|
||
});
|
||
|
||
test("保留 expect 字段", () => {
|
||
const target: RawTargetConfig = {
|
||
cpu: {},
|
||
expect: { usagePercent: { lte: 85 } },
|
||
id: "cpu-test",
|
||
type: "cpu",
|
||
};
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.expect).toEqual({ usagePercent: { lte: 85 } });
|
||
});
|
||
|
||
test("type 为 cpu", () => {
|
||
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.type).toBe("cpu");
|
||
});
|
||
});
|
||
|
||
describe("CpuChecker execute", () => {
|
||
function makeSnapshotReader(_first: CpuCoreSnapshot[], _second: CpuCoreSnapshot[]): SnapshotReader {
|
||
let callCount = 0;
|
||
const snapshots = [_first, _second];
|
||
return () => {
|
||
const result = snapshots[Math.min(callCount, snapshots.length - 1)]!;
|
||
callCount++;
|
||
return result;
|
||
};
|
||
}
|
||
|
||
test("成功匹配", async () => {
|
||
// 50% usage, 50% idle
|
||
const before = [makeCore(1000, 0, 0, 9000, 0)];
|
||
const after = [makeCore(1500, 0, 0, 9500, 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 }));
|
||
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({
|
||
idlePercent: 50,
|
||
logicalCoreCount: 1,
|
||
usagePercent: 50,
|
||
});
|
||
// 默认不包含 perCoreUsagePercent
|
||
expect(result.observation!["perCoreUsagePercent"]).toBeUndefined();
|
||
});
|
||
|
||
test("usagePercent mismatch", async () => {
|
||
// 90% usage: before idle=0, after idle=1000, total=10000
|
||
const before = [makeCore(0, 0, 0, 0, 0)];
|
||
const after = [makeCore(9000, 0, 0, 1000, 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 }));
|
||
resolved.expect = { usagePercent: { lte: 50 } };
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("usage");
|
||
});
|
||
|
||
test("idlePercent mismatch", async () => {
|
||
// idle = 10%: before idle=0, after idle=1000, total=10000
|
||
const before = [makeCore(0, 0, 0, 0, 0)];
|
||
const after = [makeCore(9000, 0, 0, 1000, 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 }));
|
||
resolved.expect = { idlePercent: { gte: 80 } };
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("idle");
|
||
});
|
||
|
||
test("maxCoreUsagePercent mismatch", async () => {
|
||
// core 0: 95% usage, core 1: 10% usage
|
||
const bf = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
|
||
const af = [makeCore(9500, 0, 0, 500, 0), makeCore(1000, 0, 0, 9000, 0)];
|
||
const reader = makeSnapshotReader(bf, af);
|
||
const checker = new CpuChecker(reader);
|
||
|
||
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
|
||
resolved.expect = { maxCoreUsagePercent: { lte: 80 } };
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("maxCoreUsage");
|
||
});
|
||
|
||
test("minCoreUsagePercent mismatch", async () => {
|
||
// core 0: 95% usage, core 1: 10% usage
|
||
const bf = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
|
||
const af = [makeCore(9500, 0, 0, 500, 0), makeCore(1000, 0, 0, 9000, 0)];
|
||
const reader = makeSnapshotReader(bf, af);
|
||
const checker = new CpuChecker(reader);
|
||
|
||
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
|
||
resolved.expect = { minCoreUsagePercent: { gte: 50 } };
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("minCoreUsage");
|
||
});
|
||
|
||
test("durationMs mismatch", async () => {
|
||
const before = [makeCore(0, 0, 0, 10000, 0)];
|
||
const after = [makeCore(1000, 0, 0, 9000, 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 }));
|
||
resolved.expect = { durationMs: { lte: 0 } };
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("duration");
|
||
});
|
||
|
||
test("超时取消", async () => {
|
||
const before = [makeCore(0, 0, 0, 10000, 0)];
|
||
const after = [makeCore(1000, 0, 0, 9000, 0)];
|
||
const reader = makeSnapshotReader(before, after);
|
||
const checker = new CpuChecker(reader);
|
||
|
||
const target: RawTargetConfig = { cpu: { sampleDuration: "10s" }, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 100 }));
|
||
|
||
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("cpu");
|
||
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)];
|
||
const reader = makeSnapshotReader(before, after);
|
||
const checker = new CpuChecker(reader);
|
||
|
||
const target: RawTargetConfig = { cpu: { includePerCore: true }, 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.observation).toMatchObject({
|
||
perCoreUsagePercent: [80, 20],
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("CpuChecker buildDetail", () => {
|
||
test("正常输出格式", () => {
|
||
const checker = new CpuChecker();
|
||
const detail = checker.buildDetail({
|
||
idlePercent: 40,
|
||
logicalCoreCount: 8,
|
||
maxCoreUsagePercent: 91.5,
|
||
minCoreUsagePercent: 8.2,
|
||
usagePercent: 60,
|
||
});
|
||
expect(detail).toBe("usage 60%, max core 91.5%, 8 cores");
|
||
});
|
||
});
|
||
|
||
describe("CpuChecker serialize", () => {
|
||
test("序列化输出", () => {
|
||
const checker = new CpuChecker();
|
||
const target: RawTargetConfig = { cpu: { sampleDuration: "1s" }, id: "cpu-test", type: "cpu" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
const result = checker.serialize(resolved);
|
||
expect(result.target).toBe("cpu sample 1000ms");
|
||
const config = JSON.parse(result.config) as { sampleDurationMs: number };
|
||
expect(config.sampleDurationMs).toBe(1000);
|
||
});
|
||
});
|