Files
VoCat/internal/server/host_stats_test.go
T
MengMengCodeandClaude Opus 4.8 115598757a feat: dashboard host, performance, task, and online-rate cards
Add four cards to the dashboard:

- Host hardware card (CPU / motherboard / memory / disk model) backed by
  a new GET /api/dashboard/host endpoint that probes /proc and /sys once
  and caches the identities. x86 hosts read cpuinfo model name, DMI board
  data, dmidecode DIMM info, and block device models; ARM boards compose
  the device-tree SoC with the Cortex part name and fall back to memory
  capacity.
- Performance card with live CPU / memory / disk usage bars and real-time
  network up/down rates. Rates derive from cumulative kernel counters
  sampled on demand by dashboard polling (no background goroutine), with
  bridge/tunnel/vocat virtual interfaces excluded to avoid double
  counting.
- Upcoming scheduled tasks card listing the next three enabled automatic
  tasks with their run times.
- Module online rate card aggregating all recognized modules into one
  large percentage colored by four levels (red/orange/yellow/green).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-16 17:46:38 +08:00

185 lines
6.2 KiB
Go
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.
package server
import (
"testing"
)
func TestParseCPUTimes(t *testing.T) {
times, ok := parseCPUTimes("cpu 38073 0 24013 6762971 3121 0 3019 0 0 0")
if !ok {
t.Fatal("parseCPUTimes rejected a valid cpu line")
}
wantTotal := uint64(38073 + 0 + 24013 + 6762971 + 3121 + 0 + 3019 + 0)
if times.total != wantTotal {
t.Fatalf("total = %d, want %d", times.total, wantTotal)
}
if wantIdle := uint64(6762971 + 3121); times.idle != wantIdle {
t.Fatalf("idle = %d, want %d", times.idle, wantIdle)
}
if _, ok := parseCPUTimes("cpu0 1 2 3 4 5 6 7 8"); ok {
t.Fatal("per-core line must not parse as the aggregate line")
}
if _, ok := parseCPUTimes("cpu 1 2 3"); ok {
t.Fatal("truncated cpu line must not parse")
}
}
func TestCPUDelta(t *testing.T) {
prev := hostCPUTimes{idle: 100, total: 200}
next := hostCPUTimes{idle: 150, total: 300}
busy, total := cpuDelta(prev, next)
if busy != 50 || total != 100 {
t.Fatalf("cpuDelta = (%d, %d), want (50, 100)", busy, total)
}
if busy, total := cpuDelta(next, prev); busy != 0 || total != 0 {
t.Fatalf("backwards counters must report zero, got (%d, %d)", busy, total)
}
}
func TestParseMeminfo(t *testing.T) {
content := "MemTotal: 2040424 kB\nMemFree: 920864 kB\nMemAvailable: 1543480 kB\nBuffers: 315908 kB\n"
total, available, ok := parseMeminfo(content)
if !ok {
t.Fatal("parseMeminfo rejected valid content")
}
if total != 2040424*1024 {
t.Fatalf("total = %d, want %d", total, 2040424*1024)
}
if available != 1543480*1024 {
t.Fatalf("available = %d, want %d", available, 1543480*1024)
}
}
func TestParseNetDevCounters(t *testing.T) {
content := `Inter-| Receive | Transmit
face |bytes packets errs drop fifo frame compressed multicast|bytes packets errs drop fifo colls carrier compressed
lo: 10 1 0 0 0 0 0 0 20 2 0 0 0 0 0 0
eth0: 100 1 0 0 0 0 0 0 200 2 0 0 0 0 0 0
br-lan: 1000 1 0 0 0 0 0 0 2000 2 0 0 0 0 0 0
utun: 300 1 0 0 0 0 0 0 400 2 0 0 0 0 0 0
vocat50a684ceb0: 500 1 0 0 0 0 0 0 600 2 0 0 0 0 0 0
wwan0: 700 1 0 0 0 0 0 0 800 2 0 0 0 0 0 0
`
rx, tx := parseNetDevCounters(content)
// Only eth0 and wwan0 count; lo, br-lan, utun and vocat are virtual.
if rx != 800 || tx != 1000 {
t.Fatalf("rx,tx = %d,%d, want 800,1000", rx, tx)
}
}
func TestHostNetInterfaceCounted(t *testing.T) {
counted := []string{"eth0", "eth1", "wwan0", "usb0", "wlan0", "enp3s0", "pppoe-wan"}
for _, name := range counted {
if !hostNetInterfaceCounted(name) {
t.Fatalf("%s should be counted", name)
}
}
skipped := []string{"lo", "br-lan", "docker0", "veth123", "ip6tnl0", "sit0", "utun", "vocat50a684ceb0", "wg0", "tun0", "tailscale0", ""}
for _, name := range skipped {
if hostNetInterfaceCounted(name) {
t.Fatalf("%s should be skipped", name)
}
}
}
func TestParseCPUInfoModelX86(t *testing.T) {
content := "processor\t: 0\nvendor_id\t: GenuineIntel\nmodel name\t: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz\n"
if model := parseCPUInfoModel(content); model != "Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz" {
t.Fatalf("model = %q", model)
}
}
func TestParseCPUInfoARM(t *testing.T) {
content := "processor\t: 0\nBogoMIPS\t: 48.00\nCPU implementer\t: 0x41\nCPU part\t: 0xd03\nprocessor\t: 1\nCPU part\t: 0xd03\n"
if model := parseCPUInfoModel(content); model != "" {
t.Fatalf("ARM cpuinfo must not report an x86 model name, got %q", model)
}
part, processors := parseCPUInfoPart(content)
if part != "0xd03" || processors != 2 {
t.Fatalf("part,processors = %q,%d, want 0xd03,2", part, processors)
}
}
func TestParseCompatibleSoC(t *testing.T) {
raw := "xunlong,orangepi-zero3\x00allwinner,sun50i-h618\x00"
if soc := parseCompatibleSoC(raw); soc != "Allwinner sun50i-h618" {
t.Fatalf("soc = %q", soc)
}
if soc := parseCompatibleSoC(""); soc != "" {
t.Fatalf("empty compatible must yield empty soc, got %q", soc)
}
}
func TestComposeARMCPUModel(t *testing.T) {
model := composeARMCPUModel("Allwinner sun50i-h618", "0xd03", 4)
if model != "Allwinner sun50i-h618 · 4× Cortex-A53" {
t.Fatalf("model = %q", model)
}
if model := composeARMCPUModel("", "", 0); model != "" {
t.Fatalf("empty inputs must yield empty model, got %q", model)
}
}
func TestParseDmidecodeMemory(t *testing.T) {
output := `# dmidecode 3.3
Getting SMBIOS data from sysfs.
SMBIOS 3.0 present.
Handle 0x0010, DMI type 17, 40 bytes
Memory Device
Array Handle: 0x000F
Error Information Handle: Not Provided
Total Width: 64 bits
Data Width: 64 bits
Size: 8 GB
Form Factor: SODIMM
Type: DDR4
Speed: 2400 MT/s
Manufacturer: Samsung
Serial Number: 12345678
Part Number: M471A1K43CB1-CRC
Rank: 1
Handle 0x0011, DMI type 17, 40 bytes
Memory Device
Size: No Module Installed
Type: Unknown
`
if model := parseDmidecodeMemory(output); model != "8 GB DDR4 M471A1K43CB1-CRC" {
t.Fatalf("model = %q", model)
}
if model := parseDmidecodeMemory("Memory Device\n\tSize: No Module Installed\n"); model != "" {
t.Fatalf("unpopulated slots must yield empty model, got %q", model)
}
}
func TestClampPercent(t *testing.T) {
if clampPercent(-1) != 0 || clampPercent(101) != 100 || clampPercent(50) != 50 {
t.Fatal("clampPercent bounds violated")
}
}
func TestParseUintTrims(t *testing.T) {
if value, ok := parseUint(" 61440000 "); !ok || value != 61440000 {
t.Fatalf("parseUint = %d,%v", value, ok)
}
if _, ok := parseUint("not-a-number"); ok {
t.Fatal("parseUint accepted garbage")
}
}
func TestSkipHostDiskPrefixes(t *testing.T) {
skipped := []string{"loop0", "ram0", "zram0", "sr0", "nbd0", "dm-0", "md0", "mtdblock0", "ubiblock0_0"}
for _, name := range skipped {
if !skipHostDisk(name) {
t.Fatalf("%s should be skipped", name)
}
}
kept := []string{"sda", "nvme0n1", "mmcblk0", "vda", "sdb"}
for _, name := range kept {
if skipHostDisk(name) {
t.Fatalf("%s should be kept", name)
}
}
}