gates / gates (push) Successful in 31s
On a box whose guest has one data volume the two built-in fill targets, "/" (the controller's overlay on the Docker data volume) and /mnt/sys_drive, are one filesystem. The target list was de-duplicated by path only, so one fill would send two warnings under two names. fillTargets now keeps one target per filesystem; the first stays (app area, system area, drives). The identity is system.FilesystemKey, not st_dev alone: inside the container "/" is an overlay with its own device number, so an overlay path takes the device of a Docker-kept file on the data-root (/etc/hostname, /etc/hosts, /etc/resolv.conf). Measured read-only on demo-hp 9201/9202 and the N100 on 2026-11-10. Tests: TestR1371_* (3), TestFilesystemKey_* (2). Red seen three times. No version, no image, no floor: it waits for a release session. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
184 lines
7.2 KiB
Go
184 lines
7.2 KiB
Go
package main
|
|
|
|
import (
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/fillwatch"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/system"
|
|
)
|
|
|
|
// R-1371 — one warning per filesystem.
|
|
//
|
|
// The box as measured 2026-11-10 (demo-hp, the N100, Tester 1): "/" and `/mnt/sys_drive` are ONE filesystem, seen
|
|
// through two paths. The filesystem identity here is the one system.FilesystemKey gives on those boxes (its own rule
|
|
// is pinned by TestFilesystemKey_AnOverlayRootIsTheDockerDataRoot): equal for the two paths, another for a drive.
|
|
//
|
|
// RED-PROOF (observed 2026-11-10, then restored): with the filesystem check removed from fillTargetsBy this test fails
|
|
// with "one filling filesystem sent 2 warnings".
|
|
func boxKeys(extra map[string]uint64) func(string) (uint64, bool) {
|
|
keys := map[string]uint64{system.DockerVolumePath: 64519, "/mnt/sys_drive": 64519}
|
|
for p, k := range extra {
|
|
keys[p] = k
|
|
}
|
|
return func(p string) (uint64, bool) { k, ok := keys[p]; return k, ok }
|
|
}
|
|
|
|
func settingsWithDrives(t *testing.T, paths ...settings.StoragePath) *settings.Settings {
|
|
t.Helper()
|
|
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, sp := range paths {
|
|
if err := sett.AddStoragePath(sp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return sett
|
|
}
|
|
|
|
// The consequence: the one filesystem fills, and the household gets ONE warning, then ONE critical — through the real
|
|
// watcher and the real push closure.
|
|
func TestR1371_OneFillSendsOneWarning(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.Paths.SystemDataPath = "/mnt/sys_drive"
|
|
sett := settingsWithDrives(t, settings.StoragePath{Path: "/mnt/felhom-drives/usb", Label: "Családi lemez", Schedulable: true})
|
|
key := boxKeys(map[string]uint64{"/mnt/felhom-drives/usb": 2049})
|
|
|
|
pct := 50.0
|
|
usage := func(p string) *fillwatch.Usage {
|
|
if p == "/mnt/felhom-drives/usb" {
|
|
return &fillwatch.Usage{UsedPercent: 20, AvailGB: 700, UsedGB: 200, TotalGB: 900}
|
|
}
|
|
// "/" and /mnt/sys_drive: the same filesystem, so the same numbers.
|
|
return &fillwatch.Usage{UsedPercent: pct, AvailGB: 68.7 * (100 - pct) / 100, UsedGB: 68.7 * pct / 100, TotalGB: 68.7}
|
|
}
|
|
w := fillwatch.New(filepath.Join(t.TempDir(), "fill.json"), log.New(io.Discard, "", 0),
|
|
func() []fillwatch.Target { return fillTargetsBy(cfg, sett, key) }, usage)
|
|
rec := &fakeFillPusher{}
|
|
w.SetNotify(fillEventPush(rec))
|
|
|
|
check := func() {
|
|
t.Helper()
|
|
for i := 0; i < 2; i++ {
|
|
if err := w.Check(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
check()
|
|
if len(rec.got) != 0 {
|
|
t.Fatalf("a half-full filesystem sent %d events", len(rec.got))
|
|
}
|
|
pct = 90
|
|
check()
|
|
if len(rec.got) != 1 {
|
|
t.Fatalf("one filling filesystem sent %d warnings (%+v), want exactly 1", len(rec.got), rec.got)
|
|
}
|
|
if rec.got[0].eventType != "disk_warning" {
|
|
t.Fatalf("event = %s, want disk_warning", rec.got[0].eventType)
|
|
}
|
|
if d, _ := rec.got[0].details.(map[string]any); d == nil || d["path"] != system.DockerVolumePath {
|
|
t.Fatalf("the warning is for %v, want the first target of the filesystem (%q)", rec.got[0].details, system.DockerVolumePath)
|
|
}
|
|
pct = 97
|
|
check()
|
|
if len(rec.got) != 2 || rec.got[1].eventType != "disk_critical" {
|
|
t.Fatalf("after 97%% the events are %+v, want one warning then one critical", rec.got)
|
|
}
|
|
}
|
|
|
|
// What stays and what goes, target by target.
|
|
func TestR1371_TargetsByFilesystem(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.Paths.SystemDataPath = "/mnt/sys_drive"
|
|
paths := func(ts []fillwatch.Target) []string {
|
|
var out []string
|
|
for _, tg := range ts {
|
|
out = append(out, tg.Path)
|
|
}
|
|
return out
|
|
}
|
|
eq := func(got, want []string) bool {
|
|
if len(got) != len(want) {
|
|
return false
|
|
}
|
|
for i := range got {
|
|
if got[i] != want[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// One volume, no drive: one target, and it is the app area (the first one added).
|
|
got := fillTargetsBy(cfg, nil, boxKeys(nil))
|
|
if len(got) != 1 || got[0].Path != system.DockerVolumePath || got[0].LabelKey != "event.fill.label_apps" {
|
|
t.Fatalf("one-volume box: targets = %+v, want only the app area", got)
|
|
}
|
|
|
|
// The OLD layout (two volumes): both areas stay.
|
|
two := func(p string) (uint64, bool) {
|
|
return map[string]uint64{system.DockerVolumePath: 1, "/mnt/sys_drive": 2}[p], p == system.DockerVolumePath || p == "/mnt/sys_drive"
|
|
}
|
|
if got := paths(fillTargetsBy(cfg, nil, two)); !eq(got, []string{"/", "/mnt/sys_drive"}) {
|
|
t.Fatalf("two-volume box: targets = %v, want both areas", got)
|
|
}
|
|
|
|
sett := settingsWithDrives(t,
|
|
settings.StoragePath{Path: "/mnt/felhom-drives/a", Label: "A", Schedulable: true},
|
|
settings.StoragePath{Path: "/mnt/felhom-drives/b", Label: "B", Schedulable: true}, // a second folder of drive A's filesystem
|
|
settings.StoragePath{Path: "/mnt/felhom-drives/gone", Label: "Gone", Schedulable: true}, // unplugged: no identity
|
|
settings.StoragePath{Path: "/mnt/felhom-drives/gone2", Label: "Gone2", Schedulable: true}, // unplugged too
|
|
settings.StoragePath{Path: "/mnt/sys_drive/internal", Label: "Belső", Schedulable: true}, // a "drive" that is the system volume
|
|
)
|
|
key := boxKeys(map[string]uint64{"/mnt/felhom-drives/a": 2049, "/mnt/felhom-drives/b": 2049, "/mnt/sys_drive/internal": 64519})
|
|
want := []string{"/", "/mnt/felhom-drives/a", "/mnt/felhom-drives/gone", "/mnt/felhom-drives/gone2"}
|
|
if got := paths(fillTargetsBy(cfg, sett, key)); !eq(got, want) {
|
|
t.Fatalf("targets = %v, want %v — one per filesystem, and every path with no identity kept", got, want)
|
|
}
|
|
|
|
// No identity at all (not Linux, or nothing readable): the list is the old one, by path.
|
|
none := func(string) (uint64, bool) { return 0, false }
|
|
if got := paths(fillTargetsBy(cfg, nil, none)); !eq(got, []string{"/", "/mnt/sys_drive"}) {
|
|
t.Fatalf("no identity: targets = %v, want both paths (one warning too many, never one too few)", got)
|
|
}
|
|
}
|
|
|
|
// The wiring: the production list uses the real identity. On this machine "/mnt/sys_drive" does not exist, so it has
|
|
// no identity and stays; a folder and its sub-folder are one filesystem and collapse.
|
|
func TestR1371_ProductionListUsesTheRealIdentity(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, ok := system.FilesystemKey(dir); !ok {
|
|
t.Skip("no filesystem identity on this machine (not Linux)")
|
|
}
|
|
cfg := &config.Config{}
|
|
cfg.Paths.SystemDataPath = dir
|
|
sett := settingsWithDrives(t, settings.StoragePath{Path: filepath.Join(dir, "sub"), Label: "Sub", Schedulable: true})
|
|
if err := os.MkdirAll(filepath.Join(dir, "sub"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := fillTargets(cfg, sett)
|
|
for _, tg := range got {
|
|
if tg.Path == filepath.Join(dir, "sub") {
|
|
t.Fatalf("fillTargets kept %q, a sub-folder of the system area's filesystem: targets = %+v", tg.Path, got)
|
|
}
|
|
}
|
|
// "/" and dir are one filesystem or two; the list must hold exactly that many targets.
|
|
rootKey, okRoot := system.FilesystemKey(system.DockerVolumePath)
|
|
dirKey, _ := system.FilesystemKey(dir)
|
|
want := 2
|
|
if okRoot && rootKey == dirKey {
|
|
want = 1
|
|
}
|
|
if len(got) != want {
|
|
t.Fatalf("fillTargets = %+v, want %d target(s): one per filesystem among / and %s", got, want, dir)
|
|
}
|
|
}
|