mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-26 01:44:48 +00: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>
164 lines
4.5 KiB
Go
164 lines
4.5 KiB
Go
package testrunner
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestCompareBaseline_P99Increase(t *testing.T) {
|
|
baseline := map[string]float64{"p99_write_latency_seconds": 0.005}
|
|
current := map[string]float64{"p99_write_latency_seconds": 0.006} // +20%
|
|
|
|
results := CompareBaseline(baseline, current)
|
|
if len(results) == 0 {
|
|
t.Fatal("expected results")
|
|
}
|
|
for _, r := range results {
|
|
if r.Metric == "p99_write_latency_seconds" && r.Pass {
|
|
t.Error("expected FAIL for 20% latency increase (limit 10%)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompareBaseline_P99WithinLimit(t *testing.T) {
|
|
baseline := map[string]float64{"p99_write_latency_seconds": 0.005}
|
|
current := map[string]float64{"p99_write_latency_seconds": 0.0054} // +8%
|
|
|
|
results := CompareBaseline(baseline, current)
|
|
for _, r := range results {
|
|
if r.Metric == "p99_write_latency_seconds" && !r.Pass {
|
|
t.Errorf("expected PASS for 8%% increase (limit 10%%): %s", r.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompareBaseline_IOPSDecrease(t *testing.T) {
|
|
baseline := map[string]float64{"write_iops": 50000}
|
|
current := map[string]float64{"write_iops": 46000} // -8%
|
|
|
|
results := CompareBaseline(baseline, current)
|
|
for _, r := range results {
|
|
if r.Metric == "write_iops" && r.Pass {
|
|
t.Error("expected FAIL for 8% IOPS decrease (limit 5%)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCompareBaseline_IOPSWithinLimit(t *testing.T) {
|
|
baseline := map[string]float64{"write_iops": 50000}
|
|
current := map[string]float64{"write_iops": 48000} // -4%
|
|
|
|
results := CompareBaseline(baseline, current)
|
|
for _, r := range results {
|
|
if r.Metric == "write_iops" && !r.Pass {
|
|
t.Errorf("expected PASS for 4%% decrease (limit 5%%): %s", r.Reason)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_DataMismatch(t *testing.T) {
|
|
metrics := map[string]float64{"data_mismatch_count": 1}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "data_mismatch" && r.Pass {
|
|
t.Error("expected FAIL for data mismatch")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_BarrierLagUnbounded(t *testing.T) {
|
|
metrics := map[string]float64{"barrier_lag_lsn_max": 1500}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "barrier_lag_lsn_unbounded" && r.Pass {
|
|
t.Error("expected FAIL for barrier_lag_lsn=1500")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_BarrierLagOK(t *testing.T) {
|
|
metrics := map[string]float64{"barrier_lag_lsn_max": 50}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "barrier_lag_lsn_unbounded" && !r.Pass {
|
|
t.Error("expected PASS for barrier_lag_lsn=50")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_BarrierErrorRate(t *testing.T) {
|
|
metrics := map[string]float64{
|
|
"barrier_requests_total": 100,
|
|
"barrier_failures_total": 10, // 10% error rate
|
|
}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "barrier_error_rate" && r.Pass {
|
|
t.Error("expected FAIL for 10% barrier error rate")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_HealthZero(t *testing.T) {
|
|
metrics := map[string]float64{"health_score": 0.0, "fault_active": 0}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "health_zero_without_fault" && r.Pass {
|
|
t.Error("expected FAIL for health=0 without fault")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_HealthZeroDuringFault_OK(t *testing.T) {
|
|
metrics := map[string]float64{"health_score": 0.0, "fault_active": 1}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "health_zero_without_fault" && !r.Pass {
|
|
t.Error("expected PASS for health=0 during active fault")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_WALFullStall(t *testing.T) {
|
|
metrics := map[string]float64{"wal_full_duration_seconds": 15}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if r.Condition == "wal_full_stall" && r.Pass {
|
|
t.Error("expected FAIL for WAL full 15s")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHardFail_AllPass(t *testing.T) {
|
|
metrics := map[string]float64{
|
|
"health_score": 1.0,
|
|
"barrier_lag_lsn_max": 10,
|
|
}
|
|
results := RunHardFailChecks(metrics)
|
|
for _, r := range results {
|
|
if !r.Pass {
|
|
t.Errorf("expected all PASS, got FAIL for %s: %s", r.Condition, r.Detail)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatRegressionReport(t *testing.T) {
|
|
report := &RegressionReport{
|
|
Results: []RegressionResult{
|
|
{Metric: "p99_write_latency_seconds", BaselineVal: 0.005, CurrentVal: 0.006, Pass: false, Reason: "change=+20%"},
|
|
},
|
|
HardFails: []HardFailResult{
|
|
{Condition: "data_mismatch", Pass: true},
|
|
},
|
|
OverallPass: false,
|
|
BaselineGitSHA: "abc123",
|
|
CurrentGitSHA: "def456",
|
|
}
|
|
s := FormatRegressionReport(report)
|
|
if s == "" {
|
|
t.Error("expected non-empty report")
|
|
}
|
|
if len(s) < 50 {
|
|
t.Errorf("report too short: %s", s)
|
|
}
|
|
}
|