mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-17 21:33:43 +08:00
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]>
185 lines
6.2 KiB
Go
185 lines
6.2 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|