internal/agent/hostinfo/hostinfo_test.go
Ref: Size: 11.4 KiB History
package hostinfo
import (
"context"
"encoding/hex"
"net"
"os"
"path/filepath"
"slices"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func writeFixture(t *testing.T, name, content string) string {
t.Helper()
p := filepath.Join(t.TempDir(), name)
require.NoError(t, os.WriteFile(p, []byte(content), 0o644))
return p
}
func TestParseOSRelease(t *testing.T) {
id, pretty, version := parseOSRelease(
"PRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\"\nID=debian\nVERSION_ID=\"12\"\nHOME_URL=\"x\"\n")
assert.Equal(t, "debian", id)
assert.Equal(t, "Debian GNU/Linux 12 (bookworm)", pretty)
assert.Equal(t, "12", version)
}
func TestParseOSReleaseEmpty(t *testing.T) {
id, pretty, version := parseOSRelease("")
assert.Empty(t, id)
assert.Empty(t, pretty)
assert.Empty(t, version)
}
func TestParseCPUModel(t *testing.T) {
assert.Equal(t, "AMD EPYC 7302P 16-Core Processor",
parseCPUModel("processor\t: 0\nmodel name\t: AMD EPYC 7302P 16-Core Processor\nflags\t: fpu\n"))
// arm-style cpuinfo has no "model name" line → empty, not a crash.
assert.Empty(t, parseCPUModel("processor\t: 0\nCPU implementer\t: 0x41\n"))
}
func TestParseMemAvailableKB(t *testing.T) {
kb, ok := parseMemAvailableKB("MemTotal: 8388608 kB\nMemAvailable: 4194304 kB\n")
require.True(t, ok)
assert.Equal(t, int64(4194304), kb)
_, ok = parseMemAvailableKB("MemTotal: 8388608 kB\n")
assert.False(t, ok)
}
// loadavgFixture is a real vm.loadavg reading captured from an Apple M1 on
// macOS 26.3.1. Keeping the raw bytes here is what makes a decoder for a
// platform CI never runs testable at all.
const loadavgFixture = "ae100000bf0c0000400b0000000000000008000000000000"
func TestDecodeLoadavg(t *testing.T) {
raw, err := hex.DecodeString(loadavgFixture)
require.NoError(t, err)
loads, ok := decodeLoadavg(raw)
require.True(t, ok)
// 4270/2048, 3263/2048, 2880/2048 — the figures the host itself reported.
assert.InDelta(t, 2.08, loads[0], 0.01)
assert.InDelta(t, 1.59, loads[1], 0.01)
assert.InDelta(t, 1.41, loads[2], 0.01)
}
// TestDecodeLoadavgRejectsUnknownShapes pins the best-effort contract: a
// struct that isn't the one we know yields no loads rather than nonsense ones,
// and a zero divisor never reaches a division.
func TestDecodeLoadavgRejectsUnknownShapes(t *testing.T) {
raw, err := hex.DecodeString(loadavgFixture)
require.NoError(t, err)
_, ok := decodeLoadavg(raw[:16]) // truncated: no fscale
assert.False(t, ok)
_, ok = decodeLoadavg(append(slices.Clone(raw), 0)) // longer than we know
assert.False(t, ok)
zeroScale := slices.Clone(raw)
copy(zeroScale[16:], make([]byte, 8))
_, ok = decodeLoadavg(zeroScale)
assert.False(t, ok)
}
// systemVersionFixture is /System/Library/CoreServices/SystemVersion.plist as
// captured from the target Mac.
const systemVersionFixture = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildID</key>
<string>E0AF3C06-11FB-11F1-A7CC-608CDE06C496</string>
<key>ProductBuildVersion</key>
<string>25D2128</string>
<key>ProductCopyright</key>
<string>1983-2026 Apple Inc.</string>
<key>ProductName</key>
<string>macOS</string>
<key>ProductUserVisibleVersion</key>
<string>26.3.1</string>
<key>ProductVersion</key>
<string>26.3.1</string>
<key>iOSSupportVersion</key>
<string>26.3</string>
</dict>
</plist>
`
func TestMacOSIdentity(t *testing.T) {
id, pretty, version := macOSIdentity(systemVersionFixture)
assert.Equal(t, "macos", id)
assert.Equal(t, "macOS 26.3.1", pretty)
assert.Equal(t, "26.3.1", version)
}
// TestMacOSIdentityBestEffort pins the package's contract on a plist that is
// missing, truncated mid-document, or from a future macOS that renamed keys:
// the OS is still identifiably a Mac, the version is simply absent.
func TestMacOSIdentityBestEffort(t *testing.T) {
id, pretty, version := macOSIdentity("")
assert.Equal(t, "macos", id)
assert.Equal(t, "macOS", pretty)
assert.Empty(t, version)
_, _, version = macOSIdentity("<key>ProductVersion</key>\n\t<string>26.3.1")
assert.Empty(t, version) // an unterminated value is not a value
}
// TestPlistStringMatchesWholeKeys guards the one way a substring scan can lie:
// ProductBuildVersion appears in the document BEFORE ProductVersion, and a
// looser match would report the build number as the OS version.
func TestPlistStringMatchesWholeKeys(t *testing.T) {
assert.Equal(t, "25D2128", plistString(systemVersionFixture, "ProductBuildVersion"))
assert.Equal(t, "26.3.1", plistString(systemVersionFixture, "ProductVersion"))
assert.Equal(t, "26.3", plistString(systemVersionFixture, "iOSSupportVersion"))
assert.Empty(t, plistString(systemVersionFixture, "NoSuchKey"))
// A key whose value is not a string has no value here, and must not report
// the next key's.
const mixed = "<key>A</key><data>ZmY=</data><key>B</key><string>b</string>"
assert.Empty(t, plistString(mixed, "A"))
assert.Equal(t, "b", plistString(mixed, "B"))
}
func TestFactsReadsFixtures(t *testing.T) {
oldOS, oldCPU, oldKern := osReleasePath, cpuInfoPath, kernelPath
defer func() { osReleasePath, cpuInfoPath, kernelPath = oldOS, oldCPU, oldKern }()
osReleasePath = writeFixture(t, "os-release",
"ID=debian\nPRETTY_NAME=\"Debian GNU/Linux 12 (bookworm)\"\nVERSION_ID=\"12\"\n")
cpuInfoPath = writeFixture(t, "cpuinfo", "model name\t: AMD EPYC 7302P 16-Core Processor\n")
kernelPath = writeFixture(t, "osrelease", "6.1.0-18-amd64\n")
f := Facts(context.Background(), func(_ context.Context, _ string, _ ...string) (string, error) { return "kvm", nil })
assert.Equal(t, "debian", f.GetOsId())
assert.Equal(t, "Debian GNU/Linux 12 (bookworm)", f.GetOsPretty())
assert.Equal(t, "12", f.GetOsVersion())
assert.Equal(t, "6.1.0-18-amd64", f.GetKernel())
assert.Equal(t, "AMD EPYC 7302P 16-Core Processor", f.GetCpuModel())
// Virt is per-platform, so its expectation lives with its platform:
// TestFactsVirtComesFromTheRunner (Linux), TestVirtIsNoneOnDarwin (Darwin).
}
func TestFactsBestEffortOnMissingFiles(t *testing.T) {
oldOS, oldCPU, oldKern := osReleasePath, cpuInfoPath, kernelPath
defer func() { osReleasePath, cpuInfoPath, kernelPath = oldOS, oldCPU, oldKern }()
osReleasePath = "/nonexistent/os-release"
cpuInfoPath = "/nonexistent/cpuinfo"
kernelPath = "/nonexistent/osrelease"
f := Facts(context.Background(), nil) // must not panic; nil Runner → virt ""
assert.Empty(t, f.GetOsId())
assert.Empty(t, f.GetKernel())
assert.Empty(t, f.GetCpuModel())
}
// TestBootIDIsStableWithinABoot guards the contract reconcile relies on: a
// single boot must always yield the same token, whatever the source.
func TestBootIDIsStableWithinABoot(t *testing.T) {
a := BootID()
b := BootID()
if a == "" {
t.Skip("no boot identity available in this environment")
}
if a != b {
t.Errorf("BootID must be stable within a boot: %q != %q", a, b)
}
}
// TestBootIDReadsSeam exercises the bootIDPath seam directly (rather than
// relying on the real kernel file being present/stable in CI) and asserts the
// trailing newline the kernel appends is trimmed.
func TestBootIDReadsSeam(t *testing.T) {
old := bootIDPath
defer func() { bootIDPath = old }()
bootIDPath = writeFixture(t, "boot_id", "1b2c3d4e-0000-1111-2222-333344445555\n")
assert.Equal(t, "1b2c3d4e-0000-1111-2222-333344445555", BootID())
}
// TestCapacityReportsTotals guards the contract the server relies on:
// Capacity must report host TOTALS (never free space), since the server
// derives available capacity by subtracting live VM specs from these numbers.
func TestCapacityReportsTotals(t *testing.T) {
c := Capacity(t.TempDir())
if c.GetVcpus() < 1 {
t.Errorf("vcpus must be at least 1, got %d", c.GetVcpus())
}
if c.GetMemMb() < 1 {
t.Errorf("mem_mb must be positive, got %d", c.GetMemMb())
}
if c.GetDiskGb() < 0 {
t.Errorf("disk_gb must not be negative, got %d", c.GetDiskGb())
}
}
// TestMetricsImplyTheCapacityTheSameProbeSees guards a coupling that reaches
// all the way to the control plane's placement refusal. The agent advertises
// this machine's totals clamped to its --max-* flags, and the clamp only lowers
// — so an advertisement BELOW the machine's real size is the server's only
// proof that an operator set a cap, and the metrics in the same report are
// where it reads that real size (see api.declaredLimit).
//
// Memory must therefore land on the nose: readMetrics computes used as
// total-minus-available, so used+available is exactly Capacity's mem_mb. Disk
// may only UNDERCOUNT — used+free omits the filesystem's reserved blocks — and
// the direction is the safe one: an undercount can fail to prove a cap, never
// invent one. Break either and the server starts refusing creates on hosts
// whose agents would have run them.
func TestMetricsImplyTheCapacityTheSameProbeSees(t *testing.T) {
dir := t.TempDir()
c, m := Capacity(dir), Metrics(dir)
if m.GetMemAvailableMb() == 0 {
t.Skip("memory probe unavailable on this machine")
}
assert.Equal(t, c.GetMemMb(), m.GetMemUsedMb()+m.GetMemAvailableMb(),
"mem_used_mb + mem_available_mb must be exactly the advertised total")
if m.GetDiskFreeGb() == 0 && m.GetDiskUsedGb() == 0 {
t.Skip("disk probe unavailable on this machine")
}
assert.LessOrEqual(t, m.GetDiskUsedGb()+m.GetDiskFreeGb(), c.GetDiskGb(),
"disk_used_gb + disk_free_gb may undercount the total, never overstate it")
}
// TestCapacityExactArithmetic and TestCapacityBestEffortOnSyscallFailure live
// in hostinfo_linux_test.go: they stub sysinfoFn/statfsFn with
// syscall.Sysinfo_t/Statfs_t literals, which only exist on Linux.
// TestBootIDMissingFile covers readBootID's error path directly (rather than
// only via the trimmed-happy-path seam test): an unreadable boot_id must
// yield "", per the package's best-effort contract, not a panic or error.
func TestBootIDMissingFile(t *testing.T) {
old := bootIDPath
defer func() { bootIDPath = old }()
bootIDPath = filepath.Join(t.TempDir(), "does-not-exist")
assert.Empty(t, BootID())
}
// TestMetricsComputes lives in hostinfo_linux_test.go: it stubs
// sysinfoFn/statfsFn with syscall.Sysinfo_t/Statfs_t literals, which only
// exist on Linux.
func TestUsableSourceAddr(t *testing.T) {
for _, tc := range []struct {
name string
addr net.Addr
want string
}{
{"routable v4", &net.UDPAddr{IP: net.ParseIP("192.168.0.190")}, "192.168.0.190"},
{"routable v6", &net.UDPAddr{IP: net.ParseIP("2001:db8::1")}, "2001:db8::1"},
{"v4-mapped v6 is unmapped", &net.UDPAddr{IP: net.ParseIP("::ffff:10.0.0.7")}, "10.0.0.7"},
{"loopback reaches nobody", &net.UDPAddr{IP: net.ParseIP("127.0.0.1")}, ""},
{"unspecified reaches nobody", &net.UDPAddr{IP: net.IPv4zero}, ""},
{"link-local reaches nobody", &net.UDPAddr{IP: net.ParseIP("169.254.1.5")}, ""},
{"not a udp address", &net.TCPAddr{IP: net.ParseIP("192.168.0.190")}, ""},
{"no address at all", &net.UDPAddr{}, ""},
} {
t.Run(tc.name, func(t *testing.T) {
if got := usableSourceAddr(tc.addr); got != tc.want {
t.Errorf("usableSourceAddr(%v) = %q, want %q", tc.addr, got, tc.want)
}
})
}
}
func TestUplinkAddr(t *testing.T) {
// Nowhere to route to: no answer, never a guess.
if got := UplinkAddr("not-an-address"); got != "" {
t.Errorf("UplinkAddr(garbage) = %q, want empty", got)
}
// A route that resolves to loopback names an address nothing else can
// dial, so the agent says nothing rather than sending it.
if got := UplinkAddr("127.0.0.1:9"); got != "" {
t.Errorf("UplinkAddr(loopback) = %q, want empty", got)
}
}