Files
seaweedfs/weed/storage/blockvol/iscsi/qa_test.go
T
Ping QiuandClaude Opus 4.6 c39080ceaa feat: Phase 4A CP4b-4 -- HA integration tests, admin HTTP, 5 bug fixes
Add HTTP admin server to iscsi-target binary (POST /assign, GET /status,
POST /replica, POST /rebuild) and 7 HA integration tests validating
failover, split-brain prevention, epoch fencing, and demote-under-IO.

New files:
- admin.go: HTTP admin endpoint with input validation
- ha_target.go: HATarget helper wrapping Target + admin HTTP calls
- ha_test.go: 7 HA tests (all PASS on WSL2, 67.7s total)

Bug fixes:
- BUG-CP4B4-1: CmdSN init (expCmdSN=0 not 1, first SCSI cmd was dropped)
- BUG-CP4B4-2: RoleNone->RoleReplica missing SetEpoch (WAL rejected)
- BUG-CP4B4-3: replica applyEntry didn't update vol.nextLSN (status=0)
- BUG-CP4B4-4: PID discovery killed primary instead of replica (shared
  binPath; fixed by grepping volFile)
- BUG-CP4B4-5: artifact collector overwrote primary log with replica log
  (added CollectLabeled method)

Also: 3s write deadline on WAL shipper data connection to avoid 120s TCP
retransmission timeout when replica is dead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 15:04:59 -08:00

2202 lines
59 KiB
Go

package iscsi
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"strings"
"sync"
"testing"
"time"
)
// TestQA is the top-level QA adversarial test suite for Phase 2 iSCSI target.
// All tests use pure TCP (net.Pipe) and run on Windows with no kernel dependencies.
func TestQA(t *testing.T) {
t.Run("PDU", testQA_PDU)
t.Run("Params", testQA_Params)
t.Run("Login", testQA_Login)
t.Run("Discovery", testQA_Discovery)
t.Run("SCSI", testQA_SCSI)
t.Run("DataIO", testQA_DataIO)
t.Run("Session", testQA_Session)
t.Run("Target", testQA_Target)
t.Run("Integration", testQA_Integration)
}
// ---------------------------------------------------------------------------
// QA helpers
// ---------------------------------------------------------------------------
// safeMockDevice wraps mockBlockDevice with a mutex for concurrent access.
type safeMockDevice struct {
mu sync.Mutex
dev *mockBlockDevice
}
func newSafeMockDevice(size uint64) *safeMockDevice {
return &safeMockDevice{dev: newMockDevice(size)}
}
func (s *safeMockDevice) ReadAt(lba uint64, length uint32) ([]byte, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.dev.ReadAt(lba, length)
}
func (s *safeMockDevice) WriteAt(lba uint64, data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.dev.WriteAt(lba, data)
}
func (s *safeMockDevice) Trim(lba uint64, length uint32) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.dev.Trim(lba, length)
}
func (s *safeMockDevice) SyncCache() error {
s.mu.Lock()
defer s.mu.Unlock()
return s.dev.SyncCache()
}
func (s *safeMockDevice) BlockSize() uint32 { return s.dev.BlockSize() }
func (s *safeMockDevice) VolumeSize() uint64 { return s.dev.VolumeSize() }
func (s *safeMockDevice) IsHealthy() bool { return s.dev.IsHealthy() }
// qaSession sets up a session with a configurable mock device and returns
// the client conn + done channel. The session runs HandleConnection in a goroutine.
func qaSession(t *testing.T, dev BlockDevice) (net.Conn, chan error) {
t.Helper()
client, server := net.Pipe()
config := DefaultTargetConfig()
config.TargetName = testTargetName
resolver := newTestResolverWithDevice(dev)
logger := log.New(io.Discard, "", 0)
sess := NewSession(server, config, resolver, resolver, logger)
done := make(chan error, 1)
go func() {
done <- sess.HandleConnection()
}()
t.Cleanup(func() {
sess.Close()
client.Close()
})
return client, done
}
// qaSessionDefault creates a session with a standard 1024-block mock device.
func qaSessionDefault(t *testing.T) (net.Conn, chan error) {
t.Helper()
return qaSession(t, newMockDevice(1024*4096))
}
// qaLoginNormal performs a normal login on the given connection, returning CmdSN for next use.
func qaLoginNormal(t *testing.T, conn net.Conn) uint32 {
t.Helper()
doLogin(t, conn)
return 0 // Login CmdSN not used for ordering; first SCSI CmdSN is 0
}
// qaLoginDiscovery performs a discovery login.
func qaLoginDiscovery(t *testing.T, conn net.Conn) {
t.Helper()
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("SessionType", "Discovery")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("discovery login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
}
}
// qaSendSCSICmd sends a SCSI command and reads the response.
func qaSendSCSICmd(t *testing.T, conn net.Conn, cdb [16]byte, cmdSN uint32,
read, write bool, dataOut []byte, expLen uint32) *PDU {
t.Helper()
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
flags := uint8(FlagF)
if read {
flags |= FlagR
}
if write {
flags |= FlagW
}
cmd.SetOpSpecific1(flags)
cmd.SetInitiatorTaskTag(cmdSN)
cmd.SetExpectedDataTransferLength(expLen)
cmd.SetCmdSN(cmdSN)
cmd.SetCDB(cdb)
if dataOut != nil {
cmd.DataSegment = dataOut
}
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
return resp
}
// qaReadAllDataIn reads Data-In PDUs until the S-bit is set, assembling the full data.
func qaReadAllDataIn(t *testing.T, conn net.Conn, first *PDU) []byte {
t.Helper()
var data []byte
data = append(data, first.DataSegment...)
pdu := first
for pdu.OpSpecific1()&FlagS == 0 {
var err error
pdu, err = ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
data = append(data, pdu.DataSegment...)
}
return data
}
// qaExpectReject reads a PDU and asserts it's a Reject.
func qaExpectReject(t *testing.T, conn net.Conn) *PDU {
t.Helper()
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpReject {
t.Fatalf("expected Reject, got %s", OpcodeName(resp.Opcode()))
}
return resp
}
// qaExpectCheckCondition asserts that the response is a SCSI Response with CHECK CONDITION.
func qaExpectCheckCondition(t *testing.T, resp *PDU) {
t.Helper()
if resp.Opcode() != OpSCSIResp {
t.Fatalf("expected SCSI-Response, got %s", OpcodeName(resp.Opcode()))
}
if resp.SCSIStatus() != SCSIStatusCheckCond {
t.Fatalf("expected CHECK_CONDITION, got status %d", resp.SCSIStatus())
}
}
// qaSetupTarget creates a TargetServer with a thread-safe mock device on a random port.
func qaSetupTarget(t *testing.T) (*TargetServer, string) {
t.Helper()
dev := newSafeMockDevice(256 * 4096)
config := DefaultTargetConfig()
config.TargetName = testTargetName
logger := log.New(io.Discard, "", 0)
ts := NewTargetServer("127.0.0.1:0", config, logger)
ts.AddVolume(testTargetName, dev)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go ts.Serve(ln)
t.Cleanup(func() { ts.Close() })
return ts, ln.Addr().String()
}
// qaDialAndLogin dials the target, logs in, returns connection and next CmdSN.
func qaDialAndLogin(t *testing.T, addr string) (net.Conn, uint32) {
t.Helper()
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { conn.Close() })
doLogin(t, conn)
return conn, 2
}
// ---------------------------------------------------------------------------
// 1. PDU Wire Format (5 tests)
// ---------------------------------------------------------------------------
func testQA_PDU(t *testing.T) {
t.Run("dataseg_underflow", func(t *testing.T) {
// DataSegLen=100, deliver 50 bytes then EOF -> error
r, w := io.Pipe()
go func() {
p := &PDU{}
p.SetOpcode(OpNOPOut)
p.SetDataSegmentLength(100)
w.Write(p.BHS[:]) // write BHS claiming 100 bytes data
w.Write(make([]byte, 50))
w.Close() // EOF after 50 bytes
}()
_, err := ReadPDU(r)
if err == nil {
t.Fatal("expected error from truncated data segment")
}
})
t.Run("zero_opcode_session", func(t *testing.T) {
// NOP-Out (opcode 0x00) to a live session with ITT
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
nop := &PDU{}
nop.SetOpcode(OpNOPOut)
nop.SetInitiatorTaskTag(0x9999)
nop.SetCmdSN(0)
if err := WritePDU(conn, nop); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpNOPIn {
t.Fatalf("expected NOP-In, got %s", OpcodeName(resp.Opcode()))
}
if resp.InitiatorTaskTag() != 0x9999 {
t.Fatalf("ITT mismatch: got %d", resp.InitiatorTaskTag())
}
})
t.Run("rapid_fire_100", func(t *testing.T) {
// 100 PDUs written to a buffer, read all back
var buf bytes.Buffer
for i := 0; i < 100; i++ {
p := &PDU{}
p.SetOpcode(OpNOPOut)
p.SetInitiatorTaskTag(uint32(i))
if err := WritePDU(&buf, p); err != nil {
t.Fatal(err)
}
}
for i := 0; i < 100; i++ {
p, err := ReadPDU(&buf)
if err != nil {
t.Fatalf("PDU %d: %v", i, err)
}
if p.InitiatorTaskTag() != uint32(i) {
t.Fatalf("PDU %d: ITT=%d", i, p.InitiatorTaskTag())
}
}
})
t.Run("huge_ahs_len", func(t *testing.T) {
// TotalAHSLen=255 (1020 bytes AHS) with truncated data -> error
r, w := io.Pipe()
go func() {
var bhs [BHSLength]byte
bhs[0] = OpNOPOut
bhs[4] = 255 // 1020 bytes AHS
w.Write(bhs[:])
w.Write(make([]byte, 100)) // only 100 of 1020
w.Close()
}()
_, err := ReadPDU(r)
if err == nil {
t.Fatal("expected error from truncated AHS")
}
})
t.Run("dataseg_len_vs_actual", func(t *testing.T) {
// DataSegLen=8192 but only 4096 bytes + EOF -> error
r, w := io.Pipe()
go func() {
var bhs [BHSLength]byte
bhs[0] = OpNOPOut
// set data segment length to 8192
bhs[5] = 0
bhs[6] = 0x20
bhs[7] = 0x00
w.Write(bhs[:])
w.Write(make([]byte, 4096))
w.Close()
}()
_, err := ReadPDU(r)
if err == nil {
t.Fatal("expected error from short data segment")
}
})
}
// ---------------------------------------------------------------------------
// 2. Key-Value Params (3 tests)
// ---------------------------------------------------------------------------
func testQA_Params(t *testing.T) {
t.Run("very_long_value", func(t *testing.T) {
// 64KB value string -> roundtrip correct
p := NewParams()
longVal := strings.Repeat("A", 65536)
p.Set("BigKey", longVal)
encoded := p.Encode()
parsed, err := ParseParams(encoded)
if err != nil {
t.Fatal(err)
}
got, ok := parsed.Get("BigKey")
if !ok {
t.Fatal("key not found")
}
if got != longVal {
t.Fatalf("value length: got %d, want %d", len(got), len(longVal))
}
})
t.Run("no_null_terminator", func(t *testing.T) {
// "Key=Value" without trailing \0 -> parsed as single entry
raw := []byte("Key=Value")
parsed, err := ParseParams(raw)
if err != nil {
t.Fatal(err)
}
if parsed.Len() != 1 {
t.Fatalf("expected 1 param, got %d", parsed.Len())
}
v, _ := parsed.Get("Key")
if v != "Value" {
t.Fatalf("got %q", v)
}
})
t.Run("100_keys", func(t *testing.T) {
// 100 distinct key=value pairs -> all parsed
p := NewParams()
for i := 0; i < 100; i++ {
p.Set(fmt.Sprintf("Key%03d", i), fmt.Sprintf("Value%03d", i))
}
encoded := p.Encode()
parsed, err := ParseParams(encoded)
if err != nil {
t.Fatal(err)
}
if parsed.Len() != 100 {
t.Fatalf("expected 100 params, got %d", parsed.Len())
}
for i := 0; i < 100; i++ {
v, ok := parsed.Get(fmt.Sprintf("Key%03d", i))
if !ok || v != fmt.Sprintf("Value%03d", i) {
t.Fatalf("key %d: got %q", i, v)
}
}
})
}
// ---------------------------------------------------------------------------
// 3. Login State Machine (7 tests)
// ---------------------------------------------------------------------------
func testQA_Login(t *testing.T) {
t.Run("scsi_before_ffp", func(t *testing.T) {
// Send SCSI Command during login phase -> rejected
conn, _ := qaSessionDefault(t)
// Don't login -- send a SCSI command directly
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagR)
cmd.SetInitiatorTaskTag(1)
cmd.SetCmdSN(1)
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
cmd.SetCDB(cdb)
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
resp := qaExpectReject(t, conn)
_ = resp
})
t.Run("discovery_then_scsi", func(t *testing.T) {
// Discovery login, then SCSI cmd -> rejected (bug #12 fix)
conn, _ := qaSessionDefault(t)
qaLoginDiscovery(t, conn)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagR)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(96)
cmd.SetCmdSN(0)
var cdb [16]byte
cdb[0] = ScsiInquiry
binary.BigEndian.PutUint16(cdb[3:5], 96)
cmd.SetCDB(cdb)
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
resp := qaExpectReject(t, conn)
_ = resp
})
t.Run("double_login_after_ffp", func(t *testing.T) {
// Normal login, then another Login Request -> rejected
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(0)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
// The negotiator will see phase mismatch (already Done) and reject
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpLoginResp {
t.Fatalf("expected Login-Response, got %s", OpcodeName(resp.Opcode()))
}
if resp.LoginStatusClass() == LoginStatusSuccess {
t.Fatal("double login should not succeed")
}
})
t.Run("login_missing_initiator_name", func(t *testing.T) {
// No InitiatorName -> rejected
conn, _ := qaSessionDefault(t)
params := NewParams()
params.Set("TargetName", testTargetName)
// No InitiatorName
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() == LoginStatusSuccess {
t.Fatal("login without InitiatorName should fail")
}
})
t.Run("login_wrong_target", func(t *testing.T) {
// TargetName doesn't exist -> rejected
conn, _ := qaSessionDefault(t)
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", "iqn.2024.com.seaweedfs:nonexistent")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() == LoginStatusSuccess {
t.Fatal("login to nonexistent target should fail")
}
if resp.LoginStatusDetail() != LoginDetailNotFound {
t.Fatalf("expected NotFound detail, got %d", resp.LoginStatusDetail())
}
})
t.Run("login_loginop_direct_jump", func(t *testing.T) {
// CSG=LoginOp (skipping security phase) -> works (bug #9 fix)
conn, _ := qaSessionDefault(t)
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
params.Set("SessionType", "Normal")
req := makeLoginReq(StageLoginOp, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("direct LoginOp jump failed: %d/%d",
resp.LoginStatusClass(), resp.LoginStatusDetail())
}
})
t.Run("login_discovery_no_target", func(t *testing.T) {
// Discovery session without TargetName -> OK
conn, _ := qaSessionDefault(t)
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("SessionType", "Discovery")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("discovery login without target should succeed: %d/%d",
resp.LoginStatusClass(), resp.LoginStatusDetail())
}
})
}
// ---------------------------------------------------------------------------
// 4. Discovery (2 tests)
// ---------------------------------------------------------------------------
func testQA_Discovery(t *testing.T) {
t.Run("100_volumes", func(t *testing.T) {
// Register 100 volumes, SendTargets=All -> all 100 in response
var targets []DiscoveryTarget
for i := 0; i < 100; i++ {
targets = append(targets, DiscoveryTarget{
Name: fmt.Sprintf("iqn.2024.com.seaweedfs:vol%03d", i),
Address: fmt.Sprintf("10.0.0.%d:3260,1", i%256),
})
}
params := NewParams()
params.Set("SendTargets", "All")
req := makeTextReq(params)
resp := HandleTextRequest(req, targets)
if len(resp.DataSegment) == 0 {
t.Fatal("empty discovery response")
}
// Count TargetName occurrences
body := string(resp.DataSegment)
count := strings.Count(body, "TargetName=")
if count != 100 {
t.Fatalf("expected 100 targets, got %d", count)
}
})
t.Run("text_in_normal_session", func(t *testing.T) {
// Normal login, then Text Request -> should get a response (not crash)
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
params := NewParams()
params.Set("SendTargets", "All")
req := makeTextReq(params)
req.SetCmdSN(0)
if err := WritePDU(conn, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
// Should get TextResp (the resolver supports ListTargets)
if resp.Opcode() != OpTextResp {
t.Fatalf("expected TextResp, got %s", OpcodeName(resp.Opcode()))
}
})
}
// ---------------------------------------------------------------------------
// 5. SCSI Command Boundaries (8 tests)
// ---------------------------------------------------------------------------
func testQA_SCSI(t *testing.T) {
t.Run("read10_span_end", func(t *testing.T) {
// READ_10 LBA+transferLen > volume -> ILLEGAL_REQUEST
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiRead10
binary.BigEndian.PutUint32(cdb[2:6], 1020) // LBA 1020
binary.BigEndian.PutUint16(cdb[7:9], 10) // 10 blocks (exceeds 1024)
resp := qaSendSCSICmd(t, conn, cdb, cmdSN, true, false, nil, 10*4096)
qaExpectCheckCondition(t, resp)
})
t.Run("read16_huge_lba", func(t *testing.T) {
// READ_16 with huge LBA -> ILLEGAL_REQUEST
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiRead16
binary.BigEndian.PutUint64(cdb[2:10], 0x0000FFFFFFFFFFFF) // huge LBA
binary.BigEndian.PutUint32(cdb[10:14], 1)
resp := qaSendSCSICmd(t, conn, cdb, cmdSN, true, false, nil, 4096)
qaExpectCheckCondition(t, resp)
})
t.Run("write10_short_data", func(t *testing.T) {
// WRITE_10 CDB says 2 blocks, only 1 block data -> ILLEGAL_REQUEST
// Test at SCSI handler level since session-level write uses R2T collection.
dev := newMockDevice(1024 * 4096)
handler := NewSCSIHandler(dev)
var cdb [16]byte
cdb[0] = ScsiWrite10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 2) // 2 blocks = 8192 bytes
data := make([]byte, 4096) // only 1 block
result := handler.HandleCommand(cdb, data)
if result.Status != SCSIStatusCheckCond {
t.Fatalf("expected CHECK_CONDITION, got status %d", result.Status)
}
if result.SenseKey != SenseIllegalRequest {
t.Fatalf("expected ILLEGAL_REQUEST, got sense %d", result.SenseKey)
}
})
t.Run("unmap_overlapping", func(t *testing.T) {
// 2 UNMAP descriptors with overlapping ranges -> both processed without panic
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
// Write data first
writeData := bytes.Repeat([]byte{0xBB}, 4096*4)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 0)
binary.BigEndian.PutUint16(wCDB[7:9], 4)
resp := qaSendSCSICmd(t, conn, wCDB, cmdSN, false, true, writeData, 4*4096)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("write failed: %d", resp.SCSIStatus())
}
cmdSN++
// UNMAP with overlapping descriptors: [0,3) and [2,4)
unmapData := make([]byte, 40) // header 8 + 2 descriptors * 16
binary.BigEndian.PutUint16(unmapData[0:2], 38)
binary.BigEndian.PutUint16(unmapData[2:4], 32)
// Descriptor 1: LBA=0, count=3
binary.BigEndian.PutUint64(unmapData[8:16], 0)
binary.BigEndian.PutUint32(unmapData[16:20], 3)
// Descriptor 2: LBA=2, count=2
binary.BigEndian.PutUint64(unmapData[24:32], 2)
binary.BigEndian.PutUint32(unmapData[32:36], 2)
var uCDB [16]byte
uCDB[0] = ScsiUnmap
resp = qaSendSCSICmd(t, conn, uCDB, cmdSN, false, true, unmapData, uint32(len(unmapData)))
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("overlapping unmap failed: %d", resp.SCSIStatus())
}
})
t.Run("unmap_past_end", func(t *testing.T) {
// UNMAP LBA+count > volume -> error from device
dev := newMockDevice(1024 * 4096)
handler := NewSCSIHandler(dev)
unmapData := make([]byte, 24)
binary.BigEndian.PutUint16(unmapData[0:2], 22)
binary.BigEndian.PutUint16(unmapData[2:4], 16)
binary.BigEndian.PutUint64(unmapData[8:16], 1020) // LBA
binary.BigEndian.PutUint32(unmapData[16:20], 10) // 10 blocks, exceeds 1024
var cdb [16]byte
cdb[0] = ScsiUnmap
result := handler.HandleCommand(cdb, unmapData)
// Mock device Trim doesn't check bounds, so we verify it processes without panic
_ = result
})
t.Run("sync_cache_range", func(t *testing.T) {
// SYNC_CACHE_10 with specific LBA+count -> accepted
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiSyncCache10
binary.BigEndian.PutUint32(cdb[2:6], 100) // LBA
binary.BigEndian.PutUint16(cdb[7:9], 50) // count
resp := qaSendSCSICmd(t, conn, cdb, cmdSN, false, false, nil, 0)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("sync cache failed: %d", resp.SCSIStatus())
}
})
t.Run("mode_sense_all_pages", func(t *testing.T) {
// MODE_SENSE_6 page_code=0x3F (all pages) -> response
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiModeSense6
cdb[2] = 0x3F // all pages
cdb[4] = 255 // alloc len
resp := qaSendSCSICmd(t, conn, cdb, cmdSN, true, false, nil, 255)
if resp.Opcode() != OpSCSIDataIn {
t.Fatalf("expected Data-In, got %s", OpcodeName(resp.Opcode()))
}
if len(resp.DataSegment) < 4 {
t.Fatal("mode sense response too short")
}
})
t.Run("unmap_bad_alignment", func(t *testing.T) {
// UNMAP blockDescLen not multiple of 16 -> ILLEGAL_REQUEST (bug #16)
dev := newMockDevice(1024 * 4096)
handler := NewSCSIHandler(dev)
unmapData := make([]byte, 24)
binary.BigEndian.PutUint16(unmapData[0:2], 22)
binary.BigEndian.PutUint16(unmapData[2:4], 15) // not multiple of 16
var cdb [16]byte
cdb[0] = ScsiUnmap
result := handler.HandleCommand(cdb, unmapData)
if result.Status != SCSIStatusCheckCond {
t.Fatal("unaligned block desc should fail")
}
if result.SenseKey != SenseIllegalRequest {
t.Fatalf("expected ILLEGAL_REQUEST, got sense %d", result.SenseKey)
}
})
}
// ---------------------------------------------------------------------------
// 6. Data-In/Data-Out Sequencing (6 tests)
// ---------------------------------------------------------------------------
func testQA_DataIO(t *testing.T) {
t.Run("dataout_wrong_ttt", func(t *testing.T) {
// Data-Out with wrong TTT -> session rejects or errors
// We test DataOutCollector directly since TTT validation happens at session level.
// The session uses ITT as TTT. Send a Data-Out with wrong DataSN to collector.
collector := NewDataOutCollector(8192)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(99) // wrong, expected 0
pdu.SetBufferOffset(0)
pdu.DataSegment = make([]byte, 4096)
err := collector.AddDataOut(pdu)
if err == nil {
t.Fatal("expected error for wrong DataSN")
}
})
t.Run("dataout_wrong_offset", func(t *testing.T) {
// Data-Out with bad BufferOffset -> rejected (bug #14)
collector := NewDataOutCollector(8192)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(0)
pdu.SetBufferOffset(4096) // wrong, expected 0
pdu.DataSegment = make([]byte, 4096)
err := collector.AddDataOut(pdu)
if err == nil {
t.Fatal("expected error for wrong buffer offset")
}
if err != ErrDataOffsetOrder {
t.Fatalf("expected ErrDataOffsetOrder, got %v", err)
}
})
t.Run("dataout_exceed_burst", func(t *testing.T) {
// Data exceeding expected transfer length -> ErrDataOverflow
collector := NewDataOutCollector(4096)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(0)
pdu.SetBufferOffset(0)
pdu.DataSegment = make([]byte, 8192) // exceeds 4096
err := collector.AddDataOut(pdu)
if err == nil {
t.Fatal("expected error for overflow")
}
if err != ErrDataOverflow {
t.Fatalf("expected ErrDataOverflow, got %v", err)
}
})
t.Run("r2t_full_cycle_large", func(t *testing.T) {
// WRITE 128KB (>FirstBurstLength=65536) -> R2T cycle completes
dev := newMockDevice(256 * 4096)
conn, _ := qaSession(t, dev)
cmdSN := qaLoginNormal(t, conn)
writeSize := uint32(128 * 1024) // 128KB = 32 blocks
data := make([]byte, writeSize)
for i := range data {
data[i] = byte(i % 199)
}
var cdb [16]byte
cdb[0] = ScsiWrite10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 32)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(cmdSN)
cmd.SetExpectedDataTransferLength(writeSize)
cmd.SetCmdSN(cmdSN)
cmd.SetCDB(cdb)
// With default config: ImmediateData=true, InitialR2T=true
// No immediate data sent -- let R2T drive everything
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
// Collect R2T / send Data-Out cycle
var totalSent uint32
for totalSent < writeSize {
// Read R2T
r2t, err := ReadPDU(conn)
if err != nil {
t.Fatalf("reading R2T at offset %d: %v", totalSent, err)
}
if r2t.Opcode() != OpR2T {
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
}
desiredLen := r2t.DesiredDataLength()
offset := r2t.BufferOffset()
ttt := r2t.TargetTransferTag()
// Send Data-Out PDUs for this R2T
var dataSN uint32
sent := uint32(0)
for sent < desiredLen {
chunkSize := desiredLen - sent
if chunkSize > 65536 {
chunkSize = 65536
}
isFinal := (sent + chunkSize) >= desiredLen
doPDU := &PDU{}
doPDU.SetOpcode(OpSCSIDataOut)
if isFinal {
doPDU.SetOpSpecific1(FlagF)
}
doPDU.SetInitiatorTaskTag(cmdSN)
doPDU.SetTargetTransferTag(ttt)
doPDU.SetDataSN(dataSN)
doPDU.SetBufferOffset(offset + sent)
doPDU.DataSegment = data[offset+sent : offset+sent+chunkSize]
if err := WritePDU(conn, doPDU); err != nil {
t.Fatal(err)
}
dataSN++
sent += chunkSize
}
totalSent += desiredLen
}
// Read SCSI Response
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpSCSIResp {
t.Fatalf("expected SCSI-Response, got %s", OpcodeName(resp.Opcode()))
}
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("write failed: status %d", resp.SCSIStatus())
}
// Verify by reading back
cmdSN++
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 0)
binary.BigEndian.PutUint16(rCDB[7:9], 32)
readResp := qaSendSCSICmd(t, conn, rCDB, cmdSN, true, false, nil, writeSize)
readData := qaReadAllDataIn(t, conn, readResp)
if !bytes.Equal(readData, data) {
t.Fatal("data mismatch after R2T write cycle")
}
})
t.Run("datain_multi_pdu", func(t *testing.T) {
// READ larger than MaxRecvDataSegLen -> multiple Data-In PDUs
dev := newMockDevice(256 * 4096)
// Pre-populate some data
pattern := bytes.Repeat([]byte{0xAA}, 4096)
for i := uint64(0); i < 8; i++ {
dev.WriteAt(i, pattern)
}
client, server := net.Pipe()
config := DefaultTargetConfig()
config.TargetName = testTargetName
config.MaxRecvDataSegmentLength = 4096 // small: force multi-PDU
resolver := newTestResolverWithDevice(dev)
logger := log.New(io.Discard, "", 0)
sess := NewSession(server, config, resolver, resolver, logger)
done := make(chan error, 1)
go func() { done <- sess.HandleConnection() }()
t.Cleanup(func() { sess.Close(); client.Close() })
doLogin(t, client)
// Read 8 blocks = 32KB, with MaxRecvDataSegLen=4096 -> 8 Data-In PDUs
var cdb [16]byte
cdb[0] = ScsiRead10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 8)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagR)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(8 * 4096)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
if err := WritePDU(client, cmd); err != nil {
t.Fatal(err)
}
// Read Data-In PDUs
var allData []byte
pduCount := 0
for {
pdu, err := ReadPDU(client)
if err != nil {
t.Fatal(err)
}
allData = append(allData, pdu.DataSegment...)
pduCount++
if pdu.OpSpecific1()&FlagS != 0 {
break
}
}
if pduCount < 2 {
t.Fatalf("expected multiple Data-In PDUs, got %d", pduCount)
}
expected := bytes.Repeat(pattern, 8)
if !bytes.Equal(allData, expected) {
t.Fatal("multi-PDU data mismatch")
}
})
t.Run("immediate_data_when_disabled", func(t *testing.T) {
// Immediate data with ImmediateData=false -> rejected (bug #15)
dev := newMockDevice(256 * 4096)
client, server := net.Pipe()
config := DefaultTargetConfig()
config.TargetName = testTargetName
config.ImmediateData = false // disable immediate data
resolver := newTestResolverWithDevice(dev)
logger := log.New(io.Discard, "", 0)
sess := NewSession(server, config, resolver, resolver, logger)
done := make(chan error, 1)
go func() { done <- sess.HandleConnection() }()
t.Cleanup(func() { sess.Close(); client.Close() })
// Login with ImmediateData negotiation
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
params.Set("SessionType", "Normal")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(client, req); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(client)
if err != nil {
t.Fatal(err)
}
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("login failed: %d/%d", resp.LoginStatusClass(), resp.LoginStatusDetail())
}
// Send WRITE with immediate data despite ImmediateData=false
writeData := make([]byte, 4096)
var cdb [16]byte
cdb[0] = ScsiWrite10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 1)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(4096)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
cmd.DataSegment = writeData // immediate data when disabled
if err := WritePDU(client, cmd); err != nil {
t.Fatal(err)
}
// Should get CHECK_CONDITION response
resp, err = ReadPDU(client)
if err != nil {
t.Fatal(err)
}
if resp.SCSIStatus() == SCSIStatusGood {
t.Fatal("expected rejection when sending immediate data with ImmediateData=false")
}
_ = done
})
}
// ---------------------------------------------------------------------------
// 7. Session Sequence Numbers + Concurrency (10 tests)
// ---------------------------------------------------------------------------
func testQA_Session(t *testing.T) {
t.Run("cmdsn_out_of_window", func(t *testing.T) {
// CmdSN < ExpCmdSN -> silently dropped (bug #13)
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
// First consume CmdSN=0 (valid, advances ExpCmdSN to 1)
cmd0 := &PDU{}
cmd0.SetOpcode(OpSCSICmd)
cmd0.SetOpSpecific1(FlagF)
cmd0.SetInitiatorTaskTag(0x9999)
cmd0.SetCmdSN(0)
cmd0.SetCDB(cdb)
if err := WritePDU(conn, cmd0); err != nil {
t.Fatal(err)
}
resp0, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp0.InitiatorTaskTag() != 0x9999 {
t.Fatalf("expected response for ITT 0x9999, got 0x%x", resp0.InitiatorTaskTag())
}
// Now send stale CmdSN=0 (ExpCmdSN is now 1, so 0 is out of window)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF)
cmd.SetInitiatorTaskTag(0xAAAA)
cmd.SetCmdSN(0) // stale
cmd.SetCDB(cdb)
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
// Send a valid command right after to prove session is alive
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF)
cmd2.SetInitiatorTaskTag(0xBBBB)
cmd2.SetCmdSN(1)
cmd2.SetCDB(cdb)
if err := WritePDU(conn, cmd2); err != nil {
t.Fatal(err)
}
// Should get response only for the valid command (stale one dropped)
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.InitiatorTaskTag() != 0xBBBB {
t.Fatalf("expected response for ITT 0xBBBB, got 0x%x", resp.InitiatorTaskTag())
}
})
t.Run("cmdsn_ahead", func(t *testing.T) {
// CmdSN > MaxCmdSN -> dropped
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
// MaxCmdSN starts at 31. Send CmdSN=100 (way out of window)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF)
cmd.SetInitiatorTaskTag(0xCCCC)
cmd.SetCmdSN(100)
cmd.SetCDB(cdb)
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
// Valid command after
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF)
cmd2.SetInitiatorTaskTag(0xDDDD)
cmd2.SetCmdSN(0)
cmd2.SetCDB(cdb)
if err := WritePDU(conn, cmd2); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.InitiatorTaskTag() != 0xDDDD {
t.Fatalf("expected response for 0xDDDD, got 0x%x", resp.InitiatorTaskTag())
}
})
t.Run("cmdsn_wrap", func(t *testing.T) {
// Drive CmdSN through 0xFFFFFFFF -> 0 -> works
if !cmdSNInWindow(0xFFFFFFFF, 0xFFFFFFFF, 0x1F) {
t.Fatal("0xFFFFFFFF should be in window [0xFFFFFFFF, 0x1F]")
}
if !cmdSNInWindow(0x00000000, 0xFFFFFFFF, 0x1F) {
t.Fatal("0x00000000 should be in window [0xFFFFFFFF, 0x1F]")
}
if !cmdSNInWindow(0x00000010, 0xFFFFFFFF, 0x1F) {
t.Fatal("0x10 should be in window [0xFFFFFFFF, 0x1F]")
}
if cmdSNInWindow(0x00000020, 0xFFFFFFFF, 0x1F) {
t.Fatal("0x20 should NOT be in window [0xFFFFFFFF, 0x1F]")
}
if cmdSNInWindow(0xFFFFFFFE, 0xFFFFFFFF, 0x1F) {
t.Fatal("0xFFFFFFFE should NOT be in window [0xFFFFFFFF, 0x1F]")
}
})
t.Run("statsn_verify", func(t *testing.T) {
// 10 commands, verify StatSN increments by 1 each
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
var lastStatSN uint32
for i := uint32(0); i < 10; i++ {
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
cmdSN := i + 2
resp := qaSendSCSICmd(t, conn, cdb, cmdSN, false, false, nil, 0)
statSN := resp.StatSN()
if i > 0 && statSN != lastStatSN+1 {
t.Fatalf("cmd %d: StatSN=%d, expected %d", i, statSN, lastStatSN+1)
}
lastStatSN = statSN
}
})
t.Run("nop_unsolicited", func(t *testing.T) {
// NOP-Out with ITT=0xFFFFFFFF (target-initiated NOP reply) -> handled
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
nop := &PDU{}
nop.SetOpcode(OpNOPOut)
nop.SetOpSpecific1(FlagF)
nop.SetInitiatorTaskTag(0xFFFFFFFF)
nop.SetCmdSN(0)
if err := WritePDU(conn, nop); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpNOPIn {
t.Fatalf("expected NOP-In, got %s", OpcodeName(resp.Opcode()))
}
})
t.Run("logout_during_io", func(t *testing.T) {
// Logout mid-session -> clean shutdown
conn, _ := qaSessionDefault(t)
qaLoginNormal(t, conn)
logout := &PDU{}
logout.SetOpcode(OpLogoutReq)
logout.SetOpSpecific1(FlagF)
logout.SetInitiatorTaskTag(0x9876)
logout.SetCmdSN(0)
if err := WritePDU(conn, logout); err != nil {
t.Fatal(err)
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.Opcode() != OpLogoutResp {
t.Fatalf("expected LogoutResp, got %s", OpcodeName(resp.Opcode()))
}
})
t.Run("16_concurrent_sessions", func(t *testing.T) {
// 16 goroutines each with own session on TargetServer
ts, addr := qaSetupTarget(t)
_ = ts
var wg sync.WaitGroup
errs := make(chan error, 16)
for i := 0; i < 16; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
errs <- fmt.Errorf("session %d dial: %w", id, err)
return
}
defer conn.Close()
// Login
params := NewParams()
params.Set("InitiatorName", fmt.Sprintf("iqn.2024.com.test:client%d", id))
params.Set("TargetName", testTargetName)
params.Set("SessionType", "Normal")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
isid := [6]byte{0x00, 0x02, 0x3D, byte(id >> 8), byte(id), 0x01}
req.SetISID(isid)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
errs <- fmt.Errorf("session %d write login: %w", id, err)
return
}
resp, err := ReadPDU(conn)
if err != nil {
errs <- fmt.Errorf("session %d read login: %w", id, err)
return
}
if resp.LoginStatusClass() != LoginStatusSuccess {
errs <- fmt.Errorf("session %d login: %d/%d", id, resp.LoginStatusClass(), resp.LoginStatusDetail())
return
}
// Write to a unique LBA
lba := uint32(id)
data := bytes.Repeat([]byte{byte(id)}, 4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], lba)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(4096)
cmd.SetCmdSN(0)
cmd.SetCDB(wCDB)
cmd.DataSegment = data
if err := WritePDU(conn, cmd); err != nil {
errs <- fmt.Errorf("session %d write cmd: %w", id, err)
return
}
wResp, err := ReadPDU(conn)
if err != nil {
errs <- fmt.Errorf("session %d read write resp: %w", id, err)
return
}
if wResp.SCSIStatus() != SCSIStatusGood {
errs <- fmt.Errorf("session %d write failed: %d", id, wResp.SCSIStatus())
return
}
// Read back
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], lba)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF | FlagR)
cmd2.SetInitiatorTaskTag(3)
cmd2.SetExpectedDataTransferLength(4096)
cmd2.SetCmdSN(1)
cmd2.SetCDB(rCDB)
if err := WritePDU(conn, cmd2); err != nil {
errs <- fmt.Errorf("session %d write read cmd: %w", id, err)
return
}
rResp, err := ReadPDU(conn)
if err != nil {
errs <- fmt.Errorf("session %d read resp: %w", id, err)
return
}
if !bytes.Equal(rResp.DataSegment, data) {
errs <- fmt.Errorf("session %d data mismatch", id)
}
}(i)
}
wg.Wait()
close(errs)
for err := range errs {
t.Error(err)
}
})
t.Run("conn_drop_mid_read", func(t *testing.T) {
// Close TCP during Data-In transfer -> session cleans up
dev := newMockDevice(256 * 4096)
// Write some data
for i := uint64(0); i < 64; i++ {
dev.WriteAt(i, bytes.Repeat([]byte{0xBB}, 4096))
}
client, server := net.Pipe()
config := DefaultTargetConfig()
config.TargetName = testTargetName
config.MaxRecvDataSegmentLength = 4096 // small segments to ensure multi-PDU
resolver := newTestResolverWithDevice(dev)
logger := log.New(io.Discard, "", 0)
sess := NewSession(server, config, resolver, resolver, logger)
done := make(chan error, 1)
go func() { done <- sess.HandleConnection() }()
doLogin(t, client)
// Request a large read (64 blocks = 256KB, with 4KB segments = 64 PDUs)
var cdb [16]byte
cdb[0] = ScsiRead10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 64)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagR)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(64 * 4096)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
if err := WritePDU(client, cmd); err != nil {
t.Fatal(err)
}
// Read first PDU then close
_, err := ReadPDU(client)
if err != nil {
t.Fatal(err)
}
client.Close()
// Session should terminate cleanly
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("session did not terminate after client dropped")
}
})
t.Run("conn_drop_mid_write", func(t *testing.T) {
// Close TCP during R2T -> session cleans up
dev := newMockDevice(256 * 4096)
client, server := net.Pipe()
config := DefaultTargetConfig()
config.TargetName = testTargetName
resolver := newTestResolverWithDevice(dev)
logger := log.New(io.Discard, "", 0)
sess := NewSession(server, config, resolver, resolver, logger)
done := make(chan error, 1)
go func() { done <- sess.HandleConnection() }()
doLogin(t, client)
// Start large write, close connection after R2T
var cdb [16]byte
cdb[0] = ScsiWrite10
binary.BigEndian.PutUint32(cdb[2:6], 0)
binary.BigEndian.PutUint16(cdb[7:9], 64)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(64 * 4096)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
if err := WritePDU(client, cmd); err != nil {
t.Fatal(err)
}
// Read R2T, then close
_, err := ReadPDU(client)
if err != nil {
t.Fatal(err)
}
client.Close()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("session did not terminate after client dropped during write")
}
})
t.Run("rapid_login_logout_50", func(t *testing.T) {
// 50 login->logout cycles -> no leak
ts, addr := qaSetupTarget(t)
_ = ts
for i := 0; i < 50; i++ {
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatalf("cycle %d: dial failed: %v", i, err)
}
// Login
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
params.Set("SessionType", "Normal")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
if err := WritePDU(conn, req); err != nil {
conn.Close()
t.Fatalf("cycle %d: login write: %v", i, err)
}
resp, err := ReadPDU(conn)
if err != nil {
conn.Close()
t.Fatalf("cycle %d: login read: %v", i, err)
}
if resp.LoginStatusClass() != LoginStatusSuccess {
conn.Close()
t.Fatalf("cycle %d: login failed", i)
}
// Logout
logout := &PDU{}
logout.SetOpcode(OpLogoutReq)
logout.SetOpSpecific1(FlagF)
logout.SetInitiatorTaskTag(1)
logout.SetCmdSN(0)
if err := WritePDU(conn, logout); err != nil {
conn.Close()
t.Fatalf("cycle %d: logout write: %v", i, err)
}
resp, err = ReadPDU(conn)
if err != nil {
conn.Close()
t.Fatalf("cycle %d: logout read: %v", i, err)
}
if resp.Opcode() != OpLogoutResp {
conn.Close()
t.Fatalf("cycle %d: expected LogoutResp, got %s", i, OpcodeName(resp.Opcode()))
}
conn.Close()
}
})
}
// ---------------------------------------------------------------------------
// 8. Target Server (6 tests)
// ---------------------------------------------------------------------------
func testQA_Target(t *testing.T) {
t.Run("accept_flood_50", func(t *testing.T) {
// 50 TCP connections rapid-fire -> handled
_, addr := qaSetupTarget(t)
conns := make([]net.Conn, 50)
for i := 0; i < 50; i++ {
var err error
conns[i], err = net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatalf("connection %d: %v", i, err)
}
}
// Close all
for _, c := range conns {
c.Close()
}
})
t.Run("shutdown_during_io", func(t *testing.T) {
// Shutdown with active sessions -> clean
ts, addr := qaSetupTarget(t)
// Create 4 sessions, each doing login
conns := make([]net.Conn, 4)
for i := 0; i < 4; i++ {
var err error
conns[i], err = net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
params := NewParams()
params.Set("InitiatorName", fmt.Sprintf("iqn.2024.com.test:c%d", i))
params.Set("TargetName", testTargetName)
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
isid := [6]byte{0x00, 0x02, 0x3D, 0x00, byte(i), 0x01}
req.SetISID(isid)
req.SetCmdSN(1)
WritePDU(conns[i], req)
ReadPDU(conns[i])
}
// Shutdown should close all sessions
done := make(chan struct{})
go func() {
ts.Close()
close(done)
}()
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("shutdown timed out with active sessions")
}
for _, c := range conns {
c.Close()
}
})
t.Run("remove_volume_active", func(t *testing.T) {
// RemoveVolume during session -> next command sees error or degraded behavior
ts, addr := qaSetupTarget(t)
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
// Login
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
WritePDU(conn, req)
resp, _ := ReadPDU(conn)
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatal("login failed")
}
// First command should work
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF)
cmd.SetInitiatorTaskTag(2)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
WritePDU(conn, cmd)
resp, _ = ReadPDU(conn)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatal("first TUR should succeed")
}
// Remove the volume
ts.RemoveVolume(testTargetName)
// The session already has its SCSI handler bound at login time,
// so subsequent commands still work on the bound device
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF)
cmd2.SetInitiatorTaskTag(3)
cmd2.SetCmdSN(1)
cmd2.SetCDB(cdb)
WritePDU(conn, cmd2)
resp2, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
// Should still work since handler was bound at login
_ = resp2
})
t.Run("same_iqn_twice", func(t *testing.T) {
// AddVolume with existing IQN -> overwrites
config := DefaultTargetConfig()
logger := log.New(io.Discard, "", 0)
ts := NewTargetServer("127.0.0.1:0", config, logger)
dev1 := newMockDevice(1024 * 4096)
dev2 := newMockDevice(2048 * 4096)
ts.AddVolume("iqn.test:vol1", dev1)
ts.AddVolume("iqn.test:vol1", dev2) // overwrite
got := ts.LookupDevice("iqn.test:vol1")
if got.VolumeSize() != dev2.VolumeSize() {
t.Fatal("second AddVolume should overwrite first")
}
})
t.Run("shutdown_idempotent", func(t *testing.T) {
// Close() twice -> no panic
config := DefaultTargetConfig()
logger := log.New(io.Discard, "", 0)
ts := NewTargetServer("127.0.0.1:0", config, logger)
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
go ts.Serve(ln)
ts.Close()
ts.Close() // second close should be no-op
})
t.Run("port_in_use", func(t *testing.T) {
// ListenAndServe on bound port -> clean error
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
config := DefaultTargetConfig()
logger := log.New(io.Discard, "", 0)
ts := NewTargetServer(ln.Addr().String(), config, logger)
err = ts.ListenAndServe()
if err == nil {
ts.Close()
t.Fatal("expected error for port in use")
}
})
}
// ---------------------------------------------------------------------------
// 9. Full-Stack Integration (8 tests)
// ---------------------------------------------------------------------------
func testQA_Integration(t *testing.T) {
t.Run("crash_recovery", func(t *testing.T) {
// Write -> close -> reopen (using mock) -> read -> data persists
// Note: uses mockBlockDevice (data survives Close since it's in-memory)
dev := newMockDevice(256 * 4096)
conn, _ := qaSession(t, dev)
cmdSN := qaLoginNormal(t, conn)
// Write
pattern := bytes.Repeat([]byte{0xDE}, 4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 5)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
resp := qaSendSCSICmd(t, conn, wCDB, cmdSN, false, true, pattern, 4096)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatal("write failed")
}
// Close connection, create new session on same device
conn.Close()
conn2, _ := qaSession(t, dev)
cmdSN2 := qaLoginNormal(t, conn2)
// Read back
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 5)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
resp2 := qaSendSCSICmd(t, conn2, rCDB, cmdSN2, true, false, nil, 4096)
data := qaReadAllDataIn(t, conn2, resp2)
if !bytes.Equal(data, pattern) {
t.Fatal("data lost after reopen")
}
})
t.Run("session_reconnect", func(t *testing.T) {
// Login -> write -> close -> new login -> read
ts, addr := qaSetupTarget(t)
_ = ts
// First connection: login + write
conn1, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("TargetName", testTargetName)
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
WritePDU(conn1, req)
resp, _ := ReadPDU(conn1)
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatal("first login failed")
}
data := bytes.Repeat([]byte{0x42}, 4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 10)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(4096)
cmd.SetCmdSN(0)
cmd.SetCDB(wCDB)
cmd.DataSegment = data
WritePDU(conn1, cmd)
wResp, _ := ReadPDU(conn1)
if wResp.SCSIStatus() != SCSIStatusGood {
t.Fatal("write failed")
}
conn1.Close()
// Second connection: login + read
conn2, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
defer conn2.Close()
req2 := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req2.SetCmdSN(1)
WritePDU(conn2, req2)
resp2, _ := ReadPDU(conn2)
if resp2.LoginStatusClass() != LoginStatusSuccess {
t.Fatal("second login failed")
}
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 10)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF | FlagR)
cmd2.SetInitiatorTaskTag(2)
cmd2.SetExpectedDataTransferLength(4096)
cmd2.SetCmdSN(0)
cmd2.SetCDB(rCDB)
WritePDU(conn2, cmd2)
rResp, _ := ReadPDU(conn2)
readData := qaReadAllDataIn(t, conn2, rResp)
if !bytes.Equal(readData, data) {
t.Fatal("data lost after reconnect")
}
})
t.Run("large_io_multi_pdu", func(t *testing.T) {
// Write 256KB -> read back -> matches
dev := newMockDevice(256 * 4096) // 256 blocks = 1MB
conn, _ := qaSession(t, dev)
cmdSN := qaLoginNormal(t, conn)
writeSize := uint32(256 * 1024)
blockCount := uint16(writeSize / 4096)
data := make([]byte, writeSize)
for i := range data {
data[i] = byte(i % 251)
}
// Write
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 0)
binary.BigEndian.PutUint16(wCDB[7:9], blockCount)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(cmdSN)
cmd.SetExpectedDataTransferLength(writeSize)
cmd.SetCmdSN(cmdSN)
cmd.SetCDB(wCDB)
// Send without immediate data -> R2T cycle
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
// Drive R2T cycle
var totalSent uint32
for totalSent < writeSize {
r2t, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if r2t.Opcode() != OpR2T {
t.Fatalf("expected R2T, got %s", OpcodeName(r2t.Opcode()))
}
desiredLen := r2t.DesiredDataLength()
offset := r2t.BufferOffset()
ttt := r2t.TargetTransferTag()
doPDU := &PDU{}
doPDU.SetOpcode(OpSCSIDataOut)
doPDU.SetOpSpecific1(FlagF)
doPDU.SetInitiatorTaskTag(cmdSN)
doPDU.SetTargetTransferTag(ttt)
doPDU.SetDataSN(0)
doPDU.SetBufferOffset(offset)
doPDU.DataSegment = data[offset : offset+desiredLen]
if err := WritePDU(conn, doPDU); err != nil {
t.Fatal(err)
}
totalSent += desiredLen
}
resp, err := ReadPDU(conn)
if err != nil {
t.Fatal(err)
}
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("write failed: %d", resp.SCSIStatus())
}
cmdSN++
// Read back
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 0)
binary.BigEndian.PutUint16(rCDB[7:9], blockCount)
readResp := qaSendSCSICmd(t, conn, rCDB, cmdSN, true, false, nil, writeSize)
readData := qaReadAllDataIn(t, conn, readResp)
if !bytes.Equal(readData, data) {
t.Fatal("large I/O data mismatch")
}
})
t.Run("trim_then_read", func(t *testing.T) {
// UNMAP -> READ -> zeros
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
// Write data first
pattern := bytes.Repeat([]byte{0xFF}, 4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 20)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
resp := qaSendSCSICmd(t, conn, wCDB, cmdSN, false, true, pattern, 4096)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatal("write failed")
}
cmdSN++
// UNMAP
unmapData := make([]byte, 24)
binary.BigEndian.PutUint16(unmapData[0:2], 22)
binary.BigEndian.PutUint16(unmapData[2:4], 16)
binary.BigEndian.PutUint64(unmapData[8:16], 20)
binary.BigEndian.PutUint32(unmapData[16:20], 1)
var uCDB [16]byte
uCDB[0] = ScsiUnmap
resp = qaSendSCSICmd(t, conn, uCDB, cmdSN, false, true, unmapData, uint32(len(unmapData)))
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("unmap failed: %d", resp.SCSIStatus())
}
cmdSN++
// Read back -> should be zeros
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], 20)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
rResp := qaSendSCSICmd(t, conn, rCDB, cmdSN, true, false, nil, 4096)
data := qaReadAllDataIn(t, conn, rResp)
zeros := make([]byte, 4096)
if !bytes.Equal(data, zeros) {
t.Fatal("trimmed block should return zeros")
}
})
t.Run("8_concurrent_writers", func(t *testing.T) {
// 8 goroutines write different LBAs -> all correct
ts, addr := qaSetupTarget(t)
_ = ts
var wg sync.WaitGroup
errs := make(chan error, 8)
for i := 0; i < 8; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
errs <- fmt.Errorf("writer %d: %v", id, err)
return
}
defer conn.Close()
// Login
params := NewParams()
params.Set("InitiatorName", fmt.Sprintf("iqn.2024.com.test:w%d", id))
params.Set("TargetName", testTargetName)
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
isid := [6]byte{0x00, 0x02, 0x3D, 0x00, byte(id), 0x02}
req.SetISID(isid)
req.SetCmdSN(1)
WritePDU(conn, req)
resp, _ := ReadPDU(conn)
if resp.LoginStatusClass() != LoginStatusSuccess {
errs <- fmt.Errorf("writer %d: login failed", id)
return
}
// Write 4 blocks at offset id*4
lba := uint32(id * 4)
data := bytes.Repeat([]byte{byte(id + 0xA0)}, 4*4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], lba)
binary.BigEndian.PutUint16(wCDB[7:9], 4)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(4 * 4096)
cmd.SetCmdSN(0)
cmd.SetCDB(wCDB)
cmd.DataSegment = data
WritePDU(conn, cmd)
wResp, _ := ReadPDU(conn)
if wResp.SCSIStatus() != SCSIStatusGood {
errs <- fmt.Errorf("writer %d: write failed", id)
return
}
// Read back
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], lba)
binary.BigEndian.PutUint16(rCDB[7:9], 4)
cmd2 := &PDU{}
cmd2.SetOpcode(OpSCSICmd)
cmd2.SetOpSpecific1(FlagF | FlagR)
cmd2.SetInitiatorTaskTag(3)
cmd2.SetExpectedDataTransferLength(4 * 4096)
cmd2.SetCmdSN(1)
cmd2.SetCDB(rCDB)
WritePDU(conn, cmd2)
rResp, rErr := ReadPDU(conn)
if rErr != nil {
errs <- fmt.Errorf("writer %d: read error: %v", id, rErr)
return
}
// Collect all Data-In
var readData []byte
readData = append(readData, rResp.DataSegment...)
for rResp.OpSpecific1()&FlagS == 0 {
rResp, rErr = ReadPDU(conn)
if rErr != nil {
errs <- fmt.Errorf("writer %d: read continuation: %v", id, rErr)
return
}
readData = append(readData, rResp.DataSegment...)
}
if !bytes.Equal(readData, data) {
errs <- fmt.Errorf("writer %d: data mismatch", id)
}
}(i)
}
wg.Wait()
close(errs)
for err := range errs {
t.Error(err)
}
})
t.Run("discovery_then_normal", func(t *testing.T) {
// Discovery login -> get targets -> close -> normal login -> I/O
ts, addr := qaSetupTarget(t)
_ = ts
// Discovery session
conn1, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
params := NewParams()
params.Set("InitiatorName", testInitiatorName)
params.Set("SessionType", "Discovery")
req := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params)
req.SetCmdSN(1)
WritePDU(conn1, req)
resp, _ := ReadPDU(conn1)
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatal("discovery login failed")
}
// Send targets query
textParams := NewParams()
textParams.Set("SendTargets", "All")
textReq := makeTextReq(textParams)
textReq.SetCmdSN(0)
WritePDU(conn1, textReq)
textResp, _ := ReadPDU(conn1)
if textResp.Opcode() != OpTextResp {
t.Fatal("expected TextResp")
}
if len(textResp.DataSegment) == 0 {
t.Fatal("empty discovery response")
}
conn1.Close()
// Normal session
conn2, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err != nil {
t.Fatal(err)
}
defer conn2.Close()
params2 := NewParams()
params2.Set("InitiatorName", testInitiatorName)
params2.Set("TargetName", testTargetName)
req2 := makeLoginReq(StageSecurityNeg, StageFullFeature, true, params2)
req2.SetCmdSN(1)
WritePDU(conn2, req2)
resp2, _ := ReadPDU(conn2)
if resp2.LoginStatusClass() != LoginStatusSuccess {
t.Fatal("normal login failed")
}
// I/O: write + read
data := bytes.Repeat([]byte{0x99}, 4096)
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], 0)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF | FlagW)
cmd.SetInitiatorTaskTag(2)
cmd.SetExpectedDataTransferLength(4096)
cmd.SetCmdSN(0)
cmd.SetCDB(wCDB)
cmd.DataSegment = data
WritePDU(conn2, cmd)
wResp, _ := ReadPDU(conn2)
if wResp.SCSIStatus() != SCSIStatusGood {
t.Fatal("write failed")
}
})
t.Run("write_read_pattern", func(t *testing.T) {
// Write 0xAA to 50 LBAs -> read all -> byte-compare
conn, _ := qaSessionDefault(t)
cmdSN := qaLoginNormal(t, conn)
pattern := bytes.Repeat([]byte{0xAA}, 4096)
for lba := uint32(0); lba < 50; lba++ {
var wCDB [16]byte
wCDB[0] = ScsiWrite10
binary.BigEndian.PutUint32(wCDB[2:6], lba)
binary.BigEndian.PutUint16(wCDB[7:9], 1)
resp := qaSendSCSICmd(t, conn, wCDB, cmdSN, false, true, pattern, 4096)
if resp.SCSIStatus() != SCSIStatusGood {
t.Fatalf("write LBA %d failed", lba)
}
cmdSN++
}
for lba := uint32(0); lba < 50; lba++ {
var rCDB [16]byte
rCDB[0] = ScsiRead10
binary.BigEndian.PutUint32(rCDB[2:6], lba)
binary.BigEndian.PutUint16(rCDB[7:9], 1)
resp := qaSendSCSICmd(t, conn, rCDB, cmdSN, true, false, nil, 4096)
data := qaReadAllDataIn(t, conn, resp)
if !bytes.Equal(data, pattern) {
t.Fatalf("LBA %d: data mismatch", lba)
}
cmdSN++
}
})
t.Run("nil_scsi_handler_reject", func(t *testing.T) {
// Discovery session + SCSI cmd -> rejected (bug #12)
conn, _ := qaSessionDefault(t)
qaLoginDiscovery(t, conn)
var cdb [16]byte
cdb[0] = ScsiTestUnitReady
cmd := &PDU{}
cmd.SetOpcode(OpSCSICmd)
cmd.SetOpSpecific1(FlagF)
cmd.SetInitiatorTaskTag(2)
cmd.SetCmdSN(0)
cmd.SetCDB(cdb)
if err := WritePDU(conn, cmd); err != nil {
t.Fatal(err)
}
resp := qaExpectReject(t, conn)
_ = resp
})
}