Browse Source

Exempt home region from duty-cycle gating; restructure tests by scenario

The home region is an explicit operator statement of which cluster this
repeater serves, which the innermost-depth heuristic alone can't express
(the home region may not be at the deepest level of the hierarchy). Never
gate it, in addition to the always-protected innermost layer. Repeaters
without a home region set keep the pure depth-based behaviour.

Also restructure the gating tests into one-scenario-per-test cases with
complete per-region assertions (review feedback), and add coverage for
the home region exemption.

Co-Authored-By: Claude Fable 5 <[email protected]>
pull/2960/head
Kasper Hägele 2 weeks ago
parent
commit
13935baafd
  1. 5
      src/helpers/RegionMap.cpp
  2. 3
      src/helpers/RegionMap.h
  3. 187
      test/test_region_gating/test_region_gating.cpp

5
src/helpers/RegionMap.cpp

@ -350,11 +350,12 @@ void RegionMap::applyDutyGate(uint8_t level) {
else wildcard.rt_flags &= (uint8_t)~REGION_DENY_FLOOD;
// Named regions: level 2 gates depth 1, level 3 gates depth 2, ... The innermost
// layer (depth == maxDepth) is always protected.
// layer (depth == maxDepth) is always protected, as is the home region (an explicit
// operator signal of this repeater's own cluster, which depth alone can't express).
for (int i = 0; i < num_regions; i++) {
RegionEntry* r = &regions[i];
int d = depthOf(r);
bool gate = (level >= 2) && (d <= (int)level - 1) && (d < maxDepth);
bool gate = (level >= 2) && (d <= (int)level - 1) && (d < maxDepth) && (r->id != home_id);
if (gate) r->rt_flags |= REGION_DENY_FLOOD;
else r->rt_flags &= (uint8_t)~REGION_DENY_FLOOD;
}

3
src/helpers/RegionMap.h

@ -60,7 +60,8 @@ public:
// --- duty-cycle region gating (transient, via rt_flags; see applyDutyGate) ---
int getMaxDepth() const; // deepest named-region depth (wildcard=0, its children=1, ...)
uint8_t getMaxGateLevel() const; // highest gate level that still protects the innermost cluster
void applyDutyGate(uint8_t level); // gate '*' first (level>=1), then broadest depth inward
void applyDutyGate(uint8_t level); // gate '*' first (level>=1), then broadest depth inward;
// the innermost layer and the home region are never gated
int exportNamesTo(char *dest, int max_len, uint8_t mask, bool invert = false);
int getTransportKeysFor(const RegionEntry& src, TransportKey dest[], int max_num);

187
test/test_region_gating/test_region_gating.cpp

@ -3,120 +3,195 @@
// Unit tests for duty-cycle region gating (issue #2747).
// These exercise the pure hierarchy/level logic; no crypto or filesystem is touched.
// Each test covers one gating scenario (one level, one transition, or one invariant)
// so a failure points directly at the behaviour that broke.
static bool gated(RegionMap& rm, const char* name) {
RegionEntry* r = rm.findByName(name); // findByName("*") returns the wildcard
return r && (r->rt_flags & REGION_DENY_FLOOD);
}
// Build: * > eu > nl > nl-ge > nl-ge-nij (a single deep chain)
static void buildNlChain(RegionMap& rm) {
RegionEntry* eu = rm.putRegion("eu", 0);
RegionEntry* nl = rm.putRegion("nl", eu->id);
RegionEntry* nlge = rm.putRegion("nl-ge", nl->id);
rm.putRegion("nl-ge-nij", nlge->id);
}
// Fixture: * > eu > nl > nl-ge > nl-ge-nij (a single deep chain)
class NlChainGating : public ::testing::Test {
protected:
TransportKeyStore ks;
RegionMap rm{ks};
void SetUp() override {
RegionEntry* eu = rm.putRegion("eu", 0);
RegionEntry* nl = rm.putRegion("nl", eu->id);
RegionEntry* nlge = rm.putRegion("nl-ge", nl->id);
rm.putRegion("nl-ge-nij", nlge->id);
}
};
// Fixture: * > cz > { cz-ulk, cz-stc, cz-lbk } (three regions on the same level)
class CzFlatGating : public ::testing::Test {
protected:
TransportKeyStore ks;
RegionMap rm{ks};
// Build: * > cz > { cz-ulk, cz-stc, cz-lbk } (three regions on the same level)
static void buildCzFlat(RegionMap& rm) {
RegionEntry* cz = rm.putRegion("cz", 0);
rm.putRegion("cz-ulk", cz->id);
rm.putRegion("cz-stc", cz->id);
rm.putRegion("cz-lbk", cz->id);
}
void SetUp() override {
RegionEntry* cz = rm.putRegion("cz", 0);
rm.putRegion("cz-ulk", cz->id);
rm.putRegion("cz-stc", cz->id);
rm.putRegion("cz-lbk", cz->id);
}
};
TEST(RegionGating, DepthAndMaxGateLevel) {
TransportKeyStore ks;
RegionMap rm(ks);
buildNlChain(rm);
// ---------- depth model ----------
TEST_F(NlChainGating, MaxDepthCountsChainDepth) {
EXPECT_EQ(rm.getMaxDepth(), 4);
}
TEST_F(NlChainGating, MaxGateLevelEqualsMaxDepth) {
EXPECT_EQ(rm.getMaxGateLevel(), 4);
}
TEST(RegionGating, GatesWildcardFirstThenOutermostInward) {
TransportKeyStore ks;
RegionMap rm(ks);
buildNlChain(rm);
// ---------- gating, level by level ----------
TEST_F(NlChainGating, LevelZeroGatesNothing) {
rm.applyDutyGate(0);
EXPECT_FALSE(gated(rm, "*"));
EXPECT_FALSE(gated(rm, "eu"));
EXPECT_FALSE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
rm.applyDutyGate(1); // wildcard first
TEST_F(NlChainGating, LevelOneGatesOnlyWildcard) {
rm.applyDutyGate(1);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_FALSE(gated(rm, "eu"));
EXPECT_FALSE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
rm.applyDutyGate(2); // + broadest named region
TEST_F(NlChainGating, LevelTwoAddsBroadestNamedRegion) {
rm.applyDutyGate(2);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_FALSE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
TEST_F(NlChainGating, LevelThreeGatesDownToSecondDeepest) {
rm.applyDutyGate(3);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_TRUE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
rm.applyDutyGate(4); // max: everything except the innermost cluster
TEST_F(NlChainGating, MaxLevelProtectsOnlyInnermostCluster) {
rm.applyDutyGate(4);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_TRUE(gated(rm, "nl"));
EXPECT_TRUE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij")); // innermost local cluster is always protected
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
TEST(RegionGating, InnermostClusterNeverGatedEvenAboveMax) {
TransportKeyStore ks;
RegionMap rm(ks);
buildNlChain(rm);
rm.applyDutyGate(99); // clamped behaviour
TEST_F(NlChainGating, LevelAboveMaxStillProtectsInnermostCluster) {
rm.applyDutyGate(99);
EXPECT_TRUE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
TEST(RegionGating, RecoveryReenablesInsideOut) {
TransportKeyStore ks;
RegionMap rm(ks);
buildNlChain(rm);
// ---------- recovery ----------
TEST_F(NlChainGating, RecoveryStepDownReenablesInsideOut) {
rm.applyDutyGate(4);
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_TRUE(gated(rm, "nl-ge"));
rm.applyDutyGate(2); // step back down: nl and nl-ge come back first
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_FALSE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
rm.applyDutyGate(0); // fully recovered
TEST_F(NlChainGating, RecoveryToZeroClearsAllGates) {
rm.applyDutyGate(4);
rm.applyDutyGate(0);
EXPECT_FALSE(gated(rm, "*"));
EXPECT_FALSE(gated(rm, "eu"));
EXPECT_FALSE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
TEST(RegionGating, MultipleRegionsOnSameLevelGateTogetherAndLeavesProtected) {
TransportKeyStore ks;
RegionMap rm(ks);
buildCzFlat(rm);
// ---------- home region exemption ----------
TEST_F(NlChainGating, HomeRegionIsNeverGated) {
rm.setHomeRegion(rm.findByName("nl-ge")); // home NOT at the deepest level
rm.applyDutyGate(4);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "eu"));
EXPECT_TRUE(gated(rm, "nl"));
EXPECT_FALSE(gated(rm, "nl-ge")); // exempt: it's the home region
EXPECT_FALSE(gated(rm, "nl-ge-nij")); // exempt: innermost layer
}
TEST_F(NlChainGating, BroadHomeRegionIsAlsoExempt) {
rm.setHomeRegion(rm.findByName("eu")); // operator chose a broad home region
rm.applyDutyGate(4);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_FALSE(gated(rm, "eu")); // exempt, even though it is depth 1
EXPECT_TRUE(gated(rm, "nl"));
EXPECT_TRUE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
TEST_F(NlChainGating, ClearingHomeRegionRestoresDepthOnlyGating) {
rm.setHomeRegion(rm.findByName("nl-ge"));
rm.applyDutyGate(4);
rm.setHomeRegion(NULL);
rm.applyDutyGate(4); // gate state is recomputed on every application
EXPECT_TRUE(gated(rm, "nl-ge"));
EXPECT_FALSE(gated(rm, "nl-ge-nij"));
}
// ---------- multiple regions on the same level ----------
TEST_F(CzFlatGating, MaxGateLevelForTwoLevelTree) {
EXPECT_EQ(rm.getMaxDepth(), 2);
EXPECT_EQ(rm.getMaxGateLevel(), 2);
}
rm.applyDutyGate(1); // wildcard only
TEST_F(CzFlatGating, LevelOneGatesOnlyWildcard) {
rm.applyDutyGate(1);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_FALSE(gated(rm, "cz"));
EXPECT_FALSE(gated(rm, "cz-ulk"));
EXPECT_FALSE(gated(rm, "cz-stc"));
EXPECT_FALSE(gated(rm, "cz-lbk"));
}
rm.applyDutyGate(2); // wildcard + cz; the three leaf regions stay up
TEST_F(CzFlatGating, MaxLevelKeepsAllLeavesOnSameLevel) {
rm.applyDutyGate(2);
EXPECT_TRUE(gated(rm, "*"));
EXPECT_TRUE(gated(rm, "cz"));
EXPECT_FALSE(gated(rm, "cz-ulk"));
EXPECT_FALSE(gated(rm, "cz-stc"));
EXPECT_FALSE(gated(rm, "cz-lbk"));
}
TEST(RegionGating, LoneWildcardIsNeverGated) {
// ---------- degenerate configurations ----------
TEST(RegionGatingEdgeCases, LoneWildcardIsNeverGated) {
TransportKeyStore ks;
RegionMap rm(ks); // no named regions
RegionMap rm(ks); // no named regions: the wildcard IS the local cluster
EXPECT_EQ(rm.getMaxDepth(), 0);
EXPECT_EQ(rm.getMaxGateLevel(), 0);
rm.applyDutyGate(1);
EXPECT_FALSE(gated(rm, "*")); // wildcard is the local cluster here, keep serving
EXPECT_FALSE(gated(rm, "*"));
}
TEST(RegionGating, SingleNamedRegionGatesWildcardButKeepsRegion) {
TEST(RegionGatingEdgeCases, SingleNamedRegionGatesWildcardButKeepsRegion) {
TransportKeyStore ks;
RegionMap rm(ks);
rm.putRegion("cz", 0);
@ -126,11 +201,9 @@ TEST(RegionGating, SingleNamedRegionGatesWildcardButKeepsRegion) {
EXPECT_FALSE(gated(rm, "cz"));
}
TEST(RegionGating, RuntimeGateIsSeparateFromConfigFlags) {
TransportKeyStore ks;
RegionMap rm(ks);
buildNlChain(rm);
// ---------- config flags vs runtime gate ----------
TEST_F(NlChainGating, RuntimeGateDoesNotTouchConfigFlags) {
RegionEntry* eu = rm.findByName("eu");
eu->flags &= (uint8_t)~REGION_DENY_FLOOD; // admin: this region allows flood
@ -138,8 +211,14 @@ TEST(RegionGating, RuntimeGateIsSeparateFromConfigFlags) {
EXPECT_TRUE(eu->rt_flags & REGION_DENY_FLOOD);
EXPECT_FALSE(eu->flags & REGION_DENY_FLOOD); // config untouched
EXPECT_TRUE(eu->effectiveFlags() & REGION_DENY_FLOOD);
}
rm.applyDutyGate(0); // recover
TEST_F(NlChainGating, RecoveryRestoresConfiguredBehaviour) {
RegionEntry* eu = rm.findByName("eu");
eu->flags &= (uint8_t)~REGION_DENY_FLOOD;
rm.applyDutyGate(2);
rm.applyDutyGate(0);
EXPECT_FALSE(eu->rt_flags & REGION_DENY_FLOOD);
EXPECT_FALSE(eu->effectiveFlags() & REGION_DENY_FLOOD); // flood allowed again
}

Loading…
Cancel
Save