Files
seaweedfs/weed/storage/blockvol/iscsi/scsi_durability_test.go
T
Ping QiuandClaude Opus 4.6 da1b81d1c9 feat: CP8-3-1 durability modes + testrunner platform + 21 adversarial tests
Durability mode implementation (sync_all, sync_quorum, best_effort):
- DurabilityMode type with superblock persistence, parse/validate/string
- MakeDistributedSync mode-aware barrier enforcement in dist_group_commit
- blockerr sentinel package (ErrDurabilityBarrierFailed, ErrDurabilityQuorumLost)
- gRPC create path: mode validation, idempotent create consistency, partial cleanup
- F1: strict mode rejects partial replica provisioning with cleanup
- F3: empty heartbeat does not overwrite persisted strict mode
- F4: SCSI error mapping uses errors.Is sentinels (not string matching)
- Proto/wire/blockapi/CLI/UI plumbing for durability_mode field
- Observability dashboard: cluster health cards + per-volume columns

Testrunner platform (YAML-driven integration test framework):
- Engine, parser, registry, reporter (JUnit XML + HTML), metrics scraping
- 52 registered actions: block, iSCSI, I/O, fault injection, assertions
- Baseline regression framework with 7 hard-fail conditions
- 15 YAML scenarios (smoke, crash, HA, fault, consistency, snapshot)
- 49 unit tests for testrunner internals

QA adversarial suite (21 tests, all PASS):
- Idempotent create mode/RF mismatch detection
- Heartbeat mode downgrade prevention (F3)
- sync_all/sync_quorum partial replica enforcement (F1)
- Concurrent create race safety
- Failover/expand mode preservation
- Cleanup resilience when delete fails
- Master restart auto-register mode handling
- Superblock roundtrip all 3 modes
- Validate edge cases (mode×RF matrix)
- RequiredReplicas quorum math verification
- Sentinel error categorization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 01:06:51 -08:00

60 lines
1.9 KiB
Go

package iscsi
import (
"errors"
"fmt"
"testing"
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/blockerr"
)
func TestMapBlockVolError_Durability_HardwareError(t *testing.T) {
durErrs := []error{
blockerr.ErrDurabilityBarrierFailed,
blockerr.ErrDurabilityQuorumLost,
fmt.Errorf("wrapped: %w", blockerr.ErrDurabilityBarrierFailed),
fmt.Errorf("%w: 1 of 2 barriers failed", blockerr.ErrDurabilityBarrierFailed),
fmt.Errorf("%w: 1 durable of 2 needed", blockerr.ErrDurabilityQuorumLost),
}
for _, err := range durErrs {
result := mapBlockVolError(err)
if result.SenseKey != SenseHardwareError {
t.Errorf("mapBlockVolError(%v) SenseKey = %d, want %d (HARDWARE_ERROR)",
err, result.SenseKey, SenseHardwareError)
}
if result.SenseASC != 0x08 {
t.Errorf("mapBlockVolError(%v) ASC = 0x%02x, want 0x08",
err, result.SenseASC)
}
}
}
func TestMapBlockVolError_NonDurability_MediumError(t *testing.T) {
normalErrs := []error{
errors.New("disk I/O error"),
fmt.Errorf("write failed: %w", errors.New("EIO")),
}
for _, err := range normalErrs {
result := mapBlockVolError(err)
if result.SenseKey != SenseMediumError {
t.Errorf("mapBlockVolError(%v) SenseKey = %d, want %d (MEDIUM_ERROR)",
err, result.SenseKey, SenseMediumError)
}
if result.SenseASC != 0x0C {
t.Errorf("mapBlockVolError(%v) ASC = 0x%02x, want 0x0C",
err, result.SenseASC)
}
}
}
func TestMapBlockVolError_StringMatchNoLongerWorks(t *testing.T) {
// Verify that string-matching is NOT used — a plain error with the
// same text should NOT be detected as a durability error.
fakeErr := errors.New("blockvol: sync_all durability barrier failed")
result := mapBlockVolError(fakeErr)
// This should be MEDIUM_ERROR because it's not the real sentinel.
if result.SenseKey != SenseMediumError {
t.Errorf("string-based error should NOT match as durability error: SenseKey=%d", result.SenseKey)
}
}