134 lines
7.0 KiB
JavaScript
134 lines
7.0 KiB
JavaScript
// Parser and effective-limit tests. Every input is fixture text shaped like
|
|
// the kernel's files; nothing reads /proc, /sys or a cgroup.
|
|
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import {
|
|
SupervisorError, parseLoadavg, parseMeminfo, parsePsi, parseCpuList, parseCpuMax, parseMemoryMax,
|
|
parseMemoryCurrent, parseMemoryEvents, parseVmstat, effectiveCpus, effectiveMemory,
|
|
} from "../src/index.mjs";
|
|
|
|
const GiB = 1024 ** 3;
|
|
const isParse = (re) => (e) => e instanceof SupervisorError && e.kind === "parse" && re.test(e.message);
|
|
|
|
const MEMINFO = [
|
|
"MemTotal: 32768000 kB",
|
|
"MemFree: 1024000 kB",
|
|
"MemAvailable: 8192000 kB",
|
|
"SwapTotal: 4096000 kB",
|
|
"SwapFree: 4000000 kB",
|
|
"HugePages_Total: 0",
|
|
"Hugepagesize: 2048 kB",
|
|
"",
|
|
].join("\n");
|
|
|
|
test("loadavg parses the three loads", () => {
|
|
assert.deepEqual(parseLoadavg("0.52 1.58 12.00 3/1234 56789\n"), { load1: 0.52, load5: 1.58, load15: 12 });
|
|
});
|
|
|
|
test("a malformed loadavg throws", () => {
|
|
for (const bad of ["", "0.52 0.58 0.59\n", "0.52 0.58 0.59 1/2\n", "x 0.58 0.59 1/2 3\n", "0.52 0.58 0.59 1/2 3 4\n", 42]) {
|
|
assert.throws(() => parseLoadavg(bad), isParse(/loadavg/), JSON.stringify(bad));
|
|
}
|
|
});
|
|
|
|
test("meminfo returns bytes and keeps unitless counts", () => {
|
|
assert.deepEqual(parseMeminfo(MEMINFO), {
|
|
memTotal: 32768000 * 1024, memAvailable: 8192000 * 1024, swapTotal: 4096000 * 1024, swapFree: 4000000 * 1024,
|
|
});
|
|
});
|
|
|
|
test("meminfo without MemAvailable, with a repeated key or a bad line throws", () => {
|
|
assert.throws(() => parseMeminfo("MemTotal: 1 kB\n"), isParse(/no MemAvailable/));
|
|
assert.throws(() => parseMeminfo(MEMINFO + "MemTotal: 1 kB\n"), isParse(/repeated key MemTotal/));
|
|
assert.throws(() => parseMeminfo(MEMINFO + "Broken line\n"), isParse(/bad line/));
|
|
assert.throws(() => parseMeminfo(MEMINFO.replace("8192000", "-1")), isParse(/bad line/));
|
|
});
|
|
|
|
test("PSI parses some and full, and cpu may lack full", () => {
|
|
const mem = "some avg10=1.50 avg60=0.20 avg300=0.00 total=123\nfull avg10=0.75 avg60=0.10 avg300=0.00 total=45\n";
|
|
assert.deepEqual(parsePsi(mem, "memory"), {
|
|
some: { avg10: 1.5, avg60: 0.2, avg300: 0, total: 123 },
|
|
full: { avg10: 0.75, avg60: 0.1, avg300: 0, total: 45 },
|
|
});
|
|
assert.equal(parsePsi("some avg10=20.00 avg60=0.00 avg300=0.00 total=9\n", "cpu").full, null);
|
|
});
|
|
|
|
test("PSI without a full line for memory or io, or with a bad line, throws", () => {
|
|
const someOnly = "some avg10=0.00 avg60=0.00 avg300=0.00 total=0\n";
|
|
assert.throws(() => parsePsi(someOnly, "memory"), isParse(/no full line/));
|
|
assert.throws(() => parsePsi(someOnly, "io"), isParse(/no full line/));
|
|
assert.throws(() => parsePsi("full avg10=0.00 avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/no some line/));
|
|
assert.throws(() => parsePsi(someOnly + someOnly, "cpu"), isParse(/repeated some/));
|
|
assert.throws(() => parsePsi("some avg10=x avg60=0.00 avg300=0.00 total=0\n", "cpu"), isParse(/not a non-negative number/));
|
|
assert.throws(() => parsePsi(someOnly, "gpu"), isParse(/unknown resource/));
|
|
});
|
|
|
|
test("CPU lists count distinct CPUs", () => {
|
|
assert.equal(parseCpuList("0-3,6,8-9\n"), 7);
|
|
assert.equal(parseCpuList("0\n"), 1);
|
|
assert.equal(parseCpuList("0-3,2-5"), 6);
|
|
for (const bad of ["", "\n", "3-1", "0,,1", "a-b", "0-"]) assert.throws(() => parseCpuList(bad), isParse(/cpu list/), bad);
|
|
});
|
|
|
|
test("cpu.max gives the quota in CPUs, or null for max", () => {
|
|
assert.equal(parseCpuMax("max 100000\n"), null);
|
|
assert.equal(parseCpuMax("50000 100000\n"), 0.5);
|
|
assert.equal(parseCpuMax("200000 100000\n"), 2);
|
|
for (const bad of ["max", "50000 0", "-1 100000", "max max"]) assert.throws(() => parseCpuMax(bad), isParse(/cpu.max/), bad);
|
|
});
|
|
|
|
test("memory.max, memory.current and memory.events", () => {
|
|
assert.equal(parseMemoryMax("max\n"), null);
|
|
assert.equal(parseMemoryMax("4294967296\n"), 4 * GiB);
|
|
assert.throws(() => parseMemoryMax("4G\n"), isParse(/memory.max/));
|
|
assert.equal(parseMemoryCurrent("123456\n"), 123456);
|
|
assert.throws(() => parseMemoryCurrent(""), isParse(/memory.current/));
|
|
assert.deepEqual(parseMemoryEvents("low 0\nhigh 4\nmax 2\noom 1\noom_kill 1\noom_group_kill 0\n"), { oom: 1, oomKill: 1 });
|
|
assert.throws(() => parseMemoryEvents("low 0\noom 1\n"), isParse(/no oom_kill/));
|
|
assert.throws(() => parseMemoryEvents("oom_kill 1\noom_kill 2\n"), isParse(/repeated key/));
|
|
});
|
|
|
|
test("vmstat gives swap counters and the host OOM counter", () => {
|
|
assert.deepEqual(parseVmstat("nr_free_pages 100\npswpin 7\npswpout 9\noom_kill 3\n"), { pswpin: 7, pswpout: 9, oomKill: 3 });
|
|
assert.deepEqual(parseVmstat("pswpin 0\npswpout 0\n"), { pswpin: 0, pswpout: 0, oomKill: null });
|
|
assert.throws(() => parseVmstat("pswpin 0\n"), isParse(/no pswpout/));
|
|
});
|
|
|
|
test("effective CPUs fall back to the host count with no cgroup input", () => {
|
|
assert.equal(effectiveCpus({ online: "0-15\n" }), 16);
|
|
});
|
|
|
|
test("effective CPUs take the smallest of host, cpuset and every quota level", () => {
|
|
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n" }), 4);
|
|
assert.equal(effectiveCpus({ online: "0-15\n", cpuset: "0-3\n", cpuMax: ["max 100000\n", "150000 100000\n"] }), 1.5);
|
|
// The outer level is the tighter one.
|
|
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["800000 100000\n", "200000 100000\n"] }), 2);
|
|
// A quota above the host count doesn't raise it.
|
|
assert.equal(effectiveCpus({ online: "0-1\n", cpuMax: ["800000 100000\n"] }), 2);
|
|
assert.equal(effectiveCpus({ online: "0-15\n", cpuMax: ["max 100000\n"] }), 16);
|
|
});
|
|
|
|
test("effective memory falls back to the host with no cgroup levels, or only max", () => {
|
|
const host = { limit: 32768000 * 1024, available: 8192000 * 1024 };
|
|
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO }), host);
|
|
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "max\n" }] }), host);
|
|
});
|
|
|
|
test("effective memory uses the tightest limited level", () => {
|
|
// 4 GiB limit with 3 GiB used: 1 GiB available, below the host's MemAvailable.
|
|
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n`, current: `${3 * GiB}\n` }] }),
|
|
{ limit: 4 * GiB, available: 1 * GiB });
|
|
// Nested: the inner level is loose, the outer one is nearly full.
|
|
assert.deepEqual(effectiveMemory({ meminfo: MEMINFO, levels: [
|
|
{ max: "max\n" }, { max: `${16 * GiB}\n`, current: `${2 * GiB}\n` }, { max: `${6 * GiB}\n`, current: `${5.5 * GiB}\n` },
|
|
] }), { limit: 6 * GiB, available: 0.5 * GiB });
|
|
// Usage above the limit floors at 0.
|
|
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: "100\n", current: "200\n" }] }).available, 0);
|
|
// A limit above MemTotal doesn't raise the limit.
|
|
assert.equal(effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${64 * GiB}\n`, current: "0\n" }] }).limit, 32768000 * 1024);
|
|
});
|
|
|
|
test("a limited level without memory.current throws", () => {
|
|
assert.throws(() => effectiveMemory({ meminfo: MEMINFO, levels: [{ max: `${4 * GiB}\n` }] }), isParse(/missing for a limited level/));
|
|
});
|