mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-21 14:46:58 +00:00
test: Phase 20 Tier 1 component tests — wiring proof for CI/CD
6 new component tests closing gaps identified in the test matrix audit: P20-T4-C3: Missing projection with active V2 core fails closed - v2Core != nil, no projection cached → gate with "missing_engine_projection" P20-T4-C6: Gate actually removes iSCSI target (enforcement) - real TargetServer → HasTarget(iqn)==true before gate - gate → HasTarget(iqn)==false (DisconnectVolume called) - ungate → HasTarget(iqn)==true (AddVolume restores) P20-T5-C3: FailoverDiagnosticSnapshot carries both mode fields - register volume with EngineProjectionMode + ClusterReplicationMode - trigger pending rebuild → volume appears in diagnostic - diagnostic entry carries both modes from registry lookup P20-T3-C5: V2PromotionMode diagnostic tri-state - disabled / placeholder_fail_closed / transport_ready - all three configurations produce correct diagnostic value P20-T1-C3: EngineProjectionMode proto round-trip - set value survives InfoMessageToProto → InfoMessageFromProto - empty value produces nil proto field (presence semantics) P20-T4-C8: ActivationGated proto round-trip - gated=true + reason survives round-trip - not-gated produces no spurious reason Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1c7154a11a
commit
6bf9a6c283
@@ -315,3 +315,73 @@ func TestT5_APISurface_DistinctNaming(t *testing.T) {
|
||||
info.EngineProjectionMode, info.ClusterReplicationMode)
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T5-C3: FailoverDiagnosticSnapshot carries both mode fields.
|
||||
func TestT5_DiagnosticSnapshot_CarriesModes(t *testing.T) {
|
||||
ms := testMasterServerForFailover(t)
|
||||
registerVolumeWithReplica(t, ms, "vol-diag-modes", "vs1", "vs2", 1, 5*time.Second)
|
||||
|
||||
// Set EngineProjectionMode + ClusterReplicationMode on the entry.
|
||||
if err := ms.blockRegistry.UpdateEntry("vol-diag-modes", func(e *BlockVolumeEntry) {
|
||||
e.EngineProjectionMode = "publish_healthy"
|
||||
e.HasEngineProjectionMode = true
|
||||
e.ClusterReplicationMode = "catching_up"
|
||||
}); err != nil {
|
||||
t.Fatalf("update entry: %v", err)
|
||||
}
|
||||
|
||||
// Trigger a pending rebuild so the volume appears in failover diagnostic.
|
||||
ms.recordPendingRebuild("vs-dead", pendingRebuild{
|
||||
VolumeName: "vol-diag-modes",
|
||||
NewPrimary: "vs2",
|
||||
Epoch: 2,
|
||||
})
|
||||
|
||||
diag := ms.FailoverDiagnosticSnapshot()
|
||||
var found *FailoverVolumeState
|
||||
for i := range diag.Volumes {
|
||||
if diag.Volumes[i].VolumeName == "vol-diag-modes" {
|
||||
found = &diag.Volumes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
t.Fatal("expected vol-diag-modes in failover diagnostic")
|
||||
}
|
||||
if found.ClusterReplicationMode != "catching_up" {
|
||||
t.Fatalf("diagnostic ClusterReplicationMode=%q, want catching_up", found.ClusterReplicationMode)
|
||||
}
|
||||
if found.EngineProjectionMode != "publish_healthy" {
|
||||
t.Fatalf("diagnostic EngineProjectionMode=%q, want publish_healthy", found.EngineProjectionMode)
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T3-C5: V2PromotionMode diagnostic reflects all three states.
|
||||
func TestT3_V2PromotionMode_DiagnosticTriState(t *testing.T) {
|
||||
// State 1: disabled
|
||||
ms1 := testMasterServerForFailover(t)
|
||||
ms1.blockV2Promotion = false
|
||||
diag1 := ms1.FailoverDiagnosticSnapshot()
|
||||
if diag1.V2PromotionMode != "disabled" {
|
||||
t.Fatalf("state 1: V2PromotionMode=%q, want disabled", diag1.V2PromotionMode)
|
||||
}
|
||||
|
||||
// State 2: placeholder_fail_closed
|
||||
ms2 := testMasterServerForFailover(t)
|
||||
ms2.blockV2Promotion = true
|
||||
ms2.blockVSQueryEvidence = ms2.defaultBlockVSQueryEvidence
|
||||
ms2.blockV2EvidenceTransport = false
|
||||
diag2 := ms2.FailoverDiagnosticSnapshot()
|
||||
if diag2.V2PromotionMode != "placeholder_fail_closed" {
|
||||
t.Fatalf("state 2: V2PromotionMode=%q, want placeholder_fail_closed", diag2.V2PromotionMode)
|
||||
}
|
||||
|
||||
// State 3: transport_ready
|
||||
ms3 := testMasterServerForFailover(t)
|
||||
ms3.blockV2Promotion = true
|
||||
ms3.blockV2EvidenceTransport = true
|
||||
diag3 := ms3.FailoverDiagnosticSnapshot()
|
||||
if diag3.V2PromotionMode != "transport_ready" {
|
||||
t.Fatalf("state 3: V2PromotionMode=%q, want transport_ready", diag3.V2PromotionMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package weed_server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
engine "github.com/seaweedfs/seaweedfs/sw-block/engine/replication"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol"
|
||||
"github.com/seaweedfs/seaweedfs/weed/storage/blockvol/iscsi"
|
||||
)
|
||||
|
||||
func TestT4_DegradedProjection_GatesActivation(t *testing.T) {
|
||||
@@ -179,3 +182,74 @@ func TestT4_ApplyCoreAssignment_GatesDegradedPrimary(t *testing.T) {
|
||||
t.Fatal("expected non-empty gate reason")
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T4-C3: Missing projection with active V2 core fails closed.
|
||||
func TestT4_MissingProjection_FailsClosed(t *testing.T) {
|
||||
bs := newTestBlockServiceDirect(t)
|
||||
path := createTestVolDirect(t, bs, "gate-missing-proj")
|
||||
|
||||
// v2Core is non-nil (production config), but no projection cached.
|
||||
// This must fail closed, not silently leave serving enabled.
|
||||
if bs.v2Core == nil {
|
||||
t.Fatal("test requires v2Core != nil")
|
||||
}
|
||||
|
||||
bs.evaluateActivationGate(path)
|
||||
|
||||
gated, reason := bs.IsActivationGated(path)
|
||||
if !gated {
|
||||
t.Fatal("expected activation gated when V2 core is active but projection missing")
|
||||
}
|
||||
if reason != "missing_engine_projection" {
|
||||
t.Fatalf("reason=%q, want %q", reason, "missing_engine_projection")
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T4-C6: Gate actually removes iSCSI target (enforcement, not bookkeeping).
|
||||
func TestT4_GateRemovesISCSITarget(t *testing.T) {
|
||||
bs := newTestBlockServiceDirect(t)
|
||||
path := createTestVolDirect(t, bs, "gate-iscsi-remove")
|
||||
|
||||
// Create a real TargetServer (no listen, just registry).
|
||||
logger := log.New(os.Stderr, "iscsi-test: ", log.LstdFlags)
|
||||
ts := iscsi.NewTargetServer("127.0.0.1:0", iscsi.DefaultTargetConfig(), logger)
|
||||
bs.targetServer = ts
|
||||
|
||||
// Register volume with target.
|
||||
vol, ok := bs.blockStore.GetBlockVolume(path)
|
||||
if !ok {
|
||||
t.Fatal("volume not found")
|
||||
}
|
||||
name := volumeNameFromPath(path)
|
||||
iqn := bs.iqnPrefix + blockvol.SanitizeIQN(name)
|
||||
adapter := blockvol.NewBlockVolAdapter(vol)
|
||||
ts.AddVolume(iqn, adapter)
|
||||
|
||||
if !ts.HasTarget(iqn) {
|
||||
t.Fatal("target should exist before gate")
|
||||
}
|
||||
|
||||
// Inject degraded projection and gate.
|
||||
bs.coreProjMu.Lock()
|
||||
bs.coreProj[path] = engine.PublicationProjection{
|
||||
Mode: engine.ModeView{Name: engine.ModeDegraded, Reason: "test"},
|
||||
}
|
||||
bs.coreProjMu.Unlock()
|
||||
bs.evaluateActivationGate(path)
|
||||
|
||||
if ts.HasTarget(iqn) {
|
||||
t.Fatal("target should be removed after gate (enforcement, not just bookkeeping)")
|
||||
}
|
||||
|
||||
// Inject healthy projection and ungate.
|
||||
bs.coreProjMu.Lock()
|
||||
bs.coreProj[path] = engine.PublicationProjection{
|
||||
Mode: engine.ModeView{Name: engine.ModePublishHealthy},
|
||||
}
|
||||
bs.coreProjMu.Unlock()
|
||||
bs.evaluateActivationGate(path)
|
||||
|
||||
if !ts.HasTarget(iqn) {
|
||||
t.Fatal("target should be restored after ungate")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,3 +623,59 @@ func TestP10P1_ProtoRoundTrip_MissingServerID_NotSynthesized(t *testing.T) {
|
||||
t.Fatalf("decode: ReplicaServerID=%q, want empty (not synthesized)", decoded.ReplicaServerID)
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T1-C3: EngineProjectionMode survives proto round-trip.
|
||||
func TestP20_ProtoRoundTrip_EngineProjectionMode(t *testing.T) {
|
||||
orig := BlockVolumeInfoMessage{
|
||||
Path: "/data/epm-roundtrip.blk",
|
||||
Epoch: 5,
|
||||
EngineProjectionMode: "publish_healthy",
|
||||
}
|
||||
pb := InfoMessageToProto(orig)
|
||||
decoded := InfoMessageFromProto(pb)
|
||||
if decoded.EngineProjectionMode != "publish_healthy" {
|
||||
t.Fatalf("EngineProjectionMode=%q after round-trip, want %q", decoded.EngineProjectionMode, "publish_healthy")
|
||||
}
|
||||
|
||||
// Empty field stays empty (nil presence).
|
||||
origEmpty := BlockVolumeInfoMessage{Path: "/data/epm-empty.blk"}
|
||||
pbEmpty := InfoMessageToProto(origEmpty)
|
||||
if pbEmpty.EngineProjectionMode != nil {
|
||||
t.Fatalf("nil EngineProjectionMode produced non-nil proto: %v", *pbEmpty.EngineProjectionMode)
|
||||
}
|
||||
decodedEmpty := InfoMessageFromProto(pbEmpty)
|
||||
if decodedEmpty.EngineProjectionMode != "" {
|
||||
t.Fatalf("empty EngineProjectionMode=%q after round-trip, want empty", decodedEmpty.EngineProjectionMode)
|
||||
}
|
||||
}
|
||||
|
||||
// P20-T4-C8: ActivationGated + ActivationGateReason survive proto round-trip.
|
||||
func TestP20_ProtoRoundTrip_ActivationGated(t *testing.T) {
|
||||
orig := BlockVolumeInfoMessage{
|
||||
Path: "/data/gate-roundtrip.blk",
|
||||
ActivationGated: true,
|
||||
ActivationGateReason: "engine_projection_mode=degraded: barrier_timeout",
|
||||
}
|
||||
pb := InfoMessageToProto(orig)
|
||||
decoded := InfoMessageFromProto(pb)
|
||||
if !decoded.ActivationGated {
|
||||
t.Fatal("ActivationGated=false after round-trip, want true")
|
||||
}
|
||||
if decoded.ActivationGateReason != orig.ActivationGateReason {
|
||||
t.Fatalf("ActivationGateReason=%q, want %q", decoded.ActivationGateReason, orig.ActivationGateReason)
|
||||
}
|
||||
|
||||
// Not-gated: false should round-trip without spurious reason.
|
||||
origNotGated := BlockVolumeInfoMessage{
|
||||
Path: "/data/gate-notgated.blk",
|
||||
ActivationGated: false,
|
||||
}
|
||||
pbNot := InfoMessageToProto(origNotGated)
|
||||
decodedNot := InfoMessageFromProto(pbNot)
|
||||
if decodedNot.ActivationGated {
|
||||
t.Fatal("not-gated produced ActivationGated=true after round-trip")
|
||||
}
|
||||
if decodedNot.ActivationGateReason != "" {
|
||||
t.Fatalf("not-gated produced reason=%q, want empty", decodedNot.ActivationGateReason)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user