test: verify shadow traffic invariants

This commit is contained in:
zgw
2026-07-30 13:51:28 +08:00
parent 2cbaceada2
commit 692f778120
2 changed files with 134 additions and 3 deletions

View File

@@ -0,0 +1,108 @@
package com.ft546.wcs.shadow;
import com.ft546.wcs.shadow.decision.DecisionType;
import com.ft546.wcs.shadow.decision.RollingDispatchPolicy;
import com.ft546.wcs.shadow.decision.ShadowCandidate;
import com.ft546.wcs.shadow.decision.ShadowDecision;
import com.ft546.wcs.shadow.decision.WaitForGraph;
import com.ft546.wcs.shadow.map.EffectiveTrafficMap;
import com.ft546.wcs.shadow.map.TrafficMapCompiler;
import com.ft546.wcs.shadow.resource.ResourceKey;
import com.ft546.wcs.shadow.resource.ResourceLedger;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ShadowInvariantPropertyTest {
@Test
void fixedSeedsPreserveCapacityAcyclicityAndDeterminism() {
for (long seed = 546001L; seed <= 546010L; seed++) {
verifySeed(seed);
}
}
private void verifySeed(long seed) {
Random random = new Random(seed);
EffectiveTrafficMap map = map();
int vehicleCount = 2 + random.nextInt(7);
List<ShadowCandidate> candidates = new ArrayList<ShadowCandidate>();
for (int index = 0; index < vehicleCount; index++) {
String site = "N" + random.nextInt(4);
candidates.add(ShadowCandidate.waiting(
"T" + index, "V" + index, false, random.nextInt(5),
random.nextInt(100), random.nextInt(10),
Collections.singleton(ResourceKey.node(site)), null));
}
RollingDispatchPolicy policy = new RollingDispatchPolicy();
List<ShadowDecision> first =
policy.decide(candidates, new ResourceLedger(map.getCapacities()), map);
List<ShadowDecision> second =
policy.decide(candidates, new ResourceLedger(map.getCapacities()), map);
assertEquals(signature(first), signature(second),
"seed=" + seed + " mapVersion=" + map.getVersionId() + " candidates=" + vehicleCount);
ResourceLedger accepted = new ResourceLedger(map.getCapacities());
for (ShadowDecision decision : first) {
if (decision.getType() == DecisionType.GRANT) {
ShadowCandidate candidate = find(candidates, decision.getVehicleId());
assertTrue(accepted.evaluate(candidate.getVehicleId(), candidate.getResources()).isGranted(),
"seed=" + seed + " duplicate grant for " + candidate.getResources());
}
}
for (String site : Arrays.asList("N0", "N1", "N2", "N3")) {
assertTrue(accepted.protectedCount(ResourceKey.node(site)) <= 1,
"seed=" + seed + " capacity exceeded at " + site);
}
WaitForGraph graph = new WaitForGraph();
for (int index = 0; index < vehicleCount; index++) {
String waiter = "V" + index;
String holder = "V" + random.nextInt(vehicleCount);
if (!waiter.equals(holder)
&& !graph.wouldCreateCycle(waiter, Collections.singleton(holder))) {
graph.addWait(waiter, Collections.singleton(holder));
}
assertTrue(graph.isAcyclic(), "seed=" + seed + " wait graph contains a cycle");
}
}
private ShadowCandidate find(List<ShadowCandidate> candidates, String vehicleId) {
for (ShadowCandidate candidate : candidates) {
if (candidate.getVehicleId().equals(vehicleId)) return candidate;
}
throw new IllegalStateException("candidate not found " + vehicleId);
}
private List<String> signature(List<ShadowDecision> decisions) {
List<String> result = new ArrayList<String>();
for (ShadowDecision decision : decisions) {
result.add(decision.getVehicleId() + "|" + decision.getType() + "|" + decision.getReasonCode());
}
return result;
}
private EffectiveTrafficMap map() {
LinkedHashSet<String> nodes =
new LinkedHashSet<String>(Arrays.asList("N0", "N1", "N2", "N3"));
LinkedHashSet<TrafficMapCompiler.EdgeDefinition> edges =
new LinkedHashSet<TrafficMapCompiler.EdgeDefinition>();
edges.add(new TrafficMapCompiler.EdgeDefinition("N0", "N1"));
edges.add(new TrafficMapCompiler.EdgeDefinition("N1", "N2"));
edges.add(new TrafficMapCompiler.EdgeDefinition("N2", "N3"));
return new TrafficMapCompiler().compile(new TrafficMapCompiler.MapDefinition(
"FT546", 2L, "ZONE-PROPERTY", true, "sha", "sha", nodes, edges,
new LinkedHashMap<ResourceKey, Integer>(), Collections.singleton("N0"),
Collections.singleton("N3")));
}
}