test: verify shadow traffic invariants
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# FT546 WCS 中心调度与交通管制 Demo
|
||||
|
||||
这是一个独立的 Java 8 Maven Demo,用于模拟 FT546 WCS 的 LEGACY 中心调度和交通管制行为。
|
||||
这是一个独立的 Java 8 Maven Demo。它保留 FT546 WCS 的 LEGACY 调度模拟,并旁路提供 P0-B1 离线 SHADOW 交通决策内核。
|
||||
|
||||
## 安全边界
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
- 不启动 Spring,不连接 SQL Server、PLC 或 WMS。
|
||||
- 不发送真实设备命令。
|
||||
- 不修改生产 `ruoyi-*` 模块。
|
||||
- 不启用 SHADOW 或 ACTIVE 控制模式。
|
||||
- SHADOW 仅在离线内存中计算、记录和回放,写端口会直接拒绝调用。
|
||||
- 不启用现场 SHADOW、HIL、LIMITED_ACTIVE、ACTIVE 或 PRODUCTION。
|
||||
- 不把离线模拟结果作为现场证据。
|
||||
|
||||
## 已覆盖行为
|
||||
|
||||
@@ -21,10 +23,26 @@
|
||||
- 队列 `0/1 → 1/3 → 1/4` 状态流转、完成归档和占用释放。
|
||||
- 无路可达时返回 HOLD,而不是抛出空指针。
|
||||
|
||||
## P0-B1 离线 SHADOW 覆盖
|
||||
|
||||
- 带版本、区域确认和基础地图校验和的不可变交通地图。
|
||||
- `NODE`、规范化无向 `EDGE` 和显式 `CONFLICT` 资源及容量。
|
||||
- `SOFT/COMMIT_GUARD/HARD/UNKNOWN_HARD/RELEASED` 资源保护契约。
|
||||
- 区段 Prepare/Commit 状态机及非法跳转拒绝。
|
||||
- 带 `plcBootId/heartbeatSeq/snapshotSeq` 的不可变复合快照。
|
||||
- 陈旧、序号倒退、PLC 重启、失联和人工模式的 fail-closed 检查。
|
||||
- 确定性 `GRANT/HOLD/YIELD`、等待图环拒绝及安全让行点门禁。
|
||||
- 匹配反馈逐步释放;重复、跳点、版本错配和 ACK 未知不释放。
|
||||
- SHADOW PLC、业务状态和权威预约三类禁写端口。
|
||||
- JSON 黄金决策事件、SHA-256 输入哈希和离线回放。
|
||||
- 固定种子 `546001` 至 `546010` 的容量、无环和确定性不变量回归。
|
||||
|
||||
`com.ft546.wcs.demo` 是 LEGACY 演示;新增能力位于 `com.ft546.wcs.shadow`,交通区段状态不会写入 LEGACY 业务队列。
|
||||
|
||||
## 运行测试
|
||||
|
||||
```bash
|
||||
mvn test
|
||||
mvn clean test
|
||||
```
|
||||
|
||||
测试必须由 Maven Surefire 实际发现并执行;验收标准为测试数大于 0、Failures 为 0、Errors 为 0。
|
||||
@@ -46,3 +64,8 @@ mvn exec:java
|
||||
- `MOVE`:模拟车辆推进
|
||||
- `COMPLETE`:队列完成并归档
|
||||
- `SUMMARY`:最终周期、活动队列、历史和协议尝试次数
|
||||
- `SHADOW-SUMMARY`:离线 SHADOW 的 GRANT/HOLD 数量和零写入声明
|
||||
|
||||
## 未覆盖的生产门槛
|
||||
|
||||
本项目不包含 SQL Server 事务与 Outbox、真实 PLC Prepare/Commit、CRC 与寄存器布局、提升机物理信号、深位真实执行、硬件在环、现场 SHADOW 样本或区域 ACTIVE 回退。相关能力仍以 `docs/WCS算法优化` 的外部签字和验收门禁为准。
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user