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

105 lines
2.8 KiB
Go

package testrunner
import (
"context"
"sort"
"testing"
)
func TestRegistry_TierGating(t *testing.T) {
r := NewRegistry()
noop := ActionHandlerFunc(func(ctx context.Context, actx *ActionContext, act Action) (map[string]string, error) {
return nil, nil
})
r.RegisterFunc("exec", TierCore, noop)
r.RegisterFunc("dd_write", TierBlock, noop)
r.RegisterFunc("inject_netem", TierChaos, noop)
r.RegisterFunc("start_weed_master", TierDevOps, noop)
// No gating — all accessible.
if _, err := r.Get("exec"); err != nil {
t.Errorf("exec ungated: %v", err)
}
if _, err := r.Get("dd_write"); err != nil {
t.Errorf("dd_write ungated: %v", err)
}
// Enable only core.
r.EnableTiers([]string{TierCore})
if _, err := r.Get("exec"); err != nil {
t.Errorf("exec (core enabled): %v", err)
}
if _, err := r.Get("dd_write"); err == nil {
t.Error("dd_write should be blocked when only core is enabled")
}
if _, err := r.Get("inject_netem"); err == nil {
t.Error("inject_netem should be blocked")
}
// List should only return core actions.
names := r.List()
if len(names) != 1 || names[0] != "exec" {
t.Errorf("List() = %v, want [exec]", names)
}
// Reset gating.
r.EnableTiers(nil)
names = r.List()
if len(names) != 4 {
t.Errorf("List() after reset = %d, want 4", len(names))
}
}
func TestRegistry_ListByTier(t *testing.T) {
r := NewRegistry()
noop := ActionHandlerFunc(func(ctx context.Context, actx *ActionContext, act Action) (map[string]string, error) {
return nil, nil
})
r.RegisterFunc("a", TierCore, noop)
r.RegisterFunc("b", TierCore, noop)
r.RegisterFunc("c", TierBlock, noop)
byTier := r.ListByTier()
coreActions := byTier[TierCore]
sort.Strings(coreActions)
if len(coreActions) != 2 || coreActions[0] != "a" || coreActions[1] != "b" {
t.Errorf("core = %v, want [a b]", coreActions)
}
if len(byTier[TierBlock]) != 1 || byTier[TierBlock][0] != "c" {
t.Errorf("block = %v, want [c]", byTier[TierBlock])
}
}
func TestRegistry_ActionTier(t *testing.T) {
r := NewRegistry()
noop := ActionHandlerFunc(func(ctx context.Context, actx *ActionContext, act Action) (map[string]string, error) {
return nil, nil
})
r.RegisterFunc("foo", TierChaos, noop)
if tier := r.ActionTier("foo"); tier != TierChaos {
t.Errorf("tier = %q, want chaos", tier)
}
if tier := r.ActionTier("nonexistent"); tier != "" {
t.Errorf("nonexistent tier = %q, want empty", tier)
}
}
func TestRegistry_EmptyTiersAllowsAll(t *testing.T) {
r := NewRegistry()
noop := ActionHandlerFunc(func(ctx context.Context, actx *ActionContext, act Action) (map[string]string, error) {
return nil, nil
})
r.RegisterFunc("x", TierDevOps, noop)
// Empty slice should not restrict.
r.EnableTiers([]string{})
if _, err := r.Get("x"); err != nil {
t.Errorf("empty tiers should allow all: %v", err)
}
}