package hostcollect import ( "os" "path/filepath" "testing" ) func readFixture(t *testing.T, elements ...string) []byte { t.Helper() path := filepath.Join(append([]string{"testdata"}, elements...)...) data, err := os.ReadFile(path) //nolint:gosec // fixture path is test-controlled if err != nil { t.Fatalf("read fixture %s: %v", path, err) } return data } func TestParseProcStatReadsAggregateAndCores(t *testing.T) { sample, err := parseProcStat(readFixture(t, "proc-healthy", "stat")) if err != nil { t.Fatalf("parseProcStat returned error: %v", err) } // user+nice+system+idle+iowait+irq+softirq+steal, with guest and guest_nice left // out because the kernel already counts guest time inside user and nice. const wantTotal = 1234567 + 8901 + 234567 + 45678901 + 12345 + 0 + 6789 + 1234 if sample.all.total != wantTotal { t.Fatalf("aggregate total = %d, want %d", sample.all.total, wantTotal) } if sample.all.idle != 45678901 || sample.all.iowait != 12345 { t.Fatalf("unexpected idle/iowait: %+v", sample.all) } if len(sample.cores) != 2 { t.Fatalf("cores = %d, want 2", len(sample.cores)) } } func TestParseProcStatToleratesOldKernelsAndGarbage(t *testing.T) { sample, err := parseProcStat(readFixture(t, "proc-messy", "stat")) if err != nil { t.Fatalf("parseProcStat returned error: %v", err) } if sample.all.total != 1000+200+300+4000 { t.Fatalf("unexpected total for a four column kernel: %d", sample.all.total) } // cpu0 and cpu2 parse; "cpu-bogus" and the non-numeric "cpu9" line do not. if len(sample.cores) != 2 { t.Fatalf("cores = %d, want 2", len(sample.cores)) } } func TestParseProcStatRequiresAggregate(t *testing.T) { if _, err := parseProcStat([]byte("intr 1 2 3\nctxt 4\n")); err == nil { t.Fatal("expected an error when /proc/stat has no cpu line") } } func TestUtilisationComputesDeltaNotAbsoluteValue(t *testing.T) { previous := cpuTimes{total: 1000, idle: 800, iowait: 100} current := cpuTimes{total: 1200, idle: 900, iowait: 150} busy, iowait, ok := utilisation(previous, current) if !ok { t.Fatal("expected a usable delta") } if busy != 25 { t.Fatalf("busy = %v, want 25", busy) } if iowait != 25 { t.Fatalf("iowait = %v, want 25", iowait) } } func TestUtilisationRejectsCounterWrapAndStandstill(t *testing.T) { cases := map[string]struct{ previous, current cpuTimes }{ "total wrapped": {cpuTimes{total: 18446744073709551000, idle: 10, iowait: 1}, cpuTimes{total: 400, idle: 20, iowait: 2}}, "idle wrapped": {cpuTimes{total: 1000, idle: 900, iowait: 10}, cpuTimes{total: 1100, idle: 5, iowait: 11}}, "iowait wrapped": {cpuTimes{total: 1000, idle: 800, iowait: 100}, cpuTimes{total: 1100, idle: 850, iowait: 4}}, "no elapsed": {cpuTimes{total: 1000, idle: 800, iowait: 100}, cpuTimes{total: 1000, idle: 800, iowait: 100}}, "idle exceeds": {cpuTimes{total: 1000, idle: 800, iowait: 100}, cpuTimes{total: 1010, idle: 900, iowait: 120}}, } for name, testCase := range cases { t.Run(name, func(t *testing.T) { if _, _, ok := utilisation(testCase.previous, testCase.current); ok { t.Fatal("expected the delta to be rejected as unusable") } }) } } func TestParseMeminfoConvertsKibibytes(t *testing.T) { memory, err := parseMeminfo(readFixture(t, "proc-healthy", "meminfo")) if err != nil { t.Fatalf("parseMeminfo returned error: %v", err) } if memory.TotalBytes != 32819484*1024 { t.Fatalf("total = %d, want %d", memory.TotalBytes, 32819484*1024) } if memory.AvailableBytes != 24680240*1024 { t.Fatalf("available = %d", memory.AvailableBytes) } if memory.SwapTotalBytes != 8388604*1024 || memory.SwapUsedBytes != (8388604-8000000)*1024 { t.Fatalf("unexpected swap: %+v", memory) } } func TestParseMeminfoFallsBackWhenMemAvailableIsAbsent(t *testing.T) { memory, err := parseMeminfo(readFixture(t, "proc-messy", "meminfo")) if err != nil { t.Fatalf("parseMeminfo returned error: %v", err) } want := uint64(123456+23456+2000000+100000) * 1024 if memory.AvailableBytes != want { t.Fatalf("available = %d, want %d", memory.AvailableBytes, want) } if memory.SwapTotalBytes != 0 || memory.SwapUsedBytes != 0 { t.Fatalf("swapless host reported swap: %+v", memory) } } func TestParseMeminfoRequiresMemTotal(t *testing.T) { if _, err := parseMeminfo([]byte("MemFree: 100 kB\n")); err == nil { t.Fatal("expected an error without MemTotal") } } func TestParseLoadAverage(t *testing.T) { load, err := parseLoadAverage(readFixture(t, "proc-healthy", "loadavg")) if err != nil { t.Fatalf("parseLoadAverage returned error: %v", err) } if load.One != 1.52 || load.Five != 2.08 || load.Fifteen != 2.35 { t.Fatalf("unexpected load: %+v", load) } for _, malformed := range []string{"not-a-load\n", "1.0 2.0\n", "-1 2 3\n", ""} { if _, err := parseLoadAverage([]byte(malformed)); err == nil { t.Fatalf("expected an error for %q", malformed) } } } func TestParseUptime(t *testing.T) { seconds, err := parseUptime(readFixture(t, "proc-healthy", "uptime")) if err != nil { t.Fatalf("parseUptime returned error: %v", err) } if seconds != 351282.31 { t.Fatalf("uptime = %v", seconds) } for _, malformed := range []string{"", "nonsense\n", "-5 10\n", "999999999999 1\n"} { if _, err := parseUptime([]byte(malformed)); err == nil { t.Fatalf("expected an error for %q", malformed) } } } func TestParseNetDevHandlesMissingSpaceAfterColon(t *testing.T) { interfaces := parseNetDev(readFixture(t, "proc-healthy", "net", "dev")) byName := map[string]uint64{} for _, item := range interfaces { byName[item.Name] = item.RxBytes } // eth0's receive counter runs straight into the colon in the fixture. if byName["eth0"] != 18446744073709551615 { t.Fatalf("eth0 rx = %d", byName["eth0"]) } if _, present := byName["wlan0"]; present { t.Fatal("a row with a non-numeric counter must be dropped, not zeroed") } if _, present := byName["tap0"]; present { t.Fatal("a truncated row must be dropped") } if len(interfaces) != 3 { t.Fatalf("interfaces = %d, want lo, eth0 and br0", len(interfaces)) } for _, item := range interfaces { if item.Name == "eth0" && (item.RxErrors != 12 || item.RxDrops != 3 || item.TxErrors != 1 || item.TxDrops != 7) { t.Fatalf("unexpected eth0 error counters: %+v", item) } } } func TestParseMountsUnescapesOctalSequences(t *testing.T) { entries := parseMounts(readFixture(t, "proc-healthy", "mounts")) found := false for _, entry := range entries { if entry.mount == "/mnt/disks/Media Backup" { found = true if entry.fsType != "btrfs" { t.Fatalf("unexpected fs type: %q", entry.fsType) } } } if !found { t.Fatal("expected the escaped mount point to be decoded") } if len(entries) != 12 { t.Fatalf("entries = %d, want 12", len(entries)) } }