- 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、异常和边界输入
245 lines
8.6 KiB
TypeScript
245 lines
8.6 KiB
TypeScript
import type { Systeminformation } from "systeminformation";
|
||
|
||
import { describe, expect, test } from "bun:test";
|
||
|
||
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||
|
||
import { MemoryChecker } from "../../../../../src/server/checker/runner/memory/execute";
|
||
|
||
function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): Systeminformation.MemData {
|
||
return {
|
||
active: 4294967296,
|
||
available: 8589934592,
|
||
buffcache: 1073741824,
|
||
buffers: 536870912,
|
||
cached: 536870912,
|
||
dirty: null,
|
||
free: 4294967296,
|
||
reclaimable: 0,
|
||
slab: 0,
|
||
swapfree: 0,
|
||
swaptotal: 0,
|
||
swapused: 0,
|
||
total: 17179869184,
|
||
used: 8589934592,
|
||
writeback: null,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function makeResolveContext(
|
||
overrides: Partial<{ configDir: string; defaultIntervalMs: number; defaultTimeoutMs: number }> = {},
|
||
) {
|
||
return {
|
||
configDir: "/test",
|
||
defaultIntervalMs: 30000,
|
||
defaultTimeoutMs: 10000,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
describe("MemoryChecker resolve", () => {
|
||
const checker = new MemoryChecker();
|
||
|
||
test("默认值:memory 为空对象", () => {
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.memory).toEqual({});
|
||
});
|
||
|
||
test("无 expect 时 expect 为 undefined", () => {
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.expect).toBeUndefined();
|
||
});
|
||
|
||
test("保留 expect 字段", () => {
|
||
const target: RawTargetConfig = {
|
||
expect: { usagePercent: { lte: 85 } },
|
||
id: "mem-test",
|
||
memory: {},
|
||
type: "memory",
|
||
};
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.expect).toEqual({ usagePercent: { lte: 85 } });
|
||
});
|
||
|
||
test("type 为 memory", () => {
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
expect(resolved.type).toBe("memory");
|
||
});
|
||
});
|
||
|
||
describe("MemoryChecker execute", () => {
|
||
test("成功匹配", 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("usagePercent mismatch", async () => {
|
||
const data = makeMemData({ active: 7730941132, 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: 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("observation 包含所有字段", async () => {
|
||
const data = makeMemData();
|
||
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());
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
const obs = result.observation!;
|
||
expect(obs).toHaveProperty("activeBytes");
|
||
expect(obs).toHaveProperty("activePercent");
|
||
expect(obs).toHaveProperty("availableBytes");
|
||
expect(obs).toHaveProperty("availablePercent");
|
||
expect(obs).toHaveProperty("buffcacheBytes");
|
||
expect(obs).toHaveProperty("freeBytes");
|
||
expect(obs).toHaveProperty("freePercent");
|
||
expect(obs).toHaveProperty("swapFreeBytes");
|
||
expect(obs).toHaveProperty("swapTotalBytes");
|
||
expect(obs).toHaveProperty("swapUsagePercent");
|
||
expect(obs).toHaveProperty("swapUsedBytes");
|
||
expect(obs).toHaveProperty("totalBytes");
|
||
expect(obs).toHaveProperty("usagePercent");
|
||
expect(obs).toHaveProperty("usedBytes");
|
||
expect(obs).toHaveProperty("usedPercent");
|
||
});
|
||
|
||
test("reader reject 返回失败结果", async () => {
|
||
const reader = () => Promise.reject(new Error("read error"));
|
||
const checker = new MemoryChecker(reader);
|
||
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
expect(result.matched).toBe(false);
|
||
expect(result.failure?.phase).toBe("memory");
|
||
expect(result.failure?.path).toBe("snapshot");
|
||
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);
|
||
const checker = new MemoryChecker(reader);
|
||
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
|
||
const ctx = { signal: new AbortController().signal };
|
||
const result = await checker.execute(resolved, ctx);
|
||
|
||
const detail = checker.buildDetail(result.observation!);
|
||
expect(detail).toContain("usage");
|
||
expect(detail).toContain("%");
|
||
expect(detail).toContain("total");
|
||
});
|
||
});
|
||
|
||
describe("MemoryChecker serialize", () => {
|
||
test("序列化输出", () => {
|
||
const checker = new MemoryChecker();
|
||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||
const resolved = checker.resolve(target, makeResolveContext());
|
||
const result = checker.serialize(resolved);
|
||
expect(result.target).toBe("memory");
|
||
const config = JSON.parse(result.config) as Record<string, unknown>;
|
||
expect(config).toEqual({});
|
||
});
|
||
});
|