142 lines
5.2 KiB
TypeScript
142 lines
5.2 KiB
TypeScript
import type { ContainerStats } from "dockerode";
|
|
import { describe, expect, test, vi } from "vitest";
|
|
|
|
import { createDb } from "@homarr/db/test";
|
|
|
|
import { calculateCpuUsage, calculateMemoryUsage } from "../docker";
|
|
|
|
vi.mock("@homarr/db", async (importActual) => {
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
const actual = await importActual<typeof import("@homarr/db")>();
|
|
return {
|
|
...actual,
|
|
db: createDb(),
|
|
};
|
|
});
|
|
|
|
// Helper to create a partial stats object, cast to ContainerStats.
|
|
// This mirrors what Podman and other Docker-compatible runtimes may return
|
|
// at runtime, even though @types/dockerode declares all fields as required.
|
|
const createStats = (overrides: Record<string, unknown> = {}) => overrides as unknown as ContainerStats;
|
|
|
|
describe("calculateCpuUsage", () => {
|
|
test("should return 0 when cpu_stats is undefined (Podman)", () => {
|
|
const stats = createStats({ cpu_stats: undefined });
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when online_cpus is 0", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 0, cpu_usage: { total_usage: 1000 }, system_cpu_usage: 5000 },
|
|
});
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when online_cpus is undefined", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { cpu_usage: { total_usage: 1000 }, system_cpu_usage: 5000 },
|
|
});
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when cpu_usage is undefined", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, system_cpu_usage: 5000 },
|
|
});
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when total_usage is 0", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 0 }, system_cpu_usage: 5000 },
|
|
});
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when system_cpu_usage is 0", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1000 }, system_cpu_usage: 0 },
|
|
});
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should calculate correct CPU percentage for valid stats", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 2000 }, system_cpu_usage: 10000 },
|
|
});
|
|
// (2000 / 10000) * 4 * 100 = 80
|
|
expect(calculateCpuUsage(stats)).toBe(80);
|
|
});
|
|
|
|
test("should handle fractional CPU usage", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 2, cpu_usage: { total_usage: 500 }, system_cpu_usage: 100000 },
|
|
});
|
|
// (500 / 100000) * 2 * 100 = 1
|
|
expect(calculateCpuUsage(stats)).toBe(1);
|
|
});
|
|
|
|
test("should use the delta against precpu_stats for the current usage", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_002_000 }, system_cpu_usage: 20_000 },
|
|
precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 },
|
|
});
|
|
// Despite a large lifetime total_usage, only the recent delta counts:
|
|
// (2000 / 10000) * 4 * 100 = 80
|
|
expect(calculateCpuUsage(stats)).toBe(80);
|
|
});
|
|
|
|
test("should report 0 for a container that was busy over its lifetime but is idle now", () => {
|
|
const stats = createStats({
|
|
cpu_stats: { online_cpus: 4, cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 20_000 },
|
|
precpu_stats: { cpu_usage: { total_usage: 1_000_000 }, system_cpu_usage: 10_000 },
|
|
});
|
|
// No CPU delta since the last reading -> current usage is 0, even though the
|
|
// lifetime average would be high. This is the regression the fix addresses.
|
|
expect(calculateCpuUsage(stats)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("calculateMemoryUsage", () => {
|
|
test("should return 0 when memory_stats is undefined (Podman)", () => {
|
|
const stats = createStats({ memory_stats: undefined });
|
|
expect(calculateMemoryUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when memory_stats.usage is 0", () => {
|
|
const stats = createStats({ memory_stats: { usage: 0 } });
|
|
expect(calculateMemoryUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should return 0 when memory_stats.usage is undefined", () => {
|
|
const stats = createStats({ memory_stats: {} });
|
|
expect(calculateMemoryUsage(stats)).toBe(0);
|
|
});
|
|
|
|
test("should subtract cache from usage", () => {
|
|
const stats = createStats({
|
|
memory_stats: { usage: 1000, stats: { cache: 200 } },
|
|
});
|
|
expect(calculateMemoryUsage(stats)).toBe(800);
|
|
});
|
|
|
|
test("should use total_inactive_file when cache is absent", () => {
|
|
const stats = createStats({
|
|
memory_stats: { usage: 1000, stats: { total_inactive_file: 300 } },
|
|
});
|
|
expect(calculateMemoryUsage(stats)).toBe(700);
|
|
});
|
|
|
|
test("should use inactive_file as last fallback", () => {
|
|
const stats = createStats({
|
|
memory_stats: { usage: 1000, stats: { inactive_file: 150 } },
|
|
});
|
|
expect(calculateMemoryUsage(stats)).toBe(850);
|
|
});
|
|
|
|
test("should return raw usage when memory_stats.stats is undefined", () => {
|
|
const stats = createStats({ memory_stats: { usage: 512 } });
|
|
expect(calculateMemoryUsage(stats)).toBe(512);
|
|
});
|
|
});
|