mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
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>
105 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|