Files
pulse/pkg/diskinventory/status_test.go
T
Richard Courtman ff67e38968 Pair a merged disk's temperature with its rows' own states
The registry merges a physical disk's rows one step at a time, choosing
the temperature by source preference and merging collection states
separately. The pairing rule from #2618 was exact for two rows only: with
three or more, an earlier step's single merged state stood in for its
rows. A silent agent, an offline Unraid inventory and a stale Proxmox
copy could leave a withdrawn 41 labelled collected, and a legacy agent's
stateless reading could pick up another row's "unsupported".

The merged disk now keeps each row's current reading and state
(merge-time only, never serialized), and the shown value takes its state
from the readings of that value: a collected reading first, then the
agent's own report over a copy. A copy of an agent source's state gives
way to the agent's own withdrawal, including a legacy agent's sourceless
one, and a value source preference keeps after its row reported again
keeps its presented state under that row's later withdrawal. Value
selection is unchanged.
2026-10-07 11:18:10 +01:00

111 lines
4.6 KiB
Go

package diskinventory
import "testing"
func TestMergeStatusPrefersAvailableEvidencePerField(t *testing.T) {
existing := &CollectionStatus{
Serial: Available("smartctl"),
Temperature: Unavailable("smartctl", "deadline exceeded"),
}
incoming := &CollectionStatus{
Serial: Unsupported("provider", "not exposed"),
Temperature: Available("provider"),
IO: Missing("kernel", "counter absent"),
}
got := MergeStatus(existing, incoming)
if got.Serial.State != FieldAvailable || got.Serial.Source != "smartctl" {
t.Fatalf("available serial evidence was downgraded: %+v", got.Serial)
}
if got.Temperature.State != FieldAvailable || got.Temperature.Source != "provider" {
t.Fatalf("available temperature evidence did not win: %+v", got.Temperature)
}
if got.IO.State != FieldMissing {
t.Fatalf("missing I/O state was not retained: %+v", got.IO)
}
}
func TestMergeReportedStatusLetsTheSourceWithdrawItsOwnAvailability(t *testing.T) {
existing := &CollectionStatus{
Serial: Available("smartctl"),
Temperature: Available("smartctl"),
IO: Available("kernel_diskstats"),
Pool: Available("proxmox_zfs"),
}
reported := &CollectionStatus{
Serial: Available("smartctl"),
Temperature: Unavailable("smartctl", "host agent stopped reporting"),
IO: Unavailable("kernel_diskstats", "host agent stopped reporting"),
Pool: Unavailable("zpool", "command failed"),
}
got := MergeReportedStatus(existing, reported)
if got.Temperature != reported.Temperature || got.IO != reported.IO {
t.Fatalf("the source's later word on its own fields was ignored: %+v", got)
}
if got.Serial != Available("smartctl") {
t.Fatalf("still-available field changed: %+v", got.Serial)
}
if got.Pool != Available("proxmox_zfs") {
t.Fatalf("another source's available evidence was withdrawn: %+v", got.Pool)
}
// A copy that is already weaker never overrides a current report, and the
// rule is plain MergeStatus when either side is absent.
if got := MergeReportedStatus(reported, existing); got.Temperature != Available("smartctl") {
t.Fatalf("current report lost to a weaker copy: %+v", got.Temperature)
}
if got := MergeReportedStatus(nil, reported); got.Temperature != reported.Temperature {
t.Fatalf("nil existing: %+v", got)
}
if got := MergeReportedStatus(existing, nil); got.Temperature != existing.Temperature {
t.Fatalf("nil report: %+v", got)
}
if existing.Temperature != Available("smartctl") {
t.Fatal("MergeReportedStatus mutated its input")
}
}
func TestTemperatureCollectedOnlyForAReadingTakenByThisObservation(t *testing.T) {
for _, tc := range []struct {
name string
temperature int
status *CollectionStatus
want bool
}{
{"no reading", 0, &CollectionStatus{Temperature: Available("smartctl")}, false},
{"collected", 41, &CollectionStatus{Temperature: Available("smartctl")}, true},
{"predates collection state", 41, nil, true},
{"no temperature state", 41, &CollectionStatus{Serial: Available("smartctl")}, true},
{"retained in standby", 41, &CollectionStatus{Temperature: Unavailable("smartctl", "disk is in standby")}, false},
{"retained past the agent lease", 41, &CollectionStatus{Temperature: Unavailable("unraid", "host agent stopped reporting")}, false},
{"unsupported", 41, &CollectionStatus{Temperature: Unsupported("proxmox_disks", "not exposed")}, false},
{"missing", 41, &CollectionStatus{Temperature: Missing("smartctl", "attribute absent")}, false},
} {
if got := TemperatureCollected(tc.temperature, tc.status); got != tc.want {
t.Errorf("%s: TemperatureCollected = %v, want %v", tc.name, got, tc.want)
}
}
}
func TestLegacyHostAgentStatusFillsTheLegacyAgentProvenance(t *testing.T) {
stopped := "host agent stopped reporting"
for _, tc := range []struct {
name string
status FieldStatus
hasReading bool
want FieldStatus
}{
{"stateless reading", FieldStatus{}, true, Available(LegacyHostAgentSource)},
{"no reading", FieldStatus{}, false, FieldStatus{}},
{"sourceless withdrawal", Unavailable("", stopped), true, Unavailable(LegacyHostAgentSource, stopped)},
{"sourceless withdrawal without a reading", Unavailable("", stopped), false, Unavailable(LegacyHostAgentSource, stopped)},
{"current agent state", Unavailable("smartctl", "disk is in standby"), false, Unavailable("smartctl", "disk is in standby")},
{"current agent reading", Available("smartctl"), true, Available("smartctl")},
} {
if got := LegacyHostAgentStatus(tc.status, tc.hasReading); got != tc.want {
t.Errorf("%s: LegacyHostAgentStatus = %+v, want %+v", tc.name, got, tc.want)
}
}
}