Compare commits
2 Commits
145bb8fd04
...
2f8fd8bd9c
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f8fd8bd9c | |||
| 3390eb5e8d |
@@ -13,13 +13,13 @@
|
||||
|
||||
## expect 校验项
|
||||
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| --------------------- | ----------------------------------------------------------------------------- | ---- | ------ |
|
||||
| `usagePercent` | 总体 CPU 使用率,范围 `0-100`,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `idlePercent` | 总体 CPU 空闲率,与 `usagePercent` 互补(`idlePercent = 100 - usagePercent`) | 否 | 无 |
|
||||
| `maxCoreUsagePercent` | 单核心最高使用率,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `minCoreUsagePercent` | 单核心最低使用率,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
|
||||
| 字段 | 说明 | 必填 | 默认值 |
|
||||
| --------------------- | ----------------------------------------------------------------------------------------------- | ---- | ------ |
|
||||
| `usagePercent` | 总体 CPU 使用率,范围 `0-100`,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `idlePercent` | 总体 CPU 空闲率,与 `usagePercent` 互补,两者之和恒为 100(`idlePercent + usagePercent = 100`) | 否 | 无 |
|
||||
| `maxCoreUsagePercent` | 单核心最高使用率,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `minCoreUsagePercent` | 单核心最低使用率,使用 `ValueMatcher` | 否 | 无 |
|
||||
| `durationMs` | 完整执行耗时校验,使用 `ValueMatcher` | 否 | 无 |
|
||||
|
||||
所有百分比字段范围为 `0-100`,表示所有可见逻辑 CPU 的总体比例,不是"核心数 × 100"。
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Memory Checker
|
||||
# Mem Checker
|
||||
|
||||
`type: memory` 用于检查本机系统级内存使用状况,包括物理内存和交换空间的使用率及字节数。
|
||||
`type: mem` 用于检查本机系统级内存使用状况,包括物理内存和交换空间的使用率及字节数。
|
||||
|
||||
## 配置项
|
||||
|
||||
Memory checker 配置为空对象,无需额外参数:
|
||||
Mem checker 配置为空对象,无需额外参数:
|
||||
|
||||
```yaml
|
||||
memory: {}
|
||||
mem: {}
|
||||
```
|
||||
|
||||
## expect 校验项
|
||||
@@ -54,10 +54,10 @@ memory: {}
|
||||
```yaml
|
||||
- id: "local-memory"
|
||||
name: "本机内存"
|
||||
type: memory
|
||||
type: mem
|
||||
interval: "30s"
|
||||
timeout: "5s"
|
||||
memory: {}
|
||||
mem: {}
|
||||
expect:
|
||||
usagePercent:
|
||||
lte: 85
|
||||
@@ -68,8 +68,8 @@ memory: {}
|
||||
```yaml
|
||||
- id: "local-memory-available"
|
||||
name: "可用内存检查"
|
||||
type: memory
|
||||
memory: {}
|
||||
type: mem
|
||||
mem: {}
|
||||
expect:
|
||||
availableBytes:
|
||||
gte: "4GB"
|
||||
@@ -80,8 +80,8 @@ memory: {}
|
||||
```yaml
|
||||
- id: "local-memory-swap"
|
||||
name: "内存和交换空间"
|
||||
type: memory
|
||||
memory: {}
|
||||
type: mem
|
||||
mem: {}
|
||||
expect:
|
||||
usagePercent:
|
||||
lte: 80
|
||||
@@ -91,14 +91,14 @@ memory: {}
|
||||
|
||||
## 语义说明
|
||||
|
||||
Memory checker 通过 `systeminformation` 库读取系统内存数据,在 Linux、macOS 和 Windows 上均可运行。
|
||||
Mem checker 通过 `systeminformation` 库读取系统内存数据,在 Linux、macOS 和 Windows 上均可运行。
|
||||
|
||||
- **`usagePercent`** 使用 `activeBytes / totalBytes` 计算,反映真实的内存压力,不受 Linux buffers/cache 缓存影响。推荐使用此字段进行内存健康检查。
|
||||
- **`usedPercent`** 使用 `usedBytes / totalBytes` 计算,包含 buffers/cache。在 Linux 上此值通常高于 `usagePercent`。
|
||||
- **Swap 字段**:当系统未配置交换分区时,`swapTotalBytes` 为 `0`,`swapUsagePercent` 为 `null`(非 `0`)。
|
||||
- **`buffcacheBytes`**:反映 Linux 的 buffers + cache 用量,在其他平台上可能为 `null`。
|
||||
|
||||
Memory checker 是即时读取(非采样),无需 `sampleDuration`,执行速度远快于 CPU checker。
|
||||
Mem checker 是即时读取(非采样),无需 `sampleDuration`,执行速度远快于 CPU checker。虽然读取本身很快,但仍受 target `timeout` 约束——若底层系统调用悬挂或阻塞超过 `timeout`,checker 会返回 `mem/timeout` failure。
|
||||
|
||||
## 跨平台注意事项
|
||||
|
||||
@@ -116,4 +116,4 @@ Memory checker 是即时读取(非采样),无需 `sampleDuration`,执行
|
||||
|
||||
## 更新触发条件
|
||||
|
||||
修改 Memory checker 配置、expect 字段、行为或语义时,必须更新本文档。
|
||||
修改 Mem checker 配置、expect 字段、行为或语义时,必须更新本文档。
|
||||
@@ -6857,7 +6857,7 @@
|
||||
"required": [
|
||||
"id",
|
||||
"type",
|
||||
"memory"
|
||||
"mem"
|
||||
],
|
||||
"properties": {
|
||||
"description": {
|
||||
@@ -7616,10 +7616,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"const": "memory",
|
||||
"const": "mem",
|
||||
"type": "string"
|
||||
},
|
||||
"memory": {
|
||||
"mem": {
|
||||
"additionalProperties": false,
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
|
||||
@@ -370,11 +370,11 @@ targets:
|
||||
|
||||
- id: "local-memory"
|
||||
name: "本机内存"
|
||||
type: memory
|
||||
type: mem
|
||||
group: "基础设施"
|
||||
interval: "30s"
|
||||
timeout: "5s"
|
||||
memory: {}
|
||||
mem: {}
|
||||
expect:
|
||||
usagePercent:
|
||||
lte: 85
|
||||
|
||||
@@ -67,6 +67,45 @@ export function readCpuSnapshot(): CpuCoreSnapshot[] {
|
||||
}));
|
||||
}
|
||||
|
||||
export function validateCpuSnapshots(before: CpuCoreSnapshot[], after: CpuCoreSnapshot[]): null | string {
|
||||
if (before.length === 0 || after.length === 0) {
|
||||
return "CPU 快照为空";
|
||||
}
|
||||
|
||||
if (before.length !== after.length) {
|
||||
return `CPU 快照核心数不一致: before=${before.length}, after=${after.length}`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < before.length; i++) {
|
||||
const bTimes = before[i]!.times;
|
||||
const aTimes = after[i]!.times;
|
||||
|
||||
for (const [name, value] of Object.entries(bTimes)) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return `CPU 快照包含非有限值: before[${i}].times.${name}=${value}`;
|
||||
}
|
||||
}
|
||||
for (const [name, value] of Object.entries(aTimes)) {
|
||||
if (!Number.isFinite(value)) {
|
||||
return `CPU 快照包含非有限值: after[${i}].times.${name}=${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
const idleDelta = aTimes.idle - bTimes.idle;
|
||||
const userDelta = aTimes.user - bTimes.user;
|
||||
const niceDelta = aTimes.nice - bTimes.nice;
|
||||
const sysDelta = aTimes.sys - bTimes.sys;
|
||||
const irqDelta = aTimes.irq - bTimes.irq;
|
||||
const coreTotalDelta = userDelta + niceDelta + sysDelta + idleDelta + irqDelta;
|
||||
|
||||
if (coreTotalDelta < 0) {
|
||||
return `CPU 快照包含负数 delta: core[${i}] totalDelta=${coreTotalDelta}`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function round1(value: number): number {
|
||||
return Math.round(value * 10) / 10;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CpuCoreSnapshot, CpuStats, CpuTargetConfig, ResolvedCpuExpectConfi
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { parseDuration } from "../../utils";
|
||||
import { calculateCpuStats, readCpuSnapshot } from "./calculate";
|
||||
import { calculateCpuStats, readCpuSnapshot, validateCpuSnapshots } from "./calculate";
|
||||
import { checkIdlePercent, checkMaxCoreUsage, checkMinCoreUsage, checkUsagePercent } from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { cpuCheckerSchemas } from "./schema";
|
||||
@@ -65,10 +65,9 @@ export class CpuChecker implements CheckerDefinition<ResolvedCpuTarget> {
|
||||
// 采样等待,支持 AbortSignal 取消
|
||||
const aborted = await waitForDuration(t.cpu.sampleDurationMs, ctx.signal);
|
||||
|
||||
const after = aborted ? null : this.readSnapshot();
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
|
||||
if (aborted || after === null) {
|
||||
let after: CpuCoreSnapshot[];
|
||||
if (aborted) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
@@ -80,7 +79,41 @@ export class CpuChecker implements CheckerDefinition<ResolvedCpuTarget> {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
after = this.readSnapshot();
|
||||
} catch (error) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure(
|
||||
"cpu",
|
||||
"snapshot",
|
||||
`CPU 快照读取失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const validationError = validateCpuSnapshots(before, after);
|
||||
if (validationError !== null) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure("cpu", "snapshot", validationError),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
const stats = calculateCpuStats(before, after);
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
const result = checkStats(stats, t.expect, durationMs);
|
||||
|
||||
const observation: Record<string, unknown> = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DnsChecker } from "./dns";
|
||||
import { HttpChecker } from "./http";
|
||||
import { IcmpChecker } from "./icmp";
|
||||
import { LlmChecker } from "./llm";
|
||||
import { MemoryChecker } from "./memory";
|
||||
import { MemChecker } from "./mem";
|
||||
import { CheckerRegistry } from "./registry";
|
||||
import { TcpChecker } from "./tcp";
|
||||
import { UdpChecker } from "./udp";
|
||||
@@ -22,7 +22,7 @@ const checkers = [
|
||||
new DnsChecker(),
|
||||
new WsChecker(),
|
||||
new CpuChecker(),
|
||||
new MemoryChecker(),
|
||||
new MemChecker(),
|
||||
];
|
||||
|
||||
export function createDefaultCheckerRegistry(): CheckerRegistry {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Systeminformation } from "systeminformation";
|
||||
|
||||
import type { MemoryStats } from "./types";
|
||||
import type { MemStats } from "./types";
|
||||
|
||||
export function calculateMemoryStats(data: Systeminformation.MemData): MemoryStats {
|
||||
export function calculateMemStats(data: Systeminformation.MemData): MemStats {
|
||||
const totalBytes = data.total;
|
||||
const usedBytes = data.used;
|
||||
const activeBytes = data.active;
|
||||
@@ -41,7 +41,7 @@ export function calculateMemoryStats(data: Systeminformation.MemData): MemorySta
|
||||
};
|
||||
}
|
||||
|
||||
export async function readMemoryData(): Promise<Systeminformation.MemData> {
|
||||
export async function readMemData(): Promise<Systeminformation.MemData> {
|
||||
const si = await import("systeminformation");
|
||||
return si.mem();
|
||||
}
|
||||
@@ -2,11 +2,11 @@ import type { Systeminformation } from "systeminformation";
|
||||
|
||||
import type { CheckResult, RawTargetConfig } from "../../types";
|
||||
import type { CheckerContext, CheckerDefinition, CheckerValidationInput, ResolveContext } from "../types";
|
||||
import type { MemoryStats, ResolvedMemoryExpectConfig, ResolvedMemoryTarget } from "./types";
|
||||
import type { MemStats, ResolvedMemExpectConfig, ResolvedMemTarget } from "./types";
|
||||
|
||||
import { errorFailure } from "../../expect/failure";
|
||||
import { checkValueExpectation } from "../../expect/value";
|
||||
import { calculateMemoryStats, readMemoryData } from "./calculate";
|
||||
import { calculateMemStats, readMemData } from "./calculate";
|
||||
import {
|
||||
checkActiveBytes,
|
||||
checkActivePercent,
|
||||
@@ -25,17 +25,17 @@ import {
|
||||
checkUsedPercent,
|
||||
} from "./expect";
|
||||
import { normalizeTargetExpect } from "./normalize";
|
||||
import { memoryCheckerSchemas } from "./schema";
|
||||
import { validateMemoryConfig } from "./validate";
|
||||
import { memCheckerSchemas } from "./schema";
|
||||
import { validateMemConfig } from "./validate";
|
||||
|
||||
export class MemoryChecker implements CheckerDefinition<ResolvedMemoryTarget> {
|
||||
readonly configKey = "memory";
|
||||
export class MemChecker implements CheckerDefinition<ResolvedMemTarget> {
|
||||
readonly configKey = "mem";
|
||||
|
||||
readonly schemas = memoryCheckerSchemas;
|
||||
readonly schemas = memCheckerSchemas;
|
||||
|
||||
readonly type = "memory";
|
||||
readonly type = "mem";
|
||||
|
||||
constructor(private readonly reader: () => Promise<Systeminformation.MemData> = readMemoryData) {}
|
||||
constructor(private readonly reader: () => Promise<Systeminformation.MemData> = readMemData) {}
|
||||
|
||||
buildDetail(observation: Record<string, unknown>): null | string {
|
||||
const usage = observation["usagePercent"];
|
||||
@@ -45,23 +45,40 @@ export class MemoryChecker implements CheckerDefinition<ResolvedMemoryTarget> {
|
||||
return `usage ${usageStr}%, total ${totalStr}`;
|
||||
}
|
||||
|
||||
async execute(t: ResolvedMemoryTarget, _ctx: CheckerContext): Promise<CheckResult> {
|
||||
async execute(t: ResolvedMemTarget, ctx: CheckerContext): Promise<CheckResult> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const start = performance.now();
|
||||
|
||||
let data: Systeminformation.MemData;
|
||||
try {
|
||||
data = await this.reader();
|
||||
} catch (error) {
|
||||
if (ctx.signal.aborted) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: errorFailure(
|
||||
"memory",
|
||||
"snapshot",
|
||||
`内存数据读取失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
failure: errorFailure("mem", "timeout", "内存读取超时:signal 已取消"),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
let data: Systeminformation.MemData;
|
||||
try {
|
||||
data = await raceWithSignal(this.reader(), ctx.signal);
|
||||
} catch (error) {
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
const isTimeout =
|
||||
error instanceof AbortError || (error instanceof Error && error.message === MEM_TIMEOUT_MESSAGE);
|
||||
return {
|
||||
detail: null,
|
||||
durationMs,
|
||||
failure: isTimeout
|
||||
? errorFailure("mem", "timeout", "内存读取超时")
|
||||
: errorFailure(
|
||||
"mem",
|
||||
"snapshot",
|
||||
`内存数据读取失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
),
|
||||
matched: false,
|
||||
observation: null,
|
||||
targetId: t.id,
|
||||
@@ -70,7 +87,7 @@ export class MemoryChecker implements CheckerDefinition<ResolvedMemoryTarget> {
|
||||
}
|
||||
|
||||
const durationMs = Math.round(performance.now() - start);
|
||||
const stats = calculateMemoryStats(data);
|
||||
const stats = calculateMemStats(data);
|
||||
const result = checkStats(stats, t.expect, durationMs);
|
||||
|
||||
const observation: Record<string, unknown> = {
|
||||
@@ -107,33 +124,33 @@ export class MemoryChecker implements CheckerDefinition<ResolvedMemoryTarget> {
|
||||
return normalizeTargetExpect(target);
|
||||
}
|
||||
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedMemoryTarget {
|
||||
resolve(target: RawTargetConfig, context: ResolveContext): ResolvedMemTarget {
|
||||
return {
|
||||
description: null,
|
||||
expect: target.expect as ResolvedMemoryExpectConfig | undefined,
|
||||
expect: target.expect as ResolvedMemExpectConfig | undefined,
|
||||
group: target.group ?? "default",
|
||||
id: target.id,
|
||||
intervalMs: context.defaultIntervalMs,
|
||||
memory: {},
|
||||
mem: {},
|
||||
name: target.name ?? null,
|
||||
timeoutMs: context.defaultTimeoutMs,
|
||||
type: "memory",
|
||||
} satisfies ResolvedMemoryTarget;
|
||||
type: "mem",
|
||||
} satisfies ResolvedMemTarget;
|
||||
}
|
||||
|
||||
serialize(t: ResolvedMemoryTarget): { config: string; target: string } {
|
||||
serialize(t: ResolvedMemTarget): { config: string; target: string } {
|
||||
return {
|
||||
config: JSON.stringify(t.memory),
|
||||
target: `memory`,
|
||||
config: JSON.stringify(t.mem),
|
||||
target: `mem`,
|
||||
};
|
||||
}
|
||||
|
||||
validate(input: CheckerValidationInput) {
|
||||
return validateMemoryConfig(input);
|
||||
return validateMemConfig(input);
|
||||
}
|
||||
}
|
||||
|
||||
function checkStats(stats: MemoryStats, expect: ResolvedMemoryExpectConfig | undefined, durationMs: number) {
|
||||
function checkStats(stats: MemStats, expect: ResolvedMemExpectConfig | undefined, durationMs: number) {
|
||||
let result = checkUsagePercent(stats.usagePercent, expect?.usagePercent);
|
||||
if (!result.matched) return result;
|
||||
result = checkUsedPercent(stats.usedPercent, expect?.usedPercent);
|
||||
@@ -181,3 +198,35 @@ function formatBytes(bytes: number): string {
|
||||
function formatNumber(value: number): string {
|
||||
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(1)));
|
||||
}
|
||||
|
||||
const MEM_TIMEOUT_MESSAGE = "Memory read aborted by signal";
|
||||
|
||||
class AbortError extends Error {
|
||||
constructor() {
|
||||
super(MEM_TIMEOUT_MESSAGE);
|
||||
this.name = "AbortError";
|
||||
}
|
||||
}
|
||||
|
||||
function raceWithSignal<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
|
||||
if (signal.aborted) return Promise.reject(new AbortError());
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
function onAbort() {
|
||||
reject(new AbortError());
|
||||
}
|
||||
|
||||
signal.addEventListener("abort", onAbort, { once: true });
|
||||
|
||||
promise.then(
|
||||
(value) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
resolve(value);
|
||||
},
|
||||
(error: unknown) => {
|
||||
signal.removeEventListener("abort", onAbort);
|
||||
reject(error instanceof Error ? error : new Error(String(error)));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
1
src/server/checker/runner/mem/index.ts
Normal file
1
src/server/checker/runner/mem/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { MemChecker } from "./execute";
|
||||
@@ -9,22 +9,22 @@ import {
|
||||
sizeSchema,
|
||||
} from "../../schema/fragments";
|
||||
|
||||
export const memoryCheckerSchemas: CheckerSchemas = {
|
||||
export const memCheckerSchemas: CheckerSchemas = {
|
||||
authoring: {
|
||||
config: createMemoryConfigSchema("authoring"),
|
||||
expect: createMemoryExpectSchema("authoring"),
|
||||
config: createMemConfigSchema("authoring"),
|
||||
expect: createMemExpectSchema("authoring"),
|
||||
},
|
||||
normalized: {
|
||||
config: createMemoryConfigSchema("normalized"),
|
||||
expect: createMemoryExpectSchema("normalized"),
|
||||
config: createMemConfigSchema("normalized"),
|
||||
expect: createMemExpectSchema("normalized"),
|
||||
},
|
||||
};
|
||||
|
||||
function createMemoryConfigSchema(_kind: "authoring" | "normalized") {
|
||||
function createMemConfigSchema(_kind: "authoring" | "normalized") {
|
||||
return Type.Object({}, { additionalProperties: false });
|
||||
}
|
||||
|
||||
function createMemoryExpectSchema(kind: "authoring" | "normalized") {
|
||||
function createMemExpectSchema(kind: "authoring" | "normalized") {
|
||||
const valueSchema =
|
||||
kind === "authoring" ? createAuthoringValueExpectationSchema() : createNormalizedValueExpectationSchema();
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { Systeminformation } from "systeminformation";
|
||||
import type { RawValueExpectation, ValueExpectation } from "../../expect/types";
|
||||
import type { ResolvedTargetBase } from "../../types";
|
||||
|
||||
export type MemoryDataReader = () => Promise<Systeminformation.MemData>;
|
||||
export type MemDataReader = () => Promise<Systeminformation.MemData>;
|
||||
|
||||
export interface MemoryStats {
|
||||
export interface MemStats {
|
||||
activeBytes: number;
|
||||
activePercent: number;
|
||||
availableBytes: number;
|
||||
@@ -23,7 +23,7 @@ export interface MemoryStats {
|
||||
usedPercent: number;
|
||||
}
|
||||
|
||||
export interface RawMemoryExpectConfig {
|
||||
export interface RawMemExpectConfig {
|
||||
activeBytes?: RawValueExpectation;
|
||||
activePercent?: RawValueExpectation;
|
||||
availableBytes?: RawValueExpectation;
|
||||
@@ -43,9 +43,9 @@ export interface RawMemoryExpectConfig {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface ResolvedMemoryConfig {}
|
||||
export interface ResolvedMemConfig {}
|
||||
|
||||
export interface ResolvedMemoryExpectConfig {
|
||||
export interface ResolvedMemExpectConfig {
|
||||
activeBytes?: ValueExpectation;
|
||||
activePercent?: ValueExpectation;
|
||||
availableBytes?: ValueExpectation;
|
||||
@@ -64,12 +64,12 @@ export interface ResolvedMemoryExpectConfig {
|
||||
usedPercent?: ValueExpectation;
|
||||
}
|
||||
|
||||
export interface ResolvedMemoryTarget extends ResolvedTargetBase {
|
||||
expect?: ResolvedMemoryExpectConfig;
|
||||
export interface ResolvedMemTarget extends ResolvedTargetBase {
|
||||
expect?: ResolvedMemExpectConfig;
|
||||
group: string;
|
||||
intervalMs: number;
|
||||
memory: ResolvedMemoryConfig;
|
||||
mem: ResolvedMemConfig;
|
||||
name: null | string;
|
||||
timeoutMs: number;
|
||||
type: "memory";
|
||||
type: "mem";
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { isPlainRecord, validateRawValueExpectation } from "../../expect/validat
|
||||
import { issue, joinPath } from "../../schema/issues";
|
||||
import { parseSize } from "../../utils";
|
||||
|
||||
const MEMORY_CONFIG_KEYS = new Set<string>([]);
|
||||
const MEM_CONFIG_KEYS = new Set<string>([]);
|
||||
|
||||
const MEMORY_EXPECT_FIELDS = [
|
||||
const MEM_EXPECT_FIELDS = [
|
||||
"activeBytes",
|
||||
"activePercent",
|
||||
"availableBytes",
|
||||
@@ -40,16 +40,16 @@ const BYTE_EXPECT_FIELDS = new Set([
|
||||
"usedBytes",
|
||||
]);
|
||||
|
||||
const MEMORY_EXPECT_KEYS = new Set<string>(MEMORY_EXPECT_FIELDS);
|
||||
const MEM_EXPECT_KEYS = new Set<string>(MEM_EXPECT_FIELDS);
|
||||
|
||||
export function validateMemoryConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
export function validateMemConfig(input: CheckerValidationInput): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
|
||||
for (let i = 0; i < input.targets.length; i++) {
|
||||
const target = input.targets[i] as unknown;
|
||||
if (!isPlainRecord(target)) continue;
|
||||
if (target["type"] !== "memory") continue;
|
||||
issues.push(...validateMemoryTarget(target, `targets[${i}]`));
|
||||
if (target["type"] !== "mem") continue;
|
||||
issues.push(...validateMemTarget(target, `targets[${i}]`));
|
||||
}
|
||||
|
||||
return issues;
|
||||
@@ -60,7 +60,7 @@ function getTargetName(target: Record<string, unknown>): string | undefined {
|
||||
return isString(target["id"]) ? target["id"] : undefined;
|
||||
}
|
||||
|
||||
function validateMemoryExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
function validateMemExpect(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const rawExpect = target["expect"];
|
||||
if (rawExpect === undefined || rawExpect === null || !isPlainRecord(rawExpect)) return [];
|
||||
const expect = rawExpect;
|
||||
@@ -68,7 +68,7 @@ function validateMemoryExpect(target: Record<string, unknown>, path: string): Co
|
||||
const targetName = getTargetName(target);
|
||||
const expectPath = joinPath(path, "expect");
|
||||
|
||||
for (const key of MEMORY_EXPECT_FIELDS) {
|
||||
for (const key of MEM_EXPECT_FIELDS) {
|
||||
if (expect[key] !== undefined) {
|
||||
issues.push(...validateRawValueExpectation(expect[key], joinPath(expectPath, key), targetName));
|
||||
if (BYTE_EXPECT_FIELDS.has(key) && isString(expect[key])) {
|
||||
@@ -82,7 +82,7 @@ function validateMemoryExpect(target: Record<string, unknown>, path: string): Co
|
||||
}
|
||||
|
||||
for (const key of Object.keys(expect)) {
|
||||
if (!MEMORY_EXPECT_KEYS.has(key)) {
|
||||
if (!MEM_EXPECT_KEYS.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(expectPath, key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
@@ -90,22 +90,22 @@ function validateMemoryExpect(target: Record<string, unknown>, path: string): Co
|
||||
return issues;
|
||||
}
|
||||
|
||||
function validateMemoryTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
function validateMemTarget(target: Record<string, unknown>, path: string): ConfigValidationIssue[] {
|
||||
const issues: ConfigValidationIssue[] = [];
|
||||
const targetName = getTargetName(target);
|
||||
|
||||
const rawMemory = target["memory"];
|
||||
if (!isPlainRecord(rawMemory)) {
|
||||
issues.push(issue("required", joinPath(path, "memory"), "缺少 memory 配置分组", targetName));
|
||||
const rawMem = target["mem"];
|
||||
if (!isPlainRecord(rawMem)) {
|
||||
issues.push(issue("required", joinPath(path, "mem"), "缺少 mem 配置分组", targetName));
|
||||
} else {
|
||||
for (const key of Object.keys(rawMemory)) {
|
||||
if (!MEMORY_CONFIG_KEYS.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(joinPath(path, "memory"), key), "是未知字段", targetName));
|
||||
for (const key of Object.keys(rawMem)) {
|
||||
if (!MEM_CONFIG_KEYS.has(key)) {
|
||||
issues.push(issue("unknown-field", joinPath(joinPath(path, "mem"), key), "是未知字段", targetName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
issues.push(...validateMemoryExpect(target, path));
|
||||
issues.push(...validateMemExpect(target, path));
|
||||
|
||||
return issues;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export { MemoryChecker } from "./execute";
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)];
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Systeminformation } from "systeminformation";
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { calculateMemoryStats } from "../../../../../src/server/checker/runner/memory/calculate";
|
||||
import { calculateMemStats } from "../../../../../src/server/checker/runner/mem/calculate";
|
||||
|
||||
function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): Systeminformation.MemData {
|
||||
return {
|
||||
@@ -25,56 +25,56 @@ function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): System
|
||||
};
|
||||
}
|
||||
|
||||
describe("calculateMemoryStats", () => {
|
||||
describe("calculateMemStats", () => {
|
||||
test("usagePercent = activeBytes / totalBytes * 100", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ active: 4294967296, total: 8589934592 }));
|
||||
const stats = calculateMemStats(makeMemData({ active: 4294967296, total: 8589934592 }));
|
||||
expect(stats.usagePercent).toBe(50);
|
||||
});
|
||||
|
||||
test("usedPercent = usedBytes / totalBytes * 100", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ total: 8589934592, used: 6442450944 }));
|
||||
const stats = calculateMemStats(makeMemData({ total: 8589934592, used: 6442450944 }));
|
||||
expect(stats.usedPercent).toBe(75);
|
||||
});
|
||||
|
||||
test("freePercent = freeBytes / totalBytes * 100", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ free: 2147483648, total: 8589934592 }));
|
||||
const stats = calculateMemStats(makeMemData({ free: 2147483648, total: 8589934592 }));
|
||||
expect(stats.freePercent).toBe(25);
|
||||
});
|
||||
|
||||
test("activePercent = activeBytes / totalBytes * 100", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ active: 3221225472, total: 8589934592 }));
|
||||
const stats = calculateMemStats(makeMemData({ active: 3221225472, total: 8589934592 }));
|
||||
expect(stats.activePercent).toBe(37.5);
|
||||
});
|
||||
|
||||
test("availablePercent = availableBytes / totalBytes * 100", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ available: 6442450944, total: 8589934592 }));
|
||||
const stats = calculateMemStats(makeMemData({ available: 6442450944, total: 8589934592 }));
|
||||
expect(stats.availablePercent).toBe(75);
|
||||
});
|
||||
|
||||
test("保留 1 位小数", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ active: 3000000000, total: 8000000000 }));
|
||||
const stats = calculateMemStats(makeMemData({ active: 3000000000, total: 8000000000 }));
|
||||
expect(stats.usagePercent).toBe(37.5);
|
||||
});
|
||||
|
||||
test("round1 处理需要四舍五入的情况", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ active: 3333333333, total: 10000000000 }));
|
||||
const stats = calculateMemStats(makeMemData({ active: 3333333333, total: 10000000000 }));
|
||||
expect(stats.usagePercent).toBe(33.3);
|
||||
});
|
||||
|
||||
test("total 为 0 时百分比为 0", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ active: 0, available: 0, free: 0, total: 0, used: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ active: 0, available: 0, free: 0, total: 0, used: 0 }));
|
||||
expect(stats.usagePercent).toBe(0);
|
||||
expect(stats.usedPercent).toBe(0);
|
||||
expect(stats.freePercent).toBe(0);
|
||||
});
|
||||
|
||||
test("buffcacheBytes 为 null 映射", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ buffcache: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ buffcache: 0 }));
|
||||
expect(stats.buffcacheBytes).toBe(0);
|
||||
});
|
||||
|
||||
test("buffcacheBytes 为正数时保留", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ buffcache: 1073741824 }));
|
||||
const stats = calculateMemStats(makeMemData({ buffcache: 1073741824 }));
|
||||
expect(stats.buffcacheBytes).toBe(1073741824);
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("calculateMemoryStats", () => {
|
||||
total: 4000,
|
||||
used: 3500,
|
||||
});
|
||||
const stats = calculateMemoryStats(data);
|
||||
const stats = calculateMemStats(data);
|
||||
expect(stats.activeBytes).toBe(1000);
|
||||
expect(stats.availableBytes).toBe(2000);
|
||||
expect(stats.freeBytes).toBe(3000);
|
||||
@@ -95,9 +95,9 @@ describe("calculateMemoryStats", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("calculateMemoryStats swap", () => {
|
||||
describe("calculateMemStats swap", () => {
|
||||
test("swap 不可用:swaptotal=0 时 swapUsagePercent=null", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ swapfree: 0, swaptotal: 0, swapused: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ swapfree: 0, swaptotal: 0, swapused: 0 }));
|
||||
expect(stats.swapUsagePercent).toBe(null);
|
||||
expect(stats.swapTotalBytes).toBe(0);
|
||||
expect(stats.swapUsedBytes).toBe(0);
|
||||
@@ -105,14 +105,12 @@ describe("calculateMemoryStats swap", () => {
|
||||
});
|
||||
|
||||
test("swap 总量为 0,swapUsagePercent 为 null(不是 0)", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ swaptotal: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ swaptotal: 0 }));
|
||||
expect(stats.swapUsagePercent).toBe(null);
|
||||
});
|
||||
|
||||
test("swap 已使用", () => {
|
||||
const stats = calculateMemoryStats(
|
||||
makeMemData({ swapfree: 1073741824, swaptotal: 4294967296, swapused: 3221225472 }),
|
||||
);
|
||||
const stats = calculateMemStats(makeMemData({ swapfree: 1073741824, swaptotal: 4294967296, swapused: 3221225472 }));
|
||||
expect(stats.swapUsagePercent).toBe(75);
|
||||
expect(stats.swapTotalBytes).toBe(4294967296);
|
||||
expect(stats.swapUsedBytes).toBe(3221225472);
|
||||
@@ -120,21 +118,21 @@ describe("calculateMemoryStats swap", () => {
|
||||
});
|
||||
|
||||
test("swap 未使用:swapUsagePercent=0(不是 null)", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||
expect(stats.swapUsagePercent).toBe(0);
|
||||
expect(stats.swapUsedBytes).toBe(0);
|
||||
expect(stats.swapFreeBytes).toBe(4294967296);
|
||||
});
|
||||
|
||||
test("swap 部分使用保留 1 位小数", () => {
|
||||
const stats = calculateMemoryStats(
|
||||
const stats = calculateMemStats(
|
||||
makeMemData({ swapfree: 3000000000, swaptotal: 10000000000, swapused: 7000000000 }),
|
||||
);
|
||||
expect(stats.swapUsagePercent).toBe(70);
|
||||
});
|
||||
|
||||
test("swap 合法 0 不被转换为 null", () => {
|
||||
const stats = calculateMemoryStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||
const stats = calculateMemStats(makeMemData({ swapfree: 4294967296, swaptotal: 4294967296, swapused: 0 }));
|
||||
expect(stats.swapUsedBytes).toBe(0);
|
||||
expect(stats.swapUsagePercent).toBe(0);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||||
|
||||
import { MemoryChecker } from "../../../../../src/server/checker/runner/memory/execute";
|
||||
import { MemChecker } from "../../../../../src/server/checker/runner/mem/execute";
|
||||
|
||||
function makeMemData(overrides: Partial<Systeminformation.MemData> = {}): Systeminformation.MemData {
|
||||
return {
|
||||
@@ -38,17 +38,17 @@ function makeResolveContext(
|
||||
};
|
||||
}
|
||||
|
||||
describe("MemoryChecker resolve", () => {
|
||||
const checker = new MemoryChecker();
|
||||
describe("MemChecker resolve", () => {
|
||||
const checker = new MemChecker();
|
||||
|
||||
test("默认值:memory 为空对象", () => {
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
test("默认值:mem 为空对象", () => {
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
expect(resolved.memory).toEqual({});
|
||||
expect(resolved.mem).toEqual({});
|
||||
});
|
||||
|
||||
test("无 expect 时 expect 为 undefined", () => {
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
expect(resolved.expect).toBeUndefined();
|
||||
});
|
||||
@@ -57,27 +57,27 @@ describe("MemoryChecker resolve", () => {
|
||||
const target: RawTargetConfig = {
|
||||
expect: { usagePercent: { lte: 85 } },
|
||||
id: "mem-test",
|
||||
memory: {},
|
||||
type: "memory",
|
||||
mem: {},
|
||||
type: "mem",
|
||||
};
|
||||
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" };
|
||||
test("type 为 mem", () => {
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
expect(resolved.type).toBe("memory");
|
||||
expect(resolved.type).toBe("mem");
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryChecker execute", () => {
|
||||
describe("MemChecker execute", () => {
|
||||
test("成功匹配", async () => {
|
||||
const data = makeMemData({ active: 4294967296, total: 8589934592 });
|
||||
const reader = () => Promise.resolve(data);
|
||||
const checker = new MemoryChecker(reader);
|
||||
const checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
resolved.expect = { usagePercent: { lte: 85 } };
|
||||
|
||||
@@ -95,9 +95,9 @@ describe("MemoryChecker execute", () => {
|
||||
test("usagePercent mismatch", async () => {
|
||||
const data = makeMemData({ active: 7730941132, total: 8589934592 });
|
||||
const reader = () => Promise.resolve(data);
|
||||
const checker = new MemoryChecker(reader);
|
||||
const checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
resolved.expect = { usagePercent: { lte: 50 } };
|
||||
|
||||
@@ -111,9 +111,9 @@ describe("MemoryChecker execute", () => {
|
||||
test("observation 包含所有字段", async () => {
|
||||
const data = makeMemData();
|
||||
const reader = () => Promise.resolve(data);
|
||||
const checker = new MemoryChecker(reader);
|
||||
const checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
|
||||
const ctx = { signal: new AbortController().signal };
|
||||
@@ -139,26 +139,86 @@ describe("MemoryChecker execute", () => {
|
||||
|
||||
test("reader reject 返回失败结果", async () => {
|
||||
const reader = () => Promise.reject(new Error("read error"));
|
||||
const checker = new MemoryChecker(reader);
|
||||
const checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
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?.phase).toBe("mem");
|
||||
expect(result.failure?.path).toBe("snapshot");
|
||||
expect(result.observation).toBeNull();
|
||||
});
|
||||
|
||||
test("signal 已 abort 时返回 timeout failure", async () => {
|
||||
const reader = () => Promise.resolve(makeMemData());
|
||||
const checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
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("mem");
|
||||
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 MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
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("mem");
|
||||
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 MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
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 checker = new MemChecker(reader);
|
||||
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
|
||||
const ctx = { signal: new AbortController().signal };
|
||||
@@ -171,13 +231,13 @@ describe("MemoryChecker execute", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("MemoryChecker serialize", () => {
|
||||
describe("MemChecker serialize", () => {
|
||||
test("序列化输出", () => {
|
||||
const checker = new MemoryChecker();
|
||||
const target: RawTargetConfig = { id: "mem-test", memory: {}, type: "memory" };
|
||||
const checker = new MemChecker();
|
||||
const target: RawTargetConfig = { id: "mem-test", mem: {}, type: "mem" };
|
||||
const resolved = checker.resolve(target, makeResolveContext());
|
||||
const result = checker.serialize(resolved);
|
||||
expect(result.target).toBe("memory");
|
||||
expect(result.target).toBe("mem");
|
||||
const config = JSON.parse(result.config) as Record<string, unknown>;
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
checkUsagePercent,
|
||||
checkUsedBytes,
|
||||
checkUsedPercent,
|
||||
} from "../../../../../src/server/checker/runner/memory/expect";
|
||||
} from "../../../../../src/server/checker/runner/mem/expect";
|
||||
|
||||
describe("Memory expect checks - 百分比字段", () => {
|
||||
describe("Mem expect checks - 百分比字段", () => {
|
||||
test("checkUsagePercent 匹配", () => {
|
||||
expect(checkUsagePercent(50, { lte: 85 }).matched).toBe(true);
|
||||
});
|
||||
@@ -63,7 +63,7 @@ describe("Memory expect checks - 百分比字段", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory expect checks - 字节字段", () => {
|
||||
describe("Mem expect checks - 字节字段", () => {
|
||||
test("checkActiveBytes 匹配", () => {
|
||||
expect(checkActiveBytes(4294967296, { lte: 8589934592 }).matched).toBe(true);
|
||||
});
|
||||
@@ -87,7 +87,7 @@ describe("Memory expect checks - 字节字段", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory expect checks - swap 字段", () => {
|
||||
describe("Mem expect checks - swap 字段", () => {
|
||||
test("checkSwapUsagePercent null 通过 gte 检查 (Number(null)=0)", () => {
|
||||
expect(checkSwapUsagePercent(null, { gte: 0 }).matched).toBe(true);
|
||||
});
|
||||
@@ -132,7 +132,7 @@ describe("Memory expect checks - swap 字段", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory expect checks - buffcacheBytes", () => {
|
||||
describe("Mem expect checks - buffcacheBytes", () => {
|
||||
test("checkBuffcacheBytes 有值时匹配", () => {
|
||||
expect(checkBuffcacheBytes(1073741824, { lte: 2147483648 }).matched).toBe(true);
|
||||
});
|
||||
@@ -1,50 +1,50 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { normalizeTargetExpect } from "../../../../../src/server/checker/runner/memory/normalize";
|
||||
import { normalizeTargetExpect } from "../../../../../src/server/checker/runner/mem/normalize";
|
||||
|
||||
describe("normalizeTargetExpect (memory)", () => {
|
||||
describe("normalizeTargetExpect (mem)", () => {
|
||||
test("无 expect 直接返回", () => {
|
||||
const target = { id: "test", memory: {}, type: "memory" };
|
||||
const target = { id: "test", mem: {}, type: "mem" };
|
||||
expect(normalizeTargetExpect(target)).toEqual(target);
|
||||
});
|
||||
|
||||
test("expect 为非对象直接返回", () => {
|
||||
const target = { expect: "not-an-object", id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: "not-an-object", id: "test", mem: {}, type: "mem" };
|
||||
expect(normalizeTargetExpect(target)).toEqual(target);
|
||||
});
|
||||
|
||||
test("字节大小字符串 512MB 转换为数字", () => {
|
||||
const target = { expect: { usedBytes: "512MB" }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usedBytes: "512MB" }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ equals: 536870912 });
|
||||
});
|
||||
|
||||
test("字节大小字符串 1GB 转换为数字", () => {
|
||||
const target = { expect: { totalBytes: "1GB" }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { totalBytes: "1GB" }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["totalBytes"]).toEqual({ equals: 1073741824 });
|
||||
});
|
||||
|
||||
test("数字字节 matcher 保持不变", () => {
|
||||
const target = { expect: { usedBytes: 1073741824 }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usedBytes: 1073741824 }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ equals: 1073741824 });
|
||||
});
|
||||
|
||||
test("百分比 matcher 正常展开", () => {
|
||||
const target = { expect: { usagePercent: 85 }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usagePercent: 85 }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ equals: 85 });
|
||||
});
|
||||
|
||||
test("matcher 对象保持不变", () => {
|
||||
const target = { expect: { usagePercent: { lte: 85 } }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usagePercent: { lte: 85 } }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ lte: 85 });
|
||||
});
|
||||
|
||||
test("字节 matcher 对象内字符串转换", () => {
|
||||
const target = { expect: { usedBytes: { gte: "512MB" } }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usedBytes: { gte: "512MB" } }, id: "test", mem: {}, type: "mem" };
|
||||
const result = normalizeTargetExpect(target);
|
||||
expect((result.expect as Record<string, unknown>)["usedBytes"]).toEqual({ gte: 536870912 });
|
||||
});
|
||||
@@ -53,8 +53,8 @@ describe("normalizeTargetExpect (memory)", () => {
|
||||
const target = {
|
||||
expect: { freePercent: 25, totalBytes: "16GB", usagePercent: { lte: 85 } },
|
||||
id: "test",
|
||||
memory: {},
|
||||
type: "memory",
|
||||
mem: {},
|
||||
type: "mem",
|
||||
};
|
||||
const result = normalizeTargetExpect(target);
|
||||
const expectObj = result.expect as Record<string, unknown>;
|
||||
@@ -64,9 +64,9 @@ describe("normalizeTargetExpect (memory)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeTargetExpect (memory) 错误", () => {
|
||||
describe("normalizeTargetExpect (mem) 错误", () => {
|
||||
test("非法大小字符串抛出", () => {
|
||||
const target = { expect: { usedBytes: "abc" }, id: "test", memory: {}, type: "memory" };
|
||||
const target = { expect: { usedBytes: "abc" }, id: "test", mem: {}, type: "mem" };
|
||||
expect(() => normalizeTargetExpect(target)).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,60 +1,60 @@
|
||||
import Ajv from "ajv";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { memoryCheckerSchemas } from "../../../../../src/server/checker/runner/memory/schema";
|
||||
import { memCheckerSchemas } from "../../../../../src/server/checker/runner/mem/schema";
|
||||
|
||||
const ajv = new Ajv({ strict: false });
|
||||
|
||||
describe("Memory checker schema", () => {
|
||||
describe("Mem checker schema", () => {
|
||||
test("authoring config 空配置通过", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.config);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.config);
|
||||
expect(validate({})).toBe(true);
|
||||
});
|
||||
|
||||
test("normalized config 空配置通过", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.config);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.config);
|
||||
expect(validate({})).toBe(true);
|
||||
});
|
||||
|
||||
test("config 拒绝额外字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.config);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.config);
|
||||
expect(validate({ extraField: true })).toBe(false);
|
||||
});
|
||||
|
||||
test("authoring expect 允许百分比 ValueMatcher 简写", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.expect);
|
||||
expect(validate({ usagePercent: 85 })).toBe(true);
|
||||
expect(validate({ usagePercent: { lte: 85 } })).toBe(true);
|
||||
});
|
||||
|
||||
test("authoring expect 允许字节字段字符串", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.expect);
|
||||
expect(validate({ usedBytes: "512MB" })).toBe(true);
|
||||
expect(validate({ totalBytes: "1GB" })).toBe(true);
|
||||
});
|
||||
|
||||
test("authoring expect 允许字节字段数字", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.expect);
|
||||
expect(validate({ usedBytes: 536870912 })).toBe(true);
|
||||
});
|
||||
|
||||
test("normalized expect 允许 matcher 对象", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(validate({ freePercent: { gte: 15 }, usagePercent: { lte: 85 } })).toBe(true);
|
||||
});
|
||||
|
||||
test("expect 拒绝未知字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.authoring.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.authoring.expect);
|
||||
expect(validate({ unknownField: 1 })).toBe(false);
|
||||
});
|
||||
|
||||
test("expect 空对象通过", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(validate({})).toBe(true);
|
||||
});
|
||||
|
||||
test("expect 允许所有合法百分比字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(
|
||||
validate({
|
||||
activePercent: { lte: 80 },
|
||||
@@ -68,7 +68,7 @@ describe("Memory checker schema", () => {
|
||||
});
|
||||
|
||||
test("expect 允许所有合法字节字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(
|
||||
validate({
|
||||
activeBytes: { lte: 8589934592 },
|
||||
@@ -84,12 +84,12 @@ describe("Memory checker schema", () => {
|
||||
});
|
||||
|
||||
test("expect 允许 durationMs 字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(validate({ durationMs: { lte: 5000 } })).toBe(true);
|
||||
});
|
||||
|
||||
test("expect 允许 buffcacheBytes 字段", () => {
|
||||
const validate = ajv.compile(memoryCheckerSchemas.normalized.expect);
|
||||
const validate = ajv.compile(memCheckerSchemas.normalized.expect);
|
||||
expect(validate({ buffcacheBytes: { lte: 2147483648 } })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -2,29 +2,29 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
|
||||
|
||||
import { validateMemoryConfig } from "../../../../../src/server/checker/runner/memory/validate";
|
||||
import { validateMemConfig } from "../../../../../src/server/checker/runner/mem/validate";
|
||||
|
||||
function validate(target: RawTargetConfig) {
|
||||
return validateMemoryConfig({ targets: [target] });
|
||||
return validateMemConfig({ targets: [target] });
|
||||
}
|
||||
|
||||
describe("validateMemoryConfig", () => {
|
||||
describe("validateMemConfig", () => {
|
||||
test("有效配置无错误", () => {
|
||||
expect(validate({ id: "mem-test", memory: {}, type: "memory" })).toEqual([]);
|
||||
expect(validate({ id: "mem-test", mem: {}, type: "mem" })).toEqual([]);
|
||||
});
|
||||
|
||||
test("缺少 memory 配置分组", () => {
|
||||
const issues = validate({ id: "mem-test", type: "memory" });
|
||||
expect(issues.some((i) => i.path.endsWith("memory") && i.code === "required")).toBe(true);
|
||||
test("缺少 mem 配置分组", () => {
|
||||
const issues = validate({ id: "mem-test", type: "mem" });
|
||||
expect(issues.some((i) => i.path.endsWith("mem") && i.code === "required")).toBe(true);
|
||||
});
|
||||
|
||||
test("memory 未知字段报错", () => {
|
||||
const issues = validate({ id: "mem-test", memory: { extra: true }, type: "memory" });
|
||||
test("mem 未知字段报错", () => {
|
||||
const issues = validate({ id: "mem-test", mem: { extra: true }, type: "mem" });
|
||||
expect(issues.some((i) => i.path.endsWith("extra") && i.code === "unknown-field")).toBe(true);
|
||||
});
|
||||
|
||||
test("expect 未知字段报错", () => {
|
||||
const issues = validate({ expect: { logicalCoreCount: { gte: 4 } }, id: "mem-test", memory: {}, type: "memory" });
|
||||
const issues = validate({ expect: { logicalCoreCount: { gte: 4 } }, id: "mem-test", mem: {}, type: "mem" });
|
||||
expect(issues.some((i) => i.path.endsWith("logicalCoreCount") && i.code === "unknown-field")).toBe(true);
|
||||
});
|
||||
|
||||
@@ -32,24 +32,24 @@ describe("validateMemoryConfig", () => {
|
||||
const issues = validate({
|
||||
expect: { usagePercent: { lte: 85 }, usedBytes: { lte: 8589934592 } },
|
||||
id: "mem-test",
|
||||
memory: {},
|
||||
type: "memory",
|
||||
mem: {},
|
||||
type: "mem",
|
||||
});
|
||||
expect(issues.filter((i) => i.path.includes("expect"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("expect 非法 ValueMatcher 报错", () => {
|
||||
const issues = validate({ expect: { usagePercent: [1, 2] }, id: "mem-test", memory: {}, type: "memory" });
|
||||
const issues = validate({ expect: { usagePercent: [1, 2] }, id: "mem-test", mem: {}, type: "mem" });
|
||||
expect(issues.some((i) => i.path.includes("usagePercent"))).toBe(true);
|
||||
});
|
||||
|
||||
test("expect 合法字节大小字符串通过", () => {
|
||||
const issues = validate({ expect: { usedBytes: "512MB" }, id: "mem-test", memory: {}, type: "memory" });
|
||||
const issues = validate({ expect: { usedBytes: "512MB" }, id: "mem-test", mem: {}, type: "mem" });
|
||||
expect(issues.filter((i) => i.path.includes("usedBytes"))).toEqual([]);
|
||||
});
|
||||
|
||||
test("expect 非法字节大小字符串报错", () => {
|
||||
const issues = validate({ expect: { usedBytes: "abc" }, id: "mem-test", memory: {}, type: "memory" });
|
||||
const issues = validate({ expect: { usedBytes: "abc" }, id: "mem-test", mem: {}, type: "mem" });
|
||||
expect(issues.some((i) => i.path.includes("usedBytes") && i.message.includes("字节大小"))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -74,13 +74,13 @@ describe("validateMemoryConfig", () => {
|
||||
usedPercent: { lte: 90 },
|
||||
},
|
||||
id: "mem-test",
|
||||
memory: {},
|
||||
type: "memory",
|
||||
mem: {},
|
||||
type: "mem",
|
||||
});
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
|
||||
test("非 memory type 的 target 不校验", () => {
|
||||
test("非 mem type 的 target 不校验", () => {
|
||||
const issues = validate({ id: "other-test", type: "http" });
|
||||
expect(issues).toEqual([]);
|
||||
});
|
||||
@@ -84,7 +84,7 @@ describe("CheckerRegistry", () => {
|
||||
"dns",
|
||||
"ws",
|
||||
"cpu",
|
||||
"memory",
|
||||
"mem",
|
||||
"custom",
|
||||
]);
|
||||
expect(second.supportedTypes).toEqual([
|
||||
@@ -98,7 +98,7 @@ describe("CheckerRegistry", () => {
|
||||
"dns",
|
||||
"ws",
|
||||
"cpu",
|
||||
"memory",
|
||||
"mem",
|
||||
]);
|
||||
expect(
|
||||
first.definitions.every((checker) => checker.schemas.authoring.config && checker.schemas.normalized.expect),
|
||||
|
||||
Reference in New Issue
Block a user