Files
seaweedfs/weed/storage/blockvol/test/smoke_test.go
T
Ping QiuandClaude Opus 4.6 44da35faf6 test: add integration test infrastructure for blockvol iSCSI
Test harness for running blockvol iSCSI tests on WSL2 and remote nodes
(m01/M02). Includes Node (SSH/local exec), ISCSIClient (discover/login/
logout), WeedTarget (weed volume server lifecycle), and test suites for
smoke, stress, crash recovery, chaos, perf benchmarks, and apps (fio/dd).

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

191 lines
5.5 KiB
Go

//go:build integration
package test
import (
"context"
"fmt"
"strings"
"testing"
"time"
)
func TestSmoke(t *testing.T) {
t.Run("Discovery", testSmokeDiscovery)
t.Run("DDIntegrity", testSmokeDDIntegrity)
t.Run("MkfsExt4", testSmokeMkfsExt4)
t.Run("MkfsXfs", testSmokeMkfsXfs)
t.Run("FioVerify", testSmokeFioVerify)
t.Run("LogoutClean", testSmokeLogoutClean)
}
func testSmokeDiscovery(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
tgt, iscsi, host := newTestTarget(t, "50M", "")
if err := tgt.Start(ctx, true); err != nil {
t.Fatalf("start: %v", err)
}
iqns, err := iscsi.Discover(ctx, host, tgt.config.Port)
if err != nil {
t.Fatalf("discover: %v", err)
}
found := false
for _, iqn := range iqns {
if iqn == tgt.config.IQN {
found = true
}
}
if !found {
t.Fatalf("IQN %s not found in discovery response: %v", tgt.config.IQN, iqns)
}
}
func testSmokeDDIntegrity(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel()
tgt, iscsi, host := newTestTarget(t, "50M", "")
dev := startAndLogin(t, ctx, tgt, iscsi, host)
// Write 1MB of random data
_, _, code, err := clientNode.RunRoot(ctx,
fmt.Sprintf("dd if=/dev/urandom of=%s bs=1M count=1 oflag=direct 2>/dev/null", dev))
if err != nil || code != 0 {
t.Fatalf("dd write: code=%d err=%v", code, err)
}
// Read back and checksum
sum1, _, _, _ := clientNode.RunRoot(ctx,
fmt.Sprintf("dd if=%s bs=1M count=1 iflag=direct 2>/dev/null | md5sum", dev))
sum2, _, _, _ := clientNode.RunRoot(ctx,
fmt.Sprintf("dd if=%s bs=1M count=1 iflag=direct 2>/dev/null | md5sum", dev))
s1 := firstLine(sum1)
s2 := firstLine(sum2)
if s1 != s2 {
t.Fatalf("checksum mismatch: %s vs %s", s1, s2)
}
t.Logf("checksums match: %s", s1)
}
func testSmokeMkfsExt4(t *testing.T) {
testSmokeMkfs(t, "ext4", "mkfs.ext4", "100M")
}
func testSmokeMkfsXfs(t *testing.T) {
t.Skip("P3-BUG-11: WRITE SAME(16) not implemented, XFS sends it for inode zeroing")
if !clientNode.HasCommand("mkfs.xfs") {
t.Skip("mkfs.xfs not available")
}
testSmokeMkfs(t, "xfs", "mkfs.xfs", "500M") // XFS needs >= 300MB
}
func testSmokeMkfs(t *testing.T, fstype, mkfsCmd, volSize string) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
tgt, iscsi, host := newTestTarget(t, volSize, "")
dev := startAndLogin(t, ctx, tgt, iscsi, host)
mnt := "/tmp/blockvol-mnt"
t.Cleanup(func() {
cleanCtx, cleanCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cleanCancel()
clientNode.RunRoot(cleanCtx, fmt.Sprintf("umount -f %s 2>/dev/null", mnt))
clientNode.RunRoot(cleanCtx, fmt.Sprintf("rm -rf %s", mnt))
})
// mkfs
mkfsArgs := " -F" // ext4: force, xfs: force overwrite
if fstype == "xfs" {
mkfsArgs = " -f"
}
_, stderr, code, err := clientNode.RunRoot(ctx,
fmt.Sprintf("%s%s %s", mkfsCmd, mkfsArgs, dev))
if err != nil || code != 0 {
t.Fatalf("mkfs.%s: code=%d stderr=%s err=%v", fstype, code, stderr, err)
}
// Mount, write, unmount
clientNode.RunRoot(ctx, fmt.Sprintf("mkdir -p %s", mnt))
_, _, code, _ = clientNode.RunRoot(ctx, fmt.Sprintf("mount %s %s", dev, mnt))
if code != 0 {
t.Fatalf("mount failed")
}
testContent := "blockvol-integration-test-data"
// Use bash -c with tee to ensure redirect works under sudo
clientNode.RunRoot(ctx, fmt.Sprintf("bash -c 'echo %s | tee %s/testfile.txt'", testContent, mnt))
clientNode.RunRoot(ctx, "sync")
clientNode.RunRoot(ctx, fmt.Sprintf("umount %s", mnt))
// Brief pause to let device settle after unmount
time.Sleep(1 * time.Second)
// Remount and verify
mountOpts := ""
if fstype == "xfs" {
mountOpts = "-o nouuid" // avoid UUID conflict with stale kernel state
}
_, stderr2, code, _ := clientNode.RunRoot(ctx, fmt.Sprintf("mount %s %s %s", mountOpts, dev, mnt))
if code != 0 {
t.Fatalf("remount failed: %s", stderr2)
}
stdout, _, _, _ := clientNode.RunRoot(ctx, fmt.Sprintf("cat %s/testfile.txt", mnt))
if !strings.Contains(stdout, testContent) {
t.Fatalf("file content mismatch: got %q, want %q", stdout, testContent)
}
t.Logf("%s: file persists across mount cycles", fstype)
}
func testSmokeFioVerify(t *testing.T) {
if !clientNode.HasCommand("fio") {
t.Skip("fio not available")
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
tgt, iscsi, host := newTestTarget(t, "100M", "")
dev := startAndLogin(t, ctx, tgt, iscsi, host)
cmd := fmt.Sprintf("fio --name=verify --filename=%s --rw=randrw --verify=crc32 "+
"--bs=4k --size=50M --randrepeat=1 --direct=1 --ioengine=libaio "+
"--runtime=60 --time_based=0 --output-format=json", dev)
stdout, stderr, code, err := clientNode.RunRoot(ctx, cmd)
if err != nil || code != 0 {
t.Fatalf("fio: code=%d stderr=%s err=%v", code, stderr, err)
}
if strings.Contains(stdout, "\"verify_errors\"") && !strings.Contains(stdout, "\"verify_errors\" : 0") {
t.Fatalf("fio verify errors detected")
}
t.Log("fio verify passed with 0 errors")
}
func testSmokeLogoutClean(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
tgt, iscsi, host := newTestTarget(t, "50M", "")
_ = startAndLogin(t, ctx, tgt, iscsi, host)
// Logout
if err := iscsi.Logout(ctx, tgt.config.IQN); err != nil {
t.Fatalf("logout: %v", err)
}
// Verify no stale sessions
stdout, _, _, _ := clientNode.RunRoot(ctx, "iscsiadm -m session 2>&1")
if strings.Contains(stdout, tgt.config.IQN) {
t.Fatalf("stale session found after logout: %s", stdout)
}
t.Log("no stale sessions after logout")
}