Files
seaweedfs/weed/storage/blockvol/iscsi/dataio_test.go
T
Ping QiuandClaude Opus 4.6 7940e6b7c9 feat: Phase 4A CP4b-4 Windows iSCSI + instrumentation + QA tests
Windows iSCSI Initiator compatibility:
- Add TargetPortalGroupTag to login response (RFC 7143 S13.9)
- Add REQUEST_SENSE, START_STOP_UNIT, MODE_SELECT(6/10) handlers
- Add PERSISTENT_RESERVE_IN/OUT, MAINTENANCE_IN (REPORT SUPPORTED OPCODES)
- Implement MODE SENSE caching (page 0x08) and control (page 0x0A) pages
- Fix Data-In residual underflow/overflow flags (U/O bits on final PDU)
- Rename ScsiReadCapacity16 -> ScsiServiceActionIn16 for correctness

Instrumentation and tooling:
- Add instrumentedAdapter with periodic PERF stats logging
- Add pprof endpoints on admin HTTP server (/debug/pprof/*)
- Add blockbench CLI tool for standalone block device benchmarking
- Add SCSI CDB debug logging in session dispatch

HA integration fixes:
- Move HA test replica ports to 9011-9014 to avoid conflicts
- Add QA adversarial tests for Phase 4A CP4b-4 (755 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 21:17:57 -08:00

408 lines
9.2 KiB
Go

package iscsi
import (
"bytes"
"testing"
)
func TestDataIO(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{"datain_single_pdu", testDataInSinglePDU},
{"datain_multi_pdu", testDataInMultiPDU},
{"datain_exact_boundary", testDataInExactBoundary},
{"datain_zero_length", testDataInZeroLength},
{"datain_datasn_ordering", testDataInDataSNOrdering},
{"datain_fbit_sbit", testDataInFbitSbit},
{"dataout_single_pdu", testDataOutSinglePDU},
{"dataout_multi_pdu", testDataOutMultiPDU},
{"dataout_immediate_data", testDataOutImmediateData},
{"dataout_immediate_plus_r2t", testDataOutImmediatePlusR2T},
{"dataout_wrong_datasn", testDataOutWrongDataSN},
{"dataout_overflow", testDataOutOverflow},
{"r2t_build", testR2TBuild},
{"scsi_response_good", testSCSIResponseGood},
{"scsi_response_check_condition", testSCSIResponseCheckCondition},
{"datain_statsn_increment", testDataInStatSNIncrement},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.run(t)
})
}
}
func testDataInSinglePDU(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(8192)
data := bytes.Repeat([]byte{0xAA}, 4096)
statSN := uint32(1)
n, err := dw.WriteDataIn(w, data, 0x100, uint32(len(data)), 1, 10, &statSN)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("expected 1 PDU, got %d", n)
}
if statSN != 2 {
t.Fatalf("StatSN should be 2, got %d", statSN)
}
pdu, err := ReadPDU(w)
if err != nil {
t.Fatal(err)
}
if pdu.Opcode() != OpSCSIDataIn {
t.Fatal("wrong opcode")
}
if pdu.OpSpecific1()&FlagF == 0 {
t.Fatal("F-bit not set")
}
if pdu.OpSpecific1()&FlagS == 0 {
t.Fatal("S-bit not set on final PDU")
}
if len(pdu.DataSegment) != 4096 {
t.Fatalf("data length: %d", len(pdu.DataSegment))
}
}
func testDataInMultiPDU(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(1024) // small segment
data := bytes.Repeat([]byte{0xBB}, 3000)
statSN := uint32(1)
n, err := dw.WriteDataIn(w, data, 0x200, uint32(len(data)), 1, 10, &statSN)
if err != nil {
t.Fatal(err)
}
// 3000 / 1024 = 3 PDUs (1024 + 1024 + 952)
if n != 3 {
t.Fatalf("expected 3 PDUs, got %d", n)
}
// Read them back
var reassembled []byte
for i := 0; i < 3; i++ {
pdu, err := ReadPDU(w)
if err != nil {
t.Fatalf("PDU %d: %v", i, err)
}
if pdu.DataSN() != uint32(i) {
t.Fatalf("PDU %d: DataSN=%d", i, pdu.DataSN())
}
reassembled = append(reassembled, pdu.DataSegment...)
if i < 2 {
if pdu.OpSpecific1()&FlagF != 0 {
t.Fatalf("PDU %d should not have F-bit", i)
}
} else {
if pdu.OpSpecific1()&FlagF == 0 {
t.Fatal("last PDU should have F-bit")
}
if pdu.OpSpecific1()&FlagS == 0 {
t.Fatal("last PDU should have S-bit")
}
}
}
if !bytes.Equal(reassembled, data) {
t.Fatal("reassembled data mismatch")
}
}
func testDataInExactBoundary(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(1024)
data := bytes.Repeat([]byte{0xCC}, 2048) // exact 2 PDUs
statSN := uint32(1)
n, err := dw.WriteDataIn(w, data, 0x300, uint32(len(data)), 1, 10, &statSN)
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Fatalf("expected 2 PDUs, got %d", n)
}
}
func testDataInZeroLength(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(8192)
statSN := uint32(5)
n, err := dw.WriteDataIn(w, nil, 0x400, 0, 1, 10, &statSN)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("expected 1 PDU for zero-length, got %d", n)
}
if statSN != 6 {
t.Fatal("StatSN should still increment")
}
}
func testDataInDataSNOrdering(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(512)
data := bytes.Repeat([]byte{0xDD}, 2048) // 4 PDUs
statSN := uint32(1)
dw.WriteDataIn(w, data, 0x500, uint32(len(data)), 1, 10, &statSN)
for i := 0; i < 4; i++ {
pdu, _ := ReadPDU(w)
if pdu.DataSN() != uint32(i) {
t.Fatalf("PDU %d: DataSN=%d", i, pdu.DataSN())
}
expectedOffset := uint32(i) * 512
if pdu.BufferOffset() != expectedOffset {
t.Fatalf("PDU %d: offset=%d, expected %d", i, pdu.BufferOffset(), expectedOffset)
}
}
}
func testDataInFbitSbit(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(1000)
data := bytes.Repeat([]byte{0xEE}, 2500)
statSN := uint32(1)
dw.WriteDataIn(w, data, 0x600, uint32(len(data)), 1, 10, &statSN)
for i := 0; i < 3; i++ {
pdu, _ := ReadPDU(w)
flags := pdu.OpSpecific1()
if i < 2 {
if flags&FlagF != 0 || flags&FlagS != 0 {
t.Fatalf("PDU %d: should have no F/S bits", i)
}
} else {
if flags&FlagF == 0 || flags&FlagS == 0 {
t.Fatal("last PDU must have F+S bits")
}
}
}
}
func testDataOutSinglePDU(t *testing.T) {
c := NewDataOutCollector(4096)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetOpSpecific1(FlagF)
pdu.SetDataSN(0)
pdu.SetBufferOffset(0)
pdu.DataSegment = bytes.Repeat([]byte{0x11}, 4096)
if err := c.AddDataOut(pdu); err != nil {
t.Fatal(err)
}
if !c.Done() {
t.Fatal("should be done")
}
if c.Remaining() != 0 {
t.Fatal("remaining should be 0")
}
}
func testDataOutMultiPDU(t *testing.T) {
c := NewDataOutCollector(8192)
for i := 0; i < 2; i++ {
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(uint32(i))
pdu.SetBufferOffset(uint32(i) * 4096)
pdu.DataSegment = bytes.Repeat([]byte{byte(i + 1)}, 4096)
if i == 1 {
pdu.SetOpSpecific1(FlagF)
}
if err := c.AddDataOut(pdu); err != nil {
t.Fatalf("PDU %d: %v", i, err)
}
}
if !c.Done() {
t.Fatal("should be done")
}
data := c.Data()
if data[0] != 0x01 || data[4096] != 0x02 {
t.Fatal("data assembly wrong")
}
}
func testDataOutImmediateData(t *testing.T) {
c := NewDataOutCollector(4096)
err := c.AddImmediateData(bytes.Repeat([]byte{0xFF}, 4096))
if err != nil {
t.Fatal(err)
}
if !c.Done() {
t.Fatal("should be done with immediate data")
}
}
func testDataOutImmediatePlusR2T(t *testing.T) {
c := NewDataOutCollector(8192)
// Immediate: first 4096
err := c.AddImmediateData(bytes.Repeat([]byte{0xAA}, 4096))
if err != nil {
t.Fatal(err)
}
if c.Done() {
t.Fatal("should not be done yet")
}
if c.Remaining() != 4096 {
t.Fatalf("remaining: %d", c.Remaining())
}
// R2T-solicited Data-Out: next 4096
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetOpSpecific1(FlagF)
pdu.SetDataSN(0)
pdu.SetBufferOffset(4096)
pdu.DataSegment = bytes.Repeat([]byte{0xBB}, 4096)
if err := c.AddDataOut(pdu); err != nil {
t.Fatal(err)
}
if !c.Done() {
t.Fatal("should be done")
}
data := c.Data()
if data[0] != 0xAA || data[4096] != 0xBB {
t.Fatal("assembly wrong")
}
}
func testDataOutWrongDataSN(t *testing.T) {
c := NewDataOutCollector(8192)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(1) // should be 0
pdu.SetBufferOffset(0)
pdu.DataSegment = make([]byte, 4096)
err := c.AddDataOut(pdu)
if err != ErrDataSNOrder {
t.Fatalf("expected ErrDataSNOrder, got %v", err)
}
}
func testDataOutOverflow(t *testing.T) {
c := NewDataOutCollector(4096)
pdu := &PDU{}
pdu.SetOpcode(OpSCSIDataOut)
pdu.SetDataSN(0)
pdu.SetBufferOffset(0)
pdu.DataSegment = make([]byte, 8192) // more than expected
err := c.AddDataOut(pdu)
if err != ErrDataOverflow {
t.Fatalf("expected ErrDataOverflow, got %v", err)
}
}
func testR2TBuild(t *testing.T) {
pdu := BuildR2T(0x100, 0x200, 0, 4096, 4096, 10, 20)
if pdu.Opcode() != OpR2T {
t.Fatal("wrong opcode")
}
if pdu.InitiatorTaskTag() != 0x100 {
t.Fatal("ITT wrong")
}
if pdu.TargetTransferTag() != 0x200 {
t.Fatal("TTT wrong")
}
if pdu.R2TSN() != 0 {
t.Fatal("R2TSN wrong")
}
if pdu.BufferOffset() != 4096 {
t.Fatal("offset wrong")
}
if pdu.DesiredDataLength() != 4096 {
t.Fatal("desired length wrong")
}
}
func testSCSIResponseGood(t *testing.T) {
w := &bytes.Buffer{}
statSN := uint32(10)
result := SCSIResult{Status: SCSIStatusGood}
err := SendSCSIResponse(w, result, 0x300, &statSN, 5, 15)
if err != nil {
t.Fatal(err)
}
if statSN != 11 {
t.Fatal("StatSN not incremented")
}
pdu, err := ReadPDU(w)
if err != nil {
t.Fatal(err)
}
if pdu.Opcode() != OpSCSIResp {
t.Fatal("wrong opcode")
}
if pdu.SCSIStatus() != SCSIStatusGood {
t.Fatal("status wrong")
}
if len(pdu.DataSegment) != 0 {
t.Fatal("no data expected for good status")
}
}
func testSCSIResponseCheckCondition(t *testing.T) {
w := &bytes.Buffer{}
statSN := uint32(20)
result := SCSIResult{
Status: SCSIStatusCheckCond,
SenseKey: SenseIllegalRequest,
SenseASC: ASCInvalidOpcode,
SenseASCQ: ASCQLuk,
}
err := SendSCSIResponse(w, result, 0x400, &statSN, 5, 15)
if err != nil {
t.Fatal(err)
}
pdu, err := ReadPDU(w)
if err != nil {
t.Fatal(err)
}
if pdu.SCSIStatus() != SCSIStatusCheckCond {
t.Fatal("status wrong")
}
// Data segment should contain sense data with 2-byte length prefix
if len(pdu.DataSegment) < 20 { // 2 + 18
t.Fatalf("sense data too short: %d", len(pdu.DataSegment))
}
senseLen := int(pdu.DataSegment[0])<<8 | int(pdu.DataSegment[1])
if senseLen != 18 {
t.Fatalf("sense length: %d", senseLen)
}
}
func testDataInStatSNIncrement(t *testing.T) {
w := &bytes.Buffer{}
dw := NewDataInWriter(1024)
data := bytes.Repeat([]byte{0x00}, 3072) // 3 PDUs
statSN := uint32(100)
dw.WriteDataIn(w, data, 0x700, uint32(len(data)), 1, 10, &statSN)
// Only the final PDU has S-bit, so StatSN increments once
if statSN != 101 {
t.Fatalf("StatSN should be 101, got %d", statSN)
}
}