Files
mesh-host/internal/profile/machine_test.go
T
jochen ec983bfc3d
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Have a machine say what it is made of, and say it once a day unasked (issue 160)
The mesh knew a machine's architecture and kernel family and nothing it is made of, and heard even
that only when it sent the machine something. The profile now carries the machine's memory, each local
filesystem's size and free space, processor model, cores and threads, distribution and version, kernel
release, virtual or physical, boot time and timezone; a fact it cannot read is named unknown with why,
never zeroed. The reconcile says the profile unasked once the mesh last heard it 23 hours ago, so on a
machine nobody pushed to nothing the mesh holds of it is older than a day.
2026-10-12 17:21:36 +02:00

162 lines
6.0 KiB
Go

package profile
import (
"context"
"encoding/json"
"errors"
"os"
"runtime"
"strings"
"testing"
"time"
)
// The parsing layer, with the machine's files given (novox/hq ADR 0017 allows a fake here and only here);
// the real machine is asked below.
var files = map[string]string{
"/proc/sys/kernel/osrelease": "7.2.8-arch1-2\n",
"/etc/os-release": "NAME=\"Arch Linux\"\nPRETTY_NAME=\"Arch Linux\"\nID=arch\nBUILD_ID=rolling\n",
"/proc/cpuinfo": "processor\t: 0\nmodel name\t: AMD Ryzen 9 5900HS\nphysical id\t: 0\ncore id\t\t: 0\n\n" +
"processor\t: 1\nmodel name\t: AMD Ryzen 9 5900HS\nphysical id\t: 0\ncore id\t\t: 0\n\n" +
"processor\t: 2\nmodel name\t: AMD Ryzen 9 5900HS\nphysical id\t: 0\ncore id\t\t: 1\n",
"/proc/meminfo": "MemTotal: 32000000 kB\nMemFree: 1 kB\n",
"/proc/stat": "cpu 1 2 3\nbtime 1791800000\n",
"/proc/self/mounts": "/dev/nvme0n1p2 / btrfs rw 0 0\n/dev/nvme0n1p2 /home btrfs rw 0 0\n" +
"proc /proc proc rw 0 0\ntank/data /srv/my\\040data zfs rw 0 0\n" +
"server:/x /mnt/x nfs4 rw 0 0\n/dev/loop3 /snap/core/1 squashfs ro 0 0\n",
}
func given(read map[string]string) Sources {
return Sources{
Read: func(p string) ([]byte, error) {
if s, ok := read[p]; ok {
return []byte(s), nil
}
return nil, &os.PathError{Op: "open", Path: p, Err: os.ErrNotExist}
},
Readlink: func(string) (string, error) { return "/usr/share/zoneinfo/Europe/Brussels", nil },
Run: func(context.Context, string, ...string) (string, error) { return "none\n", errors.New("exited 1") },
Statfs: func(p string) (int64, int64, error) { return 1000, 400, nil },
Now: func() time.Time { return time.Date(2026, 10, 12, 15, 0, 0, 0, time.UTC) },
}
}
func TestMachine_saysEveryFactItCanRead(t *testing.T) {
m := DescribeMachine(context.Background(), given(files))
if m.KernelRelease != "7.2.8-arch1-2" || m.Distribution != "Arch Linux" || m.Version != "rolling" {
t.Errorf("kernel %q, distribution %q %q", m.KernelRelease, m.Distribution, m.Version)
}
if m.Processor != "AMD Ryzen 9 5900HS" || m.Cores != 2 || m.Threads != 3 {
t.Errorf("processor %q, %d cores, %d threads", m.Processor, m.Cores, m.Threads)
}
if m.MemoryBytes != 32000000*1024 {
t.Errorf("memory %d", m.MemoryBytes)
}
if m.Virtual != "physical" {
t.Errorf("systemd-detect-virt said none and exited 1; read as %q", m.Virtual)
}
if m.BootedAt == nil || m.BootedAt.Unix() != 1791800000 {
t.Errorf("booted at %v", m.BootedAt)
}
if m.Timezone != "Europe/Brussels" {
t.Errorf("timezone %q", m.Timezone)
}
// One btrfs device under its shortest mount, the zfs dataset with its escaped space, and no network or
// read-only image filesystem.
var mounts []string
for _, f := range m.Filesystems {
mounts = append(mounts, f.Mount)
if f.SizeBytes != 1000 || f.FreeBytes != 400 {
t.Errorf("%s: size %d free %d", f.Mount, f.SizeBytes, f.FreeBytes)
}
}
if got := strings.Join(mounts, ","); got != "/,/srv/my data" {
t.Errorf("filesystems %q", got)
}
// Everything read is not unknown; the public address is, with why.
for fact := range m.Unknown {
if fact != FactPublicAddress {
t.Errorf("%s said unknown though it was readable: %s", fact, m.Unknown[fact])
}
}
if m.Unknown[FactPublicAddress] == "" {
t.Error("the public address is not said, and not said unknown either")
}
}
// The acceptance of issue 160: a machine that cannot determine a fact says so rather than reporting a zero.
func TestMachine_aFactItCannotReadIsSaidUnknownNeverZero(t *testing.T) {
src := given(map[string]string{})
src.Readlink = func(string) (string, error) { return "", os.ErrNotExist }
src.Run = func(context.Context, string, ...string) (string, error) { return "", errors.New("not found") }
m := DescribeMachine(context.Background(), src)
raw, _ := json.Marshal(m)
var said map[string]any
_ = json.Unmarshal(raw, &said)
for key, v := range said {
if key == "observed_at" || key == "unknown" {
continue
}
t.Errorf("%s is said as %v from a machine where nothing could be read", key, v)
}
for _, fact := range MachineFacts {
if strings.TrimSpace(m.Unknown[fact]) == "" {
t.Errorf("%s is neither said nor said unknown with why", fact)
}
}
}
func TestMachine_aHungFilesystemCannotHangTheReport(t *testing.T) {
src := given(files)
block := make(chan struct{})
defer close(block)
src.Statfs = func(string) (int64, int64, error) { <-block; return 0, 0, nil }
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
m := DescribeMachine(ctx, src)
if len(m.Filesystems) != 0 || !strings.Contains(m.Unknown[FactFilesystems], "did not finish") {
t.Errorf("filesystems %v, unknown %q", m.Filesystems, m.Unknown[FactFilesystems])
}
}
func TestMachine_coresUnsaidAreUnknownNotGuessed(t *testing.T) {
p := processorOf("processor\t: 0\nModel\t: Raspberry Pi 4\nprocessor\t: 1\n")
if p.model != "Raspberry Pi 4" || p.threads != 2 || p.cores != 0 || p.unknown[FactCores] == "" {
t.Errorf("%+v", p)
}
}
// Against the real machine (ADR 0017): what it says agrees with what this process sees.
func TestAgainstThisMachine_machineSaysWhatItIs(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("the facts are read from Linux's own files")
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
m := DescribeMachine(ctx, RealSources())
if m.MemoryBytes <= 0 && m.Unknown[FactMemory] == "" {
t.Error("memory neither said nor unknown")
}
if m.Threads != 0 && m.Threads < runtime.NumCPU() {
t.Errorf("%d threads said, and this process alone may use %d", m.Threads, runtime.NumCPU())
}
if m.BootedAt != nil && m.BootedAt.After(time.Now()) {
t.Errorf("booted in the future: %v", m.BootedAt)
}
for _, f := range m.Filesystems {
if f.SizeBytes <= 0 || f.FreeBytes > f.SizeBytes {
t.Errorf("%s: size %d free %d", f.Mount, f.SizeBytes, f.FreeBytes)
}
}
for _, fact := range MachineFacts {
if why, unknown := m.Unknown[fact]; unknown && strings.TrimSpace(why) == "" {
t.Errorf("%s unknown with no why", fact)
}
}
raw, _ := json.MarshalIndent(m, "", " ")
t.Logf("%s", raw)
}