1
0
Files
DiAL/tests/server/checker/runner/cpu/execute.test.ts
lanyuanxiaoyao c2dcfab80c feat: 新增本机 CPU checker
- 新增 type: cpu checker,基于 os.cpus() 两次快照计算 CPU 使用率
- 配置项:sampleDuration(默认 1s)、includePerCore(默认 false)
- expect 字段:usagePercent、idlePercent、maxCoreUsagePercent、minCoreUsagePercent、durationMs
- idlePercent 与 usagePercent 互补恒等于 100,百分比范围 0-100
- logicalCoreCount 仅输出到 observation,不作为 expect 字段
- 不暴露 userPercent / systemPercent
- 语义校验禁止 sampleDuration >= timeout
- 支持 AbortSignal 超时取消
- 完整测试覆盖:schema、validate、normalize、resolve、calculate、execute、expect、config-loader
- 新增用户文档 docs/user/checkers/cpu.md
- 更新 checker 索引、配置类型列表、示例配置和 schema
2026-05-26 22:34:57 +08:00

256 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("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);
});
});