Files
seaweedfs/weed/storage/blockvol/testrunner/local_node_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

123 lines
2.7 KiB
Go

package testrunner
import (
"context"
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
func skipIfWindows(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("LocalNode tests require Unix shell")
}
}
func TestLocalNode_Run_Echo(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
stdout, stderr, code, err := n.Run(ctx, "echo hello")
if err != nil {
t.Fatalf("Run: %v", err)
}
if code != 0 {
t.Fatalf("exit code %d, stderr: %s", code, stderr)
}
if got := stdout; got != "hello\n" {
t.Errorf("stdout = %q, want %q", got, "hello\n")
}
}
func TestLocalNode_Run_ExitCode(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, _, code, err := n.Run(ctx, "exit 42")
if err != nil {
t.Fatalf("Run: %v", err)
}
if code != 42 {
t.Errorf("exit code = %d, want 42", code)
}
}
func TestLocalNode_Run_Timeout(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
_, _, _, err := n.Run(ctx, "sleep 10")
if err == nil {
t.Fatal("expected timeout error")
}
}
func TestLocalNode_RunRoot_NonRoot(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
// Force non-root to test sudo path.
n.isRoot = false
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// sudo -n will likely fail without sudoers config, but should not panic.
_, _, _, err := n.RunRoot(ctx, "echo test")
// We only care that it doesn't panic — error is expected on most CI.
_ = err
}
func TestLocalNode_Upload(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
dir := t.TempDir()
src := filepath.Join(dir, "src.bin")
dst := filepath.Join(dir, "subdir", "dst.bin")
// Create source file.
content := []byte("test binary content 12345")
if err := os.WriteFile(src, content, 0644); err != nil {
t.Fatalf("write src: %v", err)
}
// Upload (should create subdir).
if err := n.Upload(src, dst); err != nil {
t.Fatalf("Upload: %v", err)
}
// Verify.
got, err := os.ReadFile(dst)
if err != nil {
t.Fatalf("read dst: %v", err)
}
if string(got) != string(content) {
t.Errorf("content mismatch: got %q", got)
}
// Verify executable permission.
info, _ := os.Stat(dst)
if info.Mode()&0100 == 0 {
t.Error("file is not executable")
}
}
func TestLocalNode_Close(t *testing.T) {
n := NewLocalNode("test")
n.Close() // should be no-op, no panic
}
func TestLocalNode_DetectRoot(t *testing.T) {
skipIfWindows(t)
n := NewLocalNode("test")
// We just verify it doesn't panic and returns a bool.
_ = n.IsRoot()
}