Files
nasmon/internal/collect/smart_test.go
T
a.gritsenkoandClaude Sonnet 5 09159a5f31 Initial import: nasmon health monitor, split out of home_nas
Go service (nasmon/) plus nfpm packaging (packaging/), carried over from
the home_nas monorepo. Adds rpm packaging alongside the existing deb
build (Fedora/RHEL/Rocky dependency names via nfpm overrides, per-format
systemd unit path, dpkg+rpm-compatible maintainer scripts) and a Gitea
Actions workflow that builds both formats on every push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-19 10:24:34 +03:00

166 lines
7.1 KiB
Go

package collect
import "testing"
const smartATA = `{
"smartctl": {"version": [7, 4], "exit_status": 0, "messages": []},
"device": {"name": "/dev/sda", "type": "sat", "protocol": "ATA"},
"model_name": "WDC WD80EFZZ-68BTXN0",
"serial_number": "WD-AAA",
"firmware_version": "81.00A81",
"user_capacity": {"blocks": 15628053168, "bytes": 8001563222016},
"rotation_rate": 5640,
"smart_support": {"available": true, "enabled": true},
"smart_status": {"passed": true},
"ata_smart_data": {
"self_test": {"status": {"value": 0, "string": "completed without error", "passed": true}}
},
"ata_smart_attributes": {"table": [
{"id": 5, "name": "Reallocated_Sector_Ct", "value": 200, "worst": 200, "thresh": 140, "when_failed": "", "raw": {"value": 0, "string": "0"}},
{"id": 9, "name": "Power_On_Hours", "value": 90, "worst": 90, "thresh": 0, "when_failed": "", "raw": {"value": 7541, "string": "7541"}},
{"id": 194, "name": "Temperature_Celsius", "value": 116, "worst": 100, "thresh": 0, "when_failed": "", "raw": {"value": 34, "string": "34"}},
{"id": 197, "name": "Current_Pending_Sector", "value": 200, "worst": 200, "thresh": 0, "when_failed": "", "raw": {"value": 8, "string": "8"}},
{"id": 199, "name": "UDMA_CRC_Error_Count", "value": 200, "worst": 200, "thresh": 0, "when_failed": "", "raw": {"value": 2, "string": "2"}},
{"id": 188, "name": "Command_Timeout", "value": 100, "worst": 100, "thresh": 0, "when_failed": "", "raw": {"value": 196611, "string": "3 3 3"}}
]},
"power_on_time": {"hours": 7541},
"power_cycle_count": 21,
"temperature": {"current": 34},
"ata_smart_self_test_log": {"standard": {"revision": 1, "table": [
{"type": {"value": 2, "string": "Extended offline"}, "status": {"value": 0, "string": "Completed without error", "passed": true}, "lifetime_hours": 7500},
{"type": {"value": 1, "string": "Short offline"}, "status": {"value": 0, "string": "Completed without error", "passed": true}, "lifetime_hours": 7400}
]}}
}`
const smartNVMe = `{
"smartctl": {"version": [7, 4], "exit_status": 0},
"device": {"name": "/dev/nvme0", "type": "nvme", "protocol": "NVMe"},
"model_name": "Samsung SSD 980 500GB",
"serial_number": "S64ANL0T",
"firmware_version": "2B4QFXO7",
"user_capacity": {"blocks": 976773168, "bytes": 500107862016},
"smart_support": {"available": true, "enabled": true},
"smart_status": {"passed": true, "nvme": {"value": 0}},
"nvme_smart_health_information_log": {
"critical_warning": 0, "temperature": 41, "available_spare": 100, "available_spare_threshold": 10,
"percentage_used": 3, "media_errors": 0, "num_err_log_entries": 12, "unsafe_shutdowns": 30,
"power_cycles": 300, "power_on_hours": 1234
},
"temperature": {"current": 41},
"power_cycle_count": 300,
"power_on_time": {"hours": 1234},
"nvme_self_test_log": {"current_self_test_operation": {"value": 0, "string": "No self-test in progress"},
"table": [{"self_test_code": {"value": 1, "string": "Short"}, "self_test_result": {"value": 0, "string": "Completed without error"}, "power_on_hours": 1200}]}
}`
const smartStandby = `{
"smartctl": {"version": [7, 4], "exit_status": 2,
"messages": [{"string": "Device is in STANDBY mode, exit(2)", "severity": "information"}]},
"device": {"name": "/dev/sdb", "type": "sat", "protocol": "ATA"}
}`
const smartOpenFail = `{
"smartctl": {"version": [7, 4], "exit_status": 2,
"messages": [{"string": "Smartctl open device: /dev/sdz failed: No such device", "severity": "error"}]}
}`
const smartInProgress = `{
"smartctl": {"version": [7, 4], "exit_status": 0},
"device": {"protocol": "ATA"},
"serial_number": "X",
"smart_support": {"available": true, "enabled": true},
"smart_status": {"passed": true},
"ata_smart_data": {"self_test": {"status": {"value": 249, "string": "in progress, 90% remaining", "remaining_percent": 90}}},
"ata_smart_self_test_log": {"standard": {"table": [
{"type": {"string": "Short offline"}, "status": {"string": "Completed: read failure", "passed": false}, "lifetime_hours": 10}]}}
}`
func TestParseSmartATA(t *testing.T) {
s := parseSmart([]byte(smartATA))
if s.Error != "" || s.Standby {
t.Fatalf("unexpected error/standby: %+v", s)
}
if s.Protocol != "ATA" || s.Serial != "WD-AAA" || !s.Supported || !s.Healthy {
t.Errorf("basic fields: %+v", s)
}
if !s.HasTemp || s.TempC != 34 || s.PowerOnHrs != 7541 || s.PowerCycles != 21 {
t.Errorf("temp/hours: %+v", s)
}
if a, ok := s.Attr(197); !ok || a.Raw != 8 {
t.Errorf("attr 197: %+v %v", a, ok)
}
if a, ok := s.Attr(188); !ok || a.Raw != 3 {
t.Errorf("attr 188 must be unpacked to the low word: %+v", a)
}
if s.LastTest == nil || s.LastTest.InProgress || !s.LastTest.Passed || s.LastTest.Type != "Extended offline" || s.LastTest.Hours != 7500 {
t.Errorf("last test: %+v", s.LastTest)
}
}
func TestParseSmartNVMe(t *testing.T) {
s := parseSmart([]byte(smartNVMe))
if s.NVMe == nil {
t.Fatalf("nvme log missing: %+v", s)
}
if s.NVMe.PercentageUsed != 3 || s.NVMe.AvailableSpare != 100 || s.NVMe.ErrLogEntries != 12 {
t.Errorf("nvme: %+v", s.NVMe)
}
if s.TempC != 41 || s.PowerOnHrs != 1234 || !s.Healthy {
t.Errorf("fields: %+v", s)
}
if s.LastTest == nil || s.LastTest.Type != "Short" || !s.LastTest.Passed {
t.Errorf("last test: %+v", s.LastTest)
}
}
func TestParseSmartStandby(t *testing.T) {
s := parseSmart([]byte(smartStandby))
if !s.Standby || s.Error != "" {
t.Errorf("standby not detected: %+v", s)
}
}
func TestParseSmartOpenFail(t *testing.T) {
s := parseSmart([]byte(smartOpenFail))
if s.Standby || s.Error == "" || s.Supported {
t.Errorf("open failure not reported: %+v", s)
}
}
func TestParseSmartInProgress(t *testing.T) {
s := parseSmart([]byte(smartInProgress))
if s.LastTest == nil || !s.LastTest.InProgress || s.LastTest.Remaining != 90 {
t.Errorf("in-progress test: %+v", s.LastTest)
}
}
func TestParseSmartGarbage(t *testing.T) {
s := parseSmart([]byte("not json"))
if s.Error == "" {
t.Errorf("expected parse error")
}
}
func TestParseLsblk(t *testing.T) {
in := `{"blockdevices": [
{"name":"sda","path":"/dev/sda","type":"disk","model":"WDC WD80EFZZ-68BTXN0","serial":"WD-AAA","size":8001563222016,"rota":true,"tran":"sata"},
{"name":"nvme0n1","path":"/dev/nvme0n1","type":"disk","model":"Samsung SSD 980 500GB","serial":"S64ANL0T","size":500107862016,"rota":false,"tran":"nvme"},
{"name":"loop0","path":"/dev/loop0","type":"loop","model":null,"serial":null,"size":4096,"rota":false,"tran":null},
{"name":"zd0","path":"/dev/zd0","type":"disk","model":null,"serial":null,"size":4096,"rota":false,"tran":null},
{"name":"sr0","path":"/dev/sr0","type":"rom","model":"DVD","serial":null,"size":1024,"rota":true,"tran":"sata"}
]}`
disks, err := parseLsblk([]byte(in))
if err != nil {
t.Fatal(err)
}
if len(disks) != 2 || disks[0].Name != "sda" || !disks[0].Rota || disks[1].Tran != "nvme" || disks[1].Rota {
t.Errorf("disks = %+v", disks)
}
// older util-linux prints rota as a string
old := `{"blockdevices": [{"name":"sda","type":"disk","size":"1000","rota":"1","tran":"sata"}]}`
disks, err = parseLsblk([]byte(old))
if err != nil || len(disks) != 1 || !disks[0].Rota || disks[0].Path != "/dev/sda" {
t.Errorf("old lsblk: %+v %v", disks, err)
}
}