Add unit tests for ZFS device conversion functions
Tests added by ADA run #97 but commit was missed. Covers: RaidZ types, log/cache/spare devices, nested mirrors, ConvertToModelZFSPool, and struct field tests.
This commit is contained in:
parent
72113dcec6
commit
527723c0a3
1 changed files with 440 additions and 0 deletions
|
|
@ -68,3 +68,443 @@ func TestConvertDeviceRecursiveSkipsHealthyLeaf(t *testing.T) {
|
|||
t.Fatalf("expected healthy leaf to be omitted, got %d entries", len(devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_RaidZTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
vdevName string
|
||||
expectedType string
|
||||
}{
|
||||
{"raidz1", "raidz1-0", "raidz1-0"},
|
||||
{"raidz2", "raidz2-0", "raidz2-0"},
|
||||
{"raidz3", "raidz3-0", "raidz3-0"},
|
||||
{"raidz", "raidz-0", "raidz-0"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dev := ZFSPoolDevice{
|
||||
Name: tc.vdevName,
|
||||
State: "DEGRADED",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "sda", State: "ONLINE", Leaf: 1},
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(dev, "")
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != tc.expectedType {
|
||||
t.Errorf("Type = %q, want %q", devices[0].Type, tc.expectedType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_LogDevices(t *testing.T) {
|
||||
// Log vdev container
|
||||
logVdev := ZFSPoolDevice{
|
||||
Name: "logs",
|
||||
State: "",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "nvme0n1p1", State: "ONLINE", Leaf: 1, Read: 1}, // has errors, should appear
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(logVdev, "")
|
||||
|
||||
// Should have the child with errors
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device (child with errors), got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != "log" {
|
||||
t.Errorf("child Type = %q, want log", devices[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_CacheDevices(t *testing.T) {
|
||||
// Cache vdev container
|
||||
cacheVdev := ZFSPoolDevice{
|
||||
Name: "cache",
|
||||
State: "",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "nvme1n1", State: "ONLINE", Leaf: 1, Write: 5}, // has errors
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(cacheVdev, "")
|
||||
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != "cache" {
|
||||
t.Errorf("Type = %q, want cache", devices[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_SpareDevices(t *testing.T) {
|
||||
// Spare vdev container
|
||||
spareVdev := ZFSPoolDevice{
|
||||
Name: "spares",
|
||||
State: "",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "sdc", State: "AVAIL", Leaf: 1},
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(spareVdev, "")
|
||||
|
||||
// Healthy spares with state AVAIL should be skipped
|
||||
if len(devices) != 0 {
|
||||
t.Fatalf("expected 0 devices (healthy spare), got %d", len(devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_SpareWithErrors(t *testing.T) {
|
||||
// When a spare is directly a leaf with "spare-X" name pattern
|
||||
spareLeaf := ZFSPoolDevice{
|
||||
Name: "spare-0",
|
||||
State: "FAULTED",
|
||||
Leaf: 1,
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(spareLeaf, "")
|
||||
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device (faulted spare), got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != "spare" {
|
||||
t.Errorf("Type = %q, want spare", devices[0].Type)
|
||||
}
|
||||
if devices[0].State != "FAULTED" {
|
||||
t.Errorf("State = %q, want FAULTED", devices[0].State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_NestedMirror(t *testing.T) {
|
||||
// Nested structure: mirror with healthy children
|
||||
mirror := ZFSPoolDevice{
|
||||
Name: "mirror-0",
|
||||
State: "ONLINE",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "sda", State: "ONLINE", Leaf: 1},
|
||||
{Name: "sdb", State: "ONLINE", Leaf: 1},
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(mirror, "")
|
||||
|
||||
// All healthy, should be empty
|
||||
if len(devices) != 0 {
|
||||
t.Fatalf("expected 0 devices (all healthy), got %d", len(devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_EmptyState(t *testing.T) {
|
||||
dev := ZFSPoolDevice{
|
||||
Name: "sda",
|
||||
State: "",
|
||||
Leaf: 1,
|
||||
Read: 1, // has errors
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(dev, "")
|
||||
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||
}
|
||||
if devices[0].State != "UNKNOWN" {
|
||||
t.Errorf("State = %q, want UNKNOWN", devices[0].State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_SlogDevice(t *testing.T) {
|
||||
// Separate log device (slog)
|
||||
slog := ZFSPoolDevice{
|
||||
Name: "slog-0",
|
||||
State: "DEGRADED",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "nvme0n1p1", State: "ONLINE", Leaf: 1},
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(slog, "")
|
||||
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device (degraded slog vdev), got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != "log" {
|
||||
t.Errorf("Type = %q, want log", devices[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_L2arcDevice(t *testing.T) {
|
||||
l2arc := ZFSPoolDevice{
|
||||
Name: "l2arc-0",
|
||||
State: "DEGRADED",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "nvme1n1", State: "ONLINE", Leaf: 1},
|
||||
},
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(l2arc, "")
|
||||
|
||||
if len(devices) != 1 {
|
||||
t.Fatalf("expected 1 device, got %d", len(devices))
|
||||
}
|
||||
if devices[0].Type != "cache" {
|
||||
t.Errorf("Type = %q, want cache", devices[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertDeviceRecursive_InUseSpare(t *testing.T) {
|
||||
spare := ZFSPoolDevice{
|
||||
Name: "spare-0",
|
||||
State: "INUSE",
|
||||
Leaf: 1,
|
||||
}
|
||||
|
||||
devices := convertDeviceRecursive(spare, "")
|
||||
|
||||
// INUSE is a healthy state, should be skipped
|
||||
if len(devices) != 0 {
|
||||
t.Fatalf("expected 0 devices (INUSE spare is healthy), got %d", len(devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_ConvertToModelZFSPool(t *testing.T) {
|
||||
info := &ZFSPoolInfo{
|
||||
Name: "tank",
|
||||
Health: "ONLINE",
|
||||
Size: 1000000000000,
|
||||
Alloc: 500000000000,
|
||||
Free: 500000000000,
|
||||
Frag: 10,
|
||||
Dedup: 1.5,
|
||||
State: "ONLINE",
|
||||
Status: "All vdevs healthy",
|
||||
Scan: "scrub completed",
|
||||
Errors: "No known data errors",
|
||||
Devices: []ZFSPoolDevice{
|
||||
{
|
||||
Name: "mirror-0",
|
||||
State: "ONLINE",
|
||||
Leaf: 0,
|
||||
Children: []ZFSPoolDevice{
|
||||
{Name: "sda", State: "ONLINE", Leaf: 1, Read: 1, Write: 2, Cksum: 3},
|
||||
{Name: "sdb", State: "ONLINE", Leaf: 1},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pool := info.ConvertToModelZFSPool()
|
||||
|
||||
if pool == nil {
|
||||
t.Fatal("ConvertToModelZFSPool returned nil")
|
||||
}
|
||||
if pool.Name != "tank" {
|
||||
t.Errorf("Name = %q, want tank", pool.Name)
|
||||
}
|
||||
if pool.State != "ONLINE" {
|
||||
t.Errorf("State = %q, want ONLINE", pool.State)
|
||||
}
|
||||
if pool.Health != "ONLINE" {
|
||||
t.Errorf("Health = %q, want ONLINE", pool.Health)
|
||||
}
|
||||
if pool.Status != "All vdevs healthy" {
|
||||
t.Errorf("Status = %q, want 'All vdevs healthy'", pool.Status)
|
||||
}
|
||||
if pool.Scan != "scrub completed" {
|
||||
t.Errorf("Scan = %q, want 'scrub completed'", pool.Scan)
|
||||
}
|
||||
if pool.Errors != "No known data errors" {
|
||||
t.Errorf("Errors = %q, want 'No known data errors'", pool.Errors)
|
||||
}
|
||||
// Only device with errors should be in the list
|
||||
if len(pool.Devices) != 1 {
|
||||
t.Errorf("Devices count = %d, want 1", len(pool.Devices))
|
||||
}
|
||||
if pool.ReadErrors != 1 {
|
||||
t.Errorf("ReadErrors = %d, want 1", pool.ReadErrors)
|
||||
}
|
||||
if pool.WriteErrors != 2 {
|
||||
t.Errorf("WriteErrors = %d, want 2", pool.WriteErrors)
|
||||
}
|
||||
if pool.ChecksumErrors != 3 {
|
||||
t.Errorf("ChecksumErrors = %d, want 3", pool.ChecksumErrors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_ConvertToModelZFSPool_NilReceiver(t *testing.T) {
|
||||
var info *ZFSPoolInfo
|
||||
pool := info.ConvertToModelZFSPool()
|
||||
|
||||
if pool != nil {
|
||||
t.Error("ConvertToModelZFSPool on nil should return nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_ConvertToModelZFSPool_StateFallback(t *testing.T) {
|
||||
// When State is empty, should fall back to Health
|
||||
info := &ZFSPoolInfo{
|
||||
Name: "tank",
|
||||
Health: "DEGRADED",
|
||||
State: "", // empty
|
||||
}
|
||||
|
||||
pool := info.ConvertToModelZFSPool()
|
||||
|
||||
if pool.State != "DEGRADED" {
|
||||
t.Errorf("State = %q, want DEGRADED (from Health)", pool.State)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_ConvertToModelZFSPool_NoDevices(t *testing.T) {
|
||||
info := &ZFSPoolInfo{
|
||||
Name: "tank",
|
||||
Health: "ONLINE",
|
||||
Devices: nil,
|
||||
}
|
||||
|
||||
pool := info.ConvertToModelZFSPool()
|
||||
|
||||
if pool.Devices == nil {
|
||||
t.Error("Devices should be initialized to empty slice, not nil")
|
||||
}
|
||||
if len(pool.Devices) != 0 {
|
||||
t.Errorf("Devices count = %d, want 0", len(pool.Devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_ConvertToModelZFSPool_AggregatesErrors(t *testing.T) {
|
||||
info := &ZFSPoolInfo{
|
||||
Name: "tank",
|
||||
Health: "DEGRADED",
|
||||
Devices: []ZFSPoolDevice{
|
||||
{Name: "sda", State: "DEGRADED", Leaf: 1, Read: 10, Write: 20, Cksum: 30},
|
||||
{Name: "sdb", State: "FAULTED", Leaf: 1, Read: 5, Write: 10, Cksum: 15},
|
||||
},
|
||||
}
|
||||
|
||||
pool := info.ConvertToModelZFSPool()
|
||||
|
||||
// Should aggregate errors from all devices
|
||||
if pool.ReadErrors != 15 {
|
||||
t.Errorf("ReadErrors = %d, want 15", pool.ReadErrors)
|
||||
}
|
||||
if pool.WriteErrors != 30 {
|
||||
t.Errorf("WriteErrors = %d, want 30", pool.WriteErrors)
|
||||
}
|
||||
if pool.ChecksumErrors != 45 {
|
||||
t.Errorf("ChecksumErrors = %d, want 45", pool.ChecksumErrors)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPool_Fields(t *testing.T) {
|
||||
pool := ZFSPool{
|
||||
Name: "tank",
|
||||
State: "ONLINE",
|
||||
Health: "ONLINE",
|
||||
Status: "healthy",
|
||||
Scan: "none requested",
|
||||
Errors: "No known data errors",
|
||||
ReadErrors: 10,
|
||||
WriteErrors: 20,
|
||||
ChecksumErrors: 30,
|
||||
Devices: []ZFSDevice{
|
||||
{Name: "sda", Type: "disk", State: "ONLINE", IsLeaf: true},
|
||||
},
|
||||
}
|
||||
|
||||
if pool.Name != "tank" {
|
||||
t.Errorf("Name = %q, want tank", pool.Name)
|
||||
}
|
||||
if pool.ReadErrors != 10 {
|
||||
t.Errorf("ReadErrors = %d, want 10", pool.ReadErrors)
|
||||
}
|
||||
if pool.WriteErrors != 20 {
|
||||
t.Errorf("WriteErrors = %d, want 20", pool.WriteErrors)
|
||||
}
|
||||
if pool.ChecksumErrors != 30 {
|
||||
t.Errorf("ChecksumErrors = %d, want 30", pool.ChecksumErrors)
|
||||
}
|
||||
if len(pool.Devices) != 1 {
|
||||
t.Errorf("Devices count = %d, want 1", len(pool.Devices))
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSDevice_Fields(t *testing.T) {
|
||||
dev := ZFSDevice{
|
||||
Name: "nvme0n1",
|
||||
Type: "disk",
|
||||
State: "ONLINE",
|
||||
ReadErrors: 1,
|
||||
WriteErrors: 2,
|
||||
ChecksumErrors: 3,
|
||||
IsLeaf: true,
|
||||
Message: "device is failing",
|
||||
}
|
||||
|
||||
if dev.Name != "nvme0n1" {
|
||||
t.Errorf("Name = %q, want nvme0n1", dev.Name)
|
||||
}
|
||||
if dev.Type != "disk" {
|
||||
t.Errorf("Type = %q, want disk", dev.Type)
|
||||
}
|
||||
if dev.State != "ONLINE" {
|
||||
t.Errorf("State = %q, want ONLINE", dev.State)
|
||||
}
|
||||
if !dev.IsLeaf {
|
||||
t.Error("IsLeaf should be true")
|
||||
}
|
||||
if dev.Message != "device is failing" {
|
||||
t.Errorf("Message = %q, want 'device is failing'", dev.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestZFSPoolInfo_Fields(t *testing.T) {
|
||||
info := ZFSPoolInfo{
|
||||
Name: "tank",
|
||||
Health: "ONLINE",
|
||||
Size: 1000000000000,
|
||||
Alloc: 500000000000,
|
||||
Free: 500000000000,
|
||||
Frag: 10,
|
||||
Dedup: 1.5,
|
||||
State: "ONLINE",
|
||||
Status: "All healthy",
|
||||
Scan: "scrub completed",
|
||||
Errors: "No errors",
|
||||
}
|
||||
|
||||
if info.Name != "tank" {
|
||||
t.Errorf("Name = %q, want tank", info.Name)
|
||||
}
|
||||
if info.Health != "ONLINE" {
|
||||
t.Errorf("Health = %q, want ONLINE", info.Health)
|
||||
}
|
||||
if info.Size != 1000000000000 {
|
||||
t.Errorf("Size = %d, want 1000000000000", info.Size)
|
||||
}
|
||||
if info.Alloc != 500000000000 {
|
||||
t.Errorf("Alloc = %d, want 500000000000", info.Alloc)
|
||||
}
|
||||
if info.Free != 500000000000 {
|
||||
t.Errorf("Free = %d, want 500000000000", info.Free)
|
||||
}
|
||||
if info.Frag != 10 {
|
||||
t.Errorf("Frag = %d, want 10", info.Frag)
|
||||
}
|
||||
if info.Dedup != 1.5 {
|
||||
t.Errorf("Dedup = %f, want 1.5", info.Dedup)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue