mesh/merge-gate head c7a4bf5c11d6 merged into trunk d8e785bb4b4b — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head c7a4bf5c11d6 merged into trunk d8e785bb4b4b — pass: its merge-check.sh passed
mesh/delivery delivered
Go's monotonic clock stops while a laptop is suspended, so a day judged by it could pass a day and a half of wall time before the machine said what it is. And a hung statfs left a goroutine and thread blocked per apply; now one check runs at a time and reports after it say the sizes unknown until it ends.
186 lines
6.8 KiB
Go
186 lines
6.8 KiB
Go
package profile
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"runtime"
|
|
"strings"
|
|
"sync/atomic"
|
|
"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{})
|
|
var asked atomic.Int32
|
|
src.Statfs = func(string) (int64, int64, error) { asked.Add(1); <-block; return 0, 0, nil }
|
|
defer func() {
|
|
close(block)
|
|
// The check that hung ends once its filesystem answers, and lets the next one run.
|
|
for !sizing.TryLock() {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
sizing.Unlock()
|
|
}()
|
|
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])
|
|
}
|
|
stuck := asked.Load()
|
|
|
|
// Every report while it hangs says so at once, and asks no filesystem again: no goroutine is left
|
|
// blocked behind the hung one per apply.
|
|
for i := 0; i < 3; i++ {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
m = DescribeMachine(ctx, src)
|
|
cancel()
|
|
if !strings.Contains(m.Unknown[FactFilesystems], "has not finished yet") {
|
|
t.Errorf("a report while the check hangs said %q", m.Unknown[FactFilesystems])
|
|
}
|
|
}
|
|
if asked.Load() != stuck {
|
|
t.Errorf("filesystems asked %d times while one check hung, was %d", asked.Load(), stuck)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|