Fix CI SMART and grant refresh tests

Refs Build and Test failures on pulse/v6-release.
This commit is contained in:
rcourtman 2026-06-11 16:26:21 +01:00
parent 70a20f6ac7
commit 7e26592f11
3 changed files with 76 additions and 48 deletions

View file

@ -14,29 +14,24 @@ import (
"github.com/rs/zerolog/log"
)
func TestCollectLocal_LogsStructuredContextOnDeviceCollectionFailure(t *testing.T) {
func TestCollectLocal_LogsStructuredContextAndIdentityOnlyDiskOnDeviceCollectionFailure(t *testing.T) {
stubLinuxSysfs(t,
[]string{"sda"},
map[string]string{
"/sys/block/sda/size": "200\n",
"/sys/block/sda/device/model": "Troubled Disk\n",
},
)
origRun := smartRunCommandOutput
origLook := execLookPath
origGOOS := runtimeGOOS
origReadDir := readDir
t.Cleanup(func() {
smartRunCommandOutput = origRun
execLookPath = origLook
runtimeGOOS = origGOOS
readDir = origReadDir
})
runtimeGOOS = "linux"
readDir = func(string) ([]os.DirEntry, error) { return nil, errors.New("sysfs unavailable") }
execLookPath = func(string) (string, error) { return "smartctl", nil }
smartRunCommandOutput = func(ctx context.Context, name string, args ...string) ([]byte, error) {
if name == "lsblk" {
data := lsblkJSON{Blockdevices: []lsblkDevice{
{Name: "sda", Type: "disk", Tran: "sata", Subsystems: "block:scsi:pci"},
}}
out, _ := json.Marshal(data)
return out, nil
}
if name == "smartctl" {
return nil, errors.New("read failed")
}
@ -48,14 +43,22 @@ func TestCollectLocal_LogsStructuredContextOnDeviceCollectionFailure(t *testing.
if err != nil {
t.Fatalf("CollectSMARTLocal returned unexpected error: %v", err)
}
if len(results) != 0 {
t.Fatalf("CollectSMARTLocal returned results despite command failure: %#v", results)
if len(results) != 1 {
t.Fatalf("expected failed physical disk to remain visible as identity-only, got %#v", results)
}
disk := results[0]
if disk.Device != "sda" || disk.Model != "Troubled Disk" || disk.SizeBytes != 102400 {
t.Fatalf("unexpected identity-only disk: %#v", disk)
}
if disk.Health != "UNKNOWN" || disk.Temperature != 0 || disk.Attributes != nil {
t.Fatalf("identity-only disk must not fabricate SMART data: %#v", disk)
}
for _, expected := range []string{
`"component":"smartctl_collector"`,
`"action":"collect_device_smart_failed"`,
`"device":"/dev/sda"`,
`"action":"identity_only_disk"`,
`"action":"collect_local_complete"`,
} {
if !strings.Contains(logOutput.String(), expected) {

View file

@ -467,31 +467,34 @@ func TestCollectSMARTLocalListDevicesError(t *testing.T) {
}
}
func TestCollectSMARTLocalSkipsErrors(t *testing.T) {
func TestCollectSMARTLocalReportsIdentityOnlyForProbeErrors(t *testing.T) {
stubLinuxSysfs(t,
[]string{"sda", "sdb"},
map[string]string{
"/sys/block/sda/size": "100\n",
"/sys/block/sda/device/model": "Failed Disk\n",
"/sys/block/sdb/size": "200\n",
},
)
origRun := smartRunCommandOutput
origLook := execLookPath
origNow := timeNow
origReadDir := readDir
t.Cleanup(func() {
smartRunCommandOutput = origRun
execLookPath = origLook
timeNow = origNow
readDir = origReadDir
})
fixed := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC)
timeNow = func() time.Time { return fixed }
readDir = func(string) ([]os.DirEntry, error) { return nil, errors.New("sysfs unavailable") }
execLookPath = func(string) (string, error) { return "smartctl", nil }
smartRunCommandOutput = func(ctx context.Context, name string, args ...string) ([]byte, error) {
if name == "lsblk" {
return mockLsblkJSON(
lsblkDevice{Name: "sda", Type: "disk", Tran: "sata", Subsystems: "block:scsi:pci"},
lsblkDevice{Name: "sdb", Type: "disk", Tran: "sata", Subsystems: "block:scsi:pci"},
), nil
}
if name == "smartctl" {
if len(args) == 1 && args[0] == "--scan-open" {
return nil, nil
}
device := args[len(args)-1]
if strings.Contains(device, "sda") {
return nil, errors.New("read error")
@ -515,11 +518,32 @@ func TestCollectSMARTLocalSkipsErrors(t *testing.T) {
if err != nil {
t.Fatalf("CollectSMARTLocal error: %v", err)
}
if len(result) != 1 {
t.Fatalf("expected 1 result, got %d", len(result))
if len(result) != 2 {
t.Fatalf("expected successful disk plus identity-only failed disk, got %#v", result)
}
if result[0].Device != "sdb" || !result[0].LastUpdated.Equal(fixed) {
t.Fatalf("unexpected result: %#v", result[0])
byDevice := make(map[string]DiskSMART, len(result))
for _, disk := range result {
byDevice[disk.Device] = disk
}
sdb, ok := byDevice["sdb"]
if !ok {
t.Fatalf("expected successful sdb result, got %#v", result)
}
if sdb.Health != "PASSED" || sdb.Temperature != 30 || !sdb.LastUpdated.Equal(fixed) {
t.Fatalf("unexpected successful disk result: %#v", sdb)
}
sda, ok := byDevice["sda"]
if !ok {
t.Fatalf("expected failed sda to remain visible as identity-only, got %#v", result)
}
if sda.Model != "Failed Disk" || sda.SizeBytes != 51200 {
t.Fatalf("unexpected identity-only disk: %#v", sda)
}
if sda.Health != "UNKNOWN" || sda.Temperature != 0 || sda.Attributes != nil || !sda.LastUpdated.Equal(fixed) {
t.Fatalf("identity-only disk must not fabricate SMART data: %#v", sda)
}
}

View file

@ -26,6 +26,23 @@ func TestGrantRefreshLoop_StartStop(t *testing.T) {
svc.StopGrantRefresh() // Duplicate stop is a no-op.
}
func setImmediateGrantRefreshLoop(t *testing.T, svc *Service) {
t.Helper()
svc.SetRefreshHints(RefreshHints{IntervalSeconds: 60, JitterPercent: 0})
svc.mu.RLock()
loop := svc.grantRefresh
svc.mu.RUnlock()
if loop == nil {
t.Fatal("expected grant refresh loop to be initialized")
}
loop.mu.Lock()
loop.refreshInterval = time.Millisecond
loop.jitterPercent = 0
loop.mu.Unlock()
}
func TestGrantRefreshLoop_RefreshesGrant(t *testing.T) {
setupTestPublicKey(t)
const expectedClientVersion = "6.0.0-rc.1"
@ -205,14 +222,8 @@ func TestGrantRefreshLoop_NonRevocation401KeepsActivation(t *testing.T) {
t.Fatalf("RestoreActivation: %v", err)
}
setImmediateGrantRefreshLoop(t, svc)
svc.StartGrantRefresh(context.Background())
svc.mu.RLock()
loop := svc.grantRefresh
svc.mu.RUnlock()
loop.mu.Lock()
loop.refreshInterval = time.Millisecond
loop.jitterPercent = 0
loop.mu.Unlock()
// Wait for at least one failed refresh attempt.
deadline := time.After(5 * time.Second)
@ -303,17 +314,7 @@ func TestGrantRefreshLoop_401ClearsActivation(t *testing.T) {
t.Fatalf("RestoreActivation: %v", err)
}
// Set a very short refresh interval so the loop fires immediately.
svc.SetRefreshHints(RefreshHints{IntervalSeconds: 60, JitterPercent: 0.01})
// Override the loop's interval directly for instant firing.
svc.mu.RLock()
loop := svc.grantRefresh
svc.mu.RUnlock()
loop.mu.Lock()
loop.refreshInterval = time.Millisecond
loop.jitterPercent = 0
loop.mu.Unlock()
setImmediateGrantRefreshLoop(t, svc)
// Start the refresh loop — it should hit 401 and self-exit after clearing state.
svc.StartGrantRefresh(context.Background())