Files
Daniel Markstedt 4653db7313 ability to attach all devices in the Control Board UI
when you do Attach/Insert on an empty slot, you now get a list of all available device types instead of list of all images; picking one of them either attaches the device immediately, or takes you to a submenu for making additional choices (network topology, image file)

introduce image file filtering per the device type that is being attached, including filtered lists also for inserting media into existing removable devices

add an option to attach a removable device without inserting media
2026-08-31 07:16:21 +02:00

192 lines
6.7 KiB
Go

// Copyright 2026 Daniel Markstedt. All rights reserved.
// Use of this source code is governed by the BSD 3-Clause
// license that can be found in the LICENSE file.
package ctrlboard
import (
"context"
"testing"
pb "github.com/piscsi/piscsi/go/proto"
)
type workflowClient struct {
commands []*pb.PbCommand
results []*pb.PbResult
}
func (c *workflowClient) SendCommand(command *pb.PbCommand) (*pb.PbResult, error) {
c.commands = append(c.commands, command)
result := c.results[0]
c.results = c.results[1:]
return result, nil
}
func TestBuildImageMenuSortsImagesAndCarriesSlot(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{
{Status: true, Result: &pb.PbResult_ImageFilesInfo{ImageFilesInfo: &pb.PbImageFilesInfo{ImageFiles: []*pb.PbImageFile{
{Name: "zeta.hds", Type: pb.PbDeviceType_SCHD},
{Name: "alpha.hda", Type: pb.PbDeviceType_SCHD},
}}}},
}}
workflow := NewSCSIWorkflow(client, "secret")
diagnostics := make(chan ImageListDiagnostic, 1)
workflow.SetDiagnosticSink(func(response ImageListDiagnostic) { diagnostics <- response })
slot := SCSISlot{ID: 3}
menu, err := workflow.BuildImageMenu(context.Background(), slot, 4)
if err != nil {
t.Fatal(err)
}
if got, want := menu.Items[1].Label, "alpha.hda [SCHD]"; got != want {
t.Fatalf("first image = %q, want %q", got, want)
}
selection, ok := menu.Items[1].Data.(ImageSelection)
if !ok || selection.Slot.ID != 3 || selection.Image.GetName() != "alpha.hda" {
t.Fatalf("image selection = %#v", menu.Items[1].Data)
}
if _, found := client.commands[0].GetParams()["folder"]; found {
t.Fatalf("default image request unexpectedly has a folder parameter")
}
if diagnostic := <-diagnostics; !diagnostic.Status || diagnostic.Count != 2 {
t.Fatalf("image list diagnostic = %#v", diagnostic)
}
}
func TestBuildImageMenuIncludesEmptyState(t *testing.T) {
workflow := NewSCSIWorkflow(&workflowClient{results: []*pb.PbResult{{
Status: true, Result: &pb.PbResult_ImageFilesInfo{ImageFilesInfo: &pb.PbImageFilesInfo{}},
}}}, "")
menu, err := workflow.BuildImageMenu(context.Background(), SCSISlot{ID: 1}, 4)
if err != nil {
t.Fatal(err)
}
if got, want := menu.Items[1].Label, "(No image files found)"; got != want {
t.Fatalf("empty label = %q, want %q", got, want)
}
}
func TestBuildImageMenuForTypeFiltersDaemonMappedImages(t *testing.T) {
workflow := NewSCSIWorkflow(&workflowClient{results: []*pb.PbResult{{
Status: true, Result: &pb.PbResult_ImageFilesInfo{ImageFilesInfo: &pb.PbImageFilesInfo{ImageFiles: []*pb.PbImageFile{
{Name: "disk.hds", Type: pb.PbDeviceType_SCHD},
{Name: "media.mos", Type: pb.PbDeviceType_SCMO},
}}},
}}}, "")
menu, err := workflow.BuildImageMenuForType(context.Background(), SCSISlot{ID: 1}, pb.PbDeviceType_SCMO, 4)
if err != nil {
t.Fatal(err)
}
if got, want := len(menu.Items), 2; got != want {
t.Fatalf("image item count = %d, want %d", got, want)
}
if got, want := menu.Items[1].Label, "media.mos [SCMO]"; got != want {
t.Fatalf("filtered image = %q, want %q", got, want)
}
}
func TestAttachOrInsertUsesInsertForEmptyRemovableDevice(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{{Status: true}}}
device := &pb.PbDevice{
Id: 4,
Unit: 1,
Type: pb.PbDeviceType_SCCD,
Properties: &pb.PbDeviceProperties{Removable: true},
Status: &pb.PbDeviceStatus{Removed: true},
}
message, err := NewSCSIWorkflow(client, "").AttachOrInsert(context.Background(), ImageSelection{
Slot: SCSISlot{ID: 4, Device: device},
Image: &pb.PbImageFile{Name: "System.iso", Type: pb.PbDeviceType_SCCD},
})
if err != nil {
t.Fatal(err)
}
if got, want := message, "Inserted ID 4"; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
command := client.commands[0]
if got, want := command.GetOperation(), pb.PbOperation_INSERT; got != want {
t.Fatalf("operation = %s, want %s", got, want)
}
if got, want := command.GetDevices()[0].GetUnit(), int32(1); got != want {
t.Fatalf("LUN = %d, want %d", got, want)
}
}
func TestAttachDeviceUsesPrinterDefaultCommand(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{{Status: true}}}
message, err := NewSCSIWorkflow(client, "").AttachDevice(context.Background(), DeviceAttachSelection{
Slot: SCSISlot{ID: 3}, Type: pb.PbDeviceType_SCLP, Params: map[string]string{"cmd": "different"},
})
if err != nil {
t.Fatal(err)
}
if got, want := message, "Attached ID 3"; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
device := client.commands[0].GetDevices()[0]
if got, want := device.GetParams()["cmd"], defaultPrinterCommand; got != want {
t.Fatalf("printer command = %q, want %q", got, want)
}
}
func TestDetachOrEjectEjectsPresentRemovableMedia(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{{Status: true}}}
message, err := NewSCSIWorkflow(client, "").DetachOrEject(context.Background(), SCSISlot{
ID: 5,
Device: &pb.PbDevice{
Id: 5,
Unit: 0,
Properties: &pb.PbDeviceProperties{Removable: true},
Status: &pb.PbDeviceStatus{Removed: false},
File: &pb.PbImageFile{Name: "disk.mo"},
},
})
if err != nil {
t.Fatal(err)
}
if got, want := message, "Ejected ID 5"; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
if got, want := client.commands[0].GetOperation(), pb.PbOperation_EJECT; got != want {
t.Fatalf("operation = %s, want %s", got, want)
}
}
func TestReservePreservesExistingReservations(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{
{Status: true, Result: &pb.PbResult_ReservedIdsInfo{ReservedIdsInfo: &pb.PbReservedIdsInfo{Ids: []int32{1, 6}}}},
{Status: true},
}}
message, err := NewSCSIWorkflow(client, "").Reserve(context.Background(), SCSISlot{ID: 4})
if err != nil {
t.Fatal(err)
}
if got, want := message, "Reserved ID 4"; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
if got, want := client.commands[0].GetOperation(), pb.PbOperation_RESERVED_IDS_INFO; got != want {
t.Fatalf("lookup operation = %s, want %s", got, want)
}
if got, want := client.commands[1].GetParams()["ids"], "1,6,4"; got != want {
t.Fatalf("reserved IDs = %q, want %q", got, want)
}
}
func TestReleasePreservesOtherReservations(t *testing.T) {
client := &workflowClient{results: []*pb.PbResult{
{Status: true, Result: &pb.PbResult_ReservedIdsInfo{ReservedIdsInfo: &pb.PbReservedIdsInfo{Ids: []int32{1, 4, 6}}}},
{Status: true},
}}
message, err := NewSCSIWorkflow(client, "").Release(context.Background(), SCSISlot{ID: 4, Reserved: true})
if err != nil {
t.Fatal(err)
}
if got, want := message, "Released ID 4"; got != want {
t.Fatalf("message = %q, want %q", got, want)
}
if got, want := client.commands[1].GetParams()["ids"], "1,6"; got != want {
t.Fatalf("reserved IDs = %q, want %q", got, want)
}
}