feat(wcs): publish immutable warehouse configuration

This commit is contained in:
zgw
2026-08-10 15:55:59 +08:00
parent 81d96a671c
commit 67e47bbdc5
15 changed files with 916 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
package com.ruoyi.wcs.application.configuration;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
import com.ruoyi.wcs.port.in.ConfigurationUseCase;
import com.ruoyi.wcs.port.out.ConfigurationRepository;
import com.ruoyi.wcs.port.out.TopologyDefinitionProvider;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
public final class ConfigurationApplicationService implements ConfigurationUseCase {
private final TopologyDefinitionProvider definitions;
private final TopologyCompiler compiler;
private final ConfigurationRepository repository;
public ConfigurationApplicationService(TopologyDefinitionProvider definitions,
TopologyCompiler compiler,
ConfigurationRepository repository) {
this.definitions = Objects.requireNonNull(definitions, "definitions");
this.compiler = Objects.requireNonNull(compiler, "compiler");
this.repository = Objects.requireNonNull(repository, "repository");
}
@Override
public void validate(PublishConfigurationCommand command) {
compile(command);
}
@Override
@Transactional
public ConfigurationPublicationResult publish(PublishConfigurationCommand command) {
EffectiveTopology topology = compile(command);
PublishedConfigurationView active = repository.findActive(command.getWarehouseCode());
if (active != null && active.getTopologyVersion() != command.getVersion()
&& repository.hasActiveOrderReferences(command.getWarehouseCode(),
active.getTopologyVersion())) {
throw new DomainRuleViolation("active orders reference configuration version "
+ active.getTopologyVersion());
}
repository.publish(command, topology);
return new ConfigurationPublicationResult(command.getWarehouseCode(),
command.getVersion());
}
@Override
public PublishedConfigurationView read(String warehouseCode, long version) {
PublishedConfigurationView view = repository.find(
requireText(warehouseCode, "warehouseCode"), version);
if (view == null) {
throw new DomainRuleViolation("configuration version not found: " + version);
}
return view;
}
@Override
public PublishedConfigurationView readActive(String warehouseCode) {
PublishedConfigurationView view = repository.findActive(
requireText(warehouseCode, "warehouseCode"));
if (view == null) {
throw new DomainRuleViolation("active configuration not found");
}
return view;
}
private EffectiveTopology compile(PublishConfigurationCommand command) {
Objects.requireNonNull(command, "command");
TopologyDefinition definition = definitions.load(
command.getWarehouseCode(), command.getVersion());
if (definition == null) {
throw new DomainRuleViolation("topology definition not found");
}
if (!command.getWarehouseCode().equals(definition.getWarehouseCode())
|| command.getVersion() != definition.getVersion()) {
throw new DomainRuleViolation("topology definition identity mismatch");
}
return compiler.compile(definition);
}
private static String requireText(String value, String field) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.trim();
}
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.wcs.application.configuration;
public final class ConfigurationPublicationResult {
private final String warehouseCode;
private final long publishedVersion;
public ConfigurationPublicationResult(String warehouseCode, long publishedVersion) {
this.warehouseCode = warehouseCode;
this.publishedVersion = publishedVersion;
}
public String getWarehouseCode() {
return warehouseCode;
}
public long getPublishedVersion() {
return publishedVersion;
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.wcs.application.configuration;
public final class PublishConfigurationCommand {
private final String warehouseCode;
private final long version;
public PublishConfigurationCommand(String warehouseCode, long version) {
if (warehouseCode == null || warehouseCode.trim().isEmpty()) {
throw new IllegalArgumentException("warehouseCode must not be blank");
}
this.warehouseCode = warehouseCode.trim();
this.version = version;
}
public String getWarehouseCode() {
return warehouseCode;
}
public long getVersion() {
return version;
}
}

View File

@@ -0,0 +1,52 @@
package com.ruoyi.wcs.application.configuration;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import java.util.Objects;
public final class PublishedConfigurationView {
private final String warehouseCode;
private final long topologyVersion;
private final long deviceConfigurationVersion;
private final long workflowConfigurationVersion;
private final EffectiveTopology topology;
private PublishedConfigurationView(String warehouseCode,
long topologyVersion,
long deviceConfigurationVersion,
long workflowConfigurationVersion,
EffectiveTopology topology) {
this.warehouseCode = warehouseCode;
this.topologyVersion = topologyVersion;
this.deviceConfigurationVersion = deviceConfigurationVersion;
this.workflowConfigurationVersion = workflowConfigurationVersion;
this.topology = topology;
}
public static PublishedConfigurationView from(EffectiveTopology topology) {
Objects.requireNonNull(topology, "topology");
long version = topology.version();
return new PublishedConfigurationView(topology.getWarehouseCode(),
version, version, version, topology);
}
public String getWarehouseCode() {
return warehouseCode;
}
public long getTopologyVersion() {
return topologyVersion;
}
public long getDeviceConfigurationVersion() {
return deviceConfigurationVersion;
}
public long getWorkflowConfigurationVersion() {
return workflowConfigurationVersion;
}
public EffectiveTopology getTopology() {
return topology;
}
}

View File

@@ -0,0 +1,71 @@
package com.ruoyi.wcs.domain.topology;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class EffectiveTopology {
private final String warehouseCode;
private final long version;
private final Map<String, TopologyNode> nodes;
private final Map<String, TopologyEdge> edges;
private final Map<String, ResourceDefinition> resources;
private final Map<String, List<String>> neighbors;
EffectiveTopology(String warehouseCode, long version,
Map<String, TopologyNode> nodes,
Map<String, TopologyEdge> edges,
Map<String, ResourceDefinition> resources,
Map<String, List<String>> neighbors) {
this.warehouseCode = warehouseCode;
this.version = version;
this.nodes = Collections.unmodifiableMap(
new LinkedHashMap<String, TopologyNode>(nodes));
this.edges = Collections.unmodifiableMap(
new LinkedHashMap<String, TopologyEdge>(edges));
this.resources = Collections.unmodifiableMap(
new LinkedHashMap<String, ResourceDefinition>(resources));
Map<String, List<String>> copiedNeighbors =
new LinkedHashMap<String, List<String>>();
for (Map.Entry<String, List<String>> entry : neighbors.entrySet()) {
copiedNeighbors.put(entry.getKey(), Collections.unmodifiableList(
new ArrayList<String>(entry.getValue())));
}
this.neighbors = Collections.unmodifiableMap(copiedNeighbors);
}
public String getWarehouseCode() {
return warehouseCode;
}
public long version() {
return version;
}
public boolean containsNode(String nodeId) {
return nodes.containsKey(nodeId);
}
public TopologyNode node(String nodeId) {
return nodes.get(nodeId);
}
public List<String> neighbors(String nodeId) {
List<String> result = neighbors.get(nodeId);
return result == null ? Collections.<String>emptyList() : result;
}
public TopologyEdge edge(String sourceNodeId, String targetNodeId) {
return edges.get(TopologyEdge.canonicalResourceKey(sourceNodeId, targetNodeId));
}
public ResourceDefinition resource(String resourceKey) {
return resources.get(resourceKey);
}
public Map<String, ResourceDefinition> getResources() {
return resources;
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.wcs.domain.topology;
public final class ResourceDefinition {
private final String resourceKey;
private final int capacity;
public ResourceDefinition(String resourceKey, int capacity) {
if (resourceKey == null || resourceKey.trim().isEmpty()) {
throw new IllegalArgumentException("resourceKey must not be blank");
}
this.resourceKey = resourceKey.trim();
this.capacity = capacity;
}
public String getResourceKey() {
return resourceKey;
}
public int getCapacity() {
return capacity;
}
}

View File

@@ -0,0 +1,175 @@
package com.ruoyi.wcs.domain.topology;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class TopologyCompiler {
public EffectiveTopology compile(TopologyDefinition definition) {
if (definition == null) {
throw new DomainRuleViolation("topology definition is required");
}
if (!definition.isConfirmed()) {
throw new DomainRuleViolation("topology configuration is not confirmed");
}
if (definition.getVersion() <= 0L) {
throw new DomainRuleViolation("topology version must be positive");
}
if (definition.getNodes().isEmpty()) {
throw new DomainRuleViolation("topology must contain at least one node");
}
Map<String, TopologyNode> nodes = uniqueNodes(definition.getNodes());
Map<String, List<String>> neighbors = emptyNeighbors(nodes);
Map<String, ResourceDefinition> resources = nodeResources(nodes);
Map<String, TopologyEdge> edges = compileEdges(
definition.getEdges(), nodes, neighbors, resources);
validatePorts("lift", definition.getLiftPortNodeIds(), nodes);
validatePorts("conveyor", definition.getConveyorPortNodeIds(), nodes);
validateSafeWaitNodes(nodes, neighbors);
applyExplicitResources(definition.getResources(), resources);
for (List<String> adjacent : neighbors.values()) {
Collections.sort(adjacent);
}
return new EffectiveTopology(definition.getWarehouseCode(),
definition.getVersion(), nodes, edges, resources, neighbors);
}
private static Map<String, TopologyNode> uniqueNodes(List<TopologyNode> definitions) {
Map<String, TopologyNode> nodes = new LinkedHashMap<String, TopologyNode>();
Set<String> coordinates = new HashSet<String>();
for (TopologyNode node : definitions) {
if (node == null) {
throw new DomainRuleViolation("topology node must not be null");
}
if (node.getCapacity() <= 0) {
throw new DomainRuleViolation("node capacity must be positive: "
+ node.getNodeId());
}
if (nodes.put(node.getNodeId(), node) != null) {
throw new DomainRuleViolation("duplicate topology node: "
+ node.getNodeId());
}
String coordinate = node.getFloorCode() + ":" + node.getX() + ":" + node.getY();
if (!coordinates.add(coordinate)) {
throw new DomainRuleViolation("duplicate topology coordinate: " + coordinate);
}
}
return nodes;
}
private static Map<String, List<String>> emptyNeighbors(
Map<String, TopologyNode> nodes) {
Map<String, List<String>> neighbors =
new LinkedHashMap<String, List<String>>();
for (String nodeId : nodes.keySet()) {
neighbors.put(nodeId, new ArrayList<String>());
}
return neighbors;
}
private static Map<String, ResourceDefinition> nodeResources(
Map<String, TopologyNode> nodes) {
Map<String, ResourceDefinition> resources =
new LinkedHashMap<String, ResourceDefinition>();
for (TopologyNode node : nodes.values()) {
ResourceDefinition resource = new ResourceDefinition(
"NODE:" + node.getNodeId(), node.getCapacity());
resources.put(resource.getResourceKey(), resource);
}
return resources;
}
private static Map<String, TopologyEdge> compileEdges(
List<TopologyEdge> definitions,
Map<String, TopologyNode> nodes,
Map<String, List<String>> neighbors,
Map<String, ResourceDefinition> resources) {
Map<String, TopologyEdge> edges = new LinkedHashMap<String, TopologyEdge>();
for (TopologyEdge edge : definitions) {
if (edge == null) {
throw new DomainRuleViolation("topology edge must not be null");
}
if (!nodes.containsKey(edge.getSourceNodeId())
|| !nodes.containsKey(edge.getTargetNodeId())) {
throw new DomainRuleViolation("edge references unknown node: "
+ edge.getResourceKey());
}
if (edge.getSourceNodeId().equals(edge.getTargetNodeId())) {
throw new DomainRuleViolation("self edge is not allowed: "
+ edge.getSourceNodeId());
}
if (edge.getDistanceCost() <= 0 || edge.getCapacity() <= 0) {
throw new DomainRuleViolation("edge distance and capacity must be positive: "
+ edge.getResourceKey());
}
if (edges.put(edge.getResourceKey(), edge) != null) {
throw new DomainRuleViolation("duplicate topology edge: "
+ edge.getResourceKey());
}
neighbors.get(edge.getSourceNodeId()).add(edge.getTargetNodeId());
neighbors.get(edge.getTargetNodeId()).add(edge.getSourceNodeId());
resources.put(edge.getResourceKey(), new ResourceDefinition(
edge.getResourceKey(), edge.getCapacity()));
}
return edges;
}
private static void validatePorts(String type, List<String> portNodeIds,
Map<String, TopologyNode> nodes) {
Set<String> seen = new HashSet<String>();
for (String nodeId : portNodeIds) {
if (nodeId == null || !nodes.containsKey(nodeId)) {
throw new DomainRuleViolation(type
+ " port references unknown node: " + nodeId);
}
if (!seen.add(nodeId)) {
throw new DomainRuleViolation("duplicate " + type + " port: " + nodeId);
}
}
}
private static void validateSafeWaitNodes(Map<String, TopologyNode> nodes,
Map<String, List<String>> neighbors) {
for (TopologyNode node : nodes.values()) {
if (node.isSafeWait() && neighbors.get(node.getNodeId()).size() <= 1) {
throw new DomainRuleViolation("safe wait node is a unique exit: "
+ node.getNodeId());
}
}
}
private static void applyExplicitResources(
List<ResourceDefinition> definitions,
Map<String, ResourceDefinition> resources) {
Set<String> explicitKeys = new HashSet<String>();
for (ResourceDefinition resource : definitions) {
if (resource == null) {
throw new DomainRuleViolation("resource definition must not be null");
}
if (resource.getCapacity() <= 0) {
throw new DomainRuleViolation("resource capacity must be positive: "
+ resource.getResourceKey());
}
if (!explicitKeys.add(resource.getResourceKey())) {
throw new DomainRuleViolation("duplicate resource definition: "
+ resource.getResourceKey());
}
if (!resource.getResourceKey().startsWith("CONFLICT:")
&& !resources.containsKey(resource.getResourceKey())) {
throw new DomainRuleViolation("resource references unknown topology item: "
+ resource.getResourceKey());
}
resources.put(resource.getResourceKey(), resource);
}
}
}

View File

@@ -0,0 +1,74 @@
package com.ruoyi.wcs.domain.topology;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class TopologyDefinition {
private final String warehouseCode;
private final long version;
private final boolean confirmed;
private final List<TopologyNode> nodes;
private final List<TopologyEdge> edges;
private final List<ResourceDefinition> resources;
private final List<String> liftPortNodeIds;
private final List<String> conveyorPortNodeIds;
public TopologyDefinition(String warehouseCode, long version, boolean confirmed,
List<TopologyNode> nodes, List<TopologyEdge> edges,
List<ResourceDefinition> resources,
List<String> liftPortNodeIds,
List<String> conveyorPortNodeIds) {
if (warehouseCode == null || warehouseCode.trim().isEmpty()) {
throw new IllegalArgumentException("warehouseCode must not be blank");
}
this.warehouseCode = warehouseCode.trim();
this.version = version;
this.confirmed = confirmed;
this.nodes = immutable(nodes, "nodes");
this.edges = immutable(edges, "edges");
this.resources = immutable(resources, "resources");
this.liftPortNodeIds = immutable(liftPortNodeIds, "liftPortNodeIds");
this.conveyorPortNodeIds = immutable(conveyorPortNodeIds,
"conveyorPortNodeIds");
}
public String getWarehouseCode() {
return warehouseCode;
}
public long getVersion() {
return version;
}
public boolean isConfirmed() {
return confirmed;
}
public List<TopologyNode> getNodes() {
return nodes;
}
public List<TopologyEdge> getEdges() {
return edges;
}
public List<ResourceDefinition> getResources() {
return resources;
}
public List<String> getLiftPortNodeIds() {
return liftPortNodeIds;
}
public List<String> getConveyorPortNodeIds() {
return conveyorPortNodeIds;
}
private static <T> List<T> immutable(List<T> source, String field) {
if (source == null) {
throw new IllegalArgumentException(field + " must not be null");
}
return Collections.unmodifiableList(new ArrayList<T>(source));
}
}

View File

@@ -0,0 +1,63 @@
package com.ruoyi.wcs.domain.topology;
public final class TopologyEdge {
private final String sourceNodeId;
private final String targetNodeId;
private final int distanceCost;
private final int capacity;
private final String resourceKey;
public TopologyEdge(String sourceNodeId, String targetNodeId,
int distanceCost, int capacity) {
this.sourceNodeId = requireText(sourceNodeId, "sourceNodeId");
this.targetNodeId = requireText(targetNodeId, "targetNodeId");
this.distanceCost = distanceCost;
this.capacity = capacity;
this.resourceKey = canonicalResourceKey(this.sourceNodeId, this.targetNodeId);
}
public String getSourceNodeId() {
return sourceNodeId;
}
public String getTargetNodeId() {
return targetNodeId;
}
public int getDistanceCost() {
return distanceCost;
}
public int getCapacity() {
return capacity;
}
public String getResourceKey() {
return resourceKey;
}
public String other(String nodeId) {
if (sourceNodeId.equals(nodeId)) {
return targetNodeId;
}
if (targetNodeId.equals(nodeId)) {
return sourceNodeId;
}
throw new IllegalArgumentException("node is not an endpoint: " + nodeId);
}
public static String canonicalResourceKey(String first, String second) {
String a = requireText(first, "first");
String b = requireText(second, "second");
return a.compareTo(b) <= 0
? "EDGE:" + a + ":" + b
: "EDGE:" + b + ":" + a;
}
private static String requireText(String value, String field) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.trim();
}
}

View File

@@ -0,0 +1,57 @@
package com.ruoyi.wcs.domain.topology;
public final class TopologyNode {
private final String nodeId;
private final String floorCode;
private final int x;
private final int y;
private final int capacity;
private final boolean safeWait;
private final boolean safeYield;
public TopologyNode(String nodeId, String floorCode, int x, int y,
int capacity, boolean safeWait, boolean safeYield) {
this.nodeId = requireText(nodeId, "nodeId");
this.floorCode = requireText(floorCode, "floorCode");
this.x = x;
this.y = y;
this.capacity = capacity;
this.safeWait = safeWait;
this.safeYield = safeYield;
}
public String getNodeId() {
return nodeId;
}
public String getFloorCode() {
return floorCode;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getCapacity() {
return capacity;
}
public boolean isSafeWait() {
return safeWait;
}
public boolean isSafeYield() {
return safeYield;
}
private static String requireText(String value, String field) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return value.trim();
}
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.wcs.port.in;
import com.ruoyi.wcs.application.configuration.ConfigurationPublicationResult;
import com.ruoyi.wcs.application.configuration.PublishConfigurationCommand;
import com.ruoyi.wcs.application.configuration.PublishedConfigurationView;
public interface ConfigurationUseCase {
void validate(PublishConfigurationCommand command);
ConfigurationPublicationResult publish(PublishConfigurationCommand command);
PublishedConfigurationView read(String warehouseCode, long version);
PublishedConfigurationView readActive(String warehouseCode);
}

View File

@@ -0,0 +1,15 @@
package com.ruoyi.wcs.port.out;
import com.ruoyi.wcs.application.configuration.PublishConfigurationCommand;
import com.ruoyi.wcs.application.configuration.PublishedConfigurationView;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
public interface ConfigurationRepository {
void publish(PublishConfigurationCommand command, EffectiveTopology topology);
PublishedConfigurationView find(String warehouseCode, long version);
PublishedConfigurationView findActive(String warehouseCode);
boolean hasActiveOrderReferences(String warehouseCode, long version);
}

View File

@@ -0,0 +1,7 @@
package com.ruoyi.wcs.port.out;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
public interface TopologyDefinitionProvider {
TopologyDefinition load(String warehouseCode, long version);
}

View File

@@ -0,0 +1,140 @@
package com.ruoyi.wcs.application.configuration;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import com.ruoyi.wcs.domain.topology.EffectiveTopology;
import com.ruoyi.wcs.domain.topology.ResourceDefinition;
import com.ruoyi.wcs.domain.topology.TopologyDefinition;
import com.ruoyi.wcs.domain.topology.TopologyEdge;
import com.ruoyi.wcs.domain.topology.TopologyNode;
import com.ruoyi.wcs.domain.topology.TopologyCompiler;
import com.ruoyi.wcs.port.out.ConfigurationRepository;
import com.ruoyi.wcs.port.out.TopologyDefinitionProvider;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class ConfigurationApplicationServiceTest {
@Test
void publishesOneImmutableConfigurationVersionAtomically() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
ConfigurationPublicationResult result = service.publish(
new PublishConfigurationCommand("FT546", 7L));
assertEquals(7L, result.getPublishedVersion());
assertEquals(1, repository.publishCount);
assertEquals(0, repository.partialWriteCount);
assertEquals(7L, service.readActive("FT546").getTopologyVersion());
}
@Test
void readsOneInternallyConsistentPublishedVersion() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
service.publish(new PublishConfigurationCommand("FT546", 7L));
PublishedConfigurationView view = service.read("FT546", 7L);
assertEquals(7L, view.getTopologyVersion());
assertEquals(7L, view.getDeviceConfigurationVersion());
assertEquals(7L, view.getWorkflowConfigurationVersion());
}
@Test
void refusesToReplaceVersionReferencedByAnActiveOrder() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
Map<Long, TopologyDefinition> definitions = new LinkedHashMap<Long, TopologyDefinition>();
definitions.put(7L, definition(7L));
definitions.put(8L, definition(8L));
ConfigurationApplicationService service = service(repository,
(warehouseCode, version) -> definitions.get(version));
service.publish(new PublishConfigurationCommand("FT546", 7L));
repository.activeReferences = true;
DomainRuleViolation error = assertThrows(DomainRuleViolation.class,
() -> service.publish(new PublishConfigurationCommand("FT546", 8L)));
assertEquals("active orders reference configuration version 7", error.getMessage());
assertEquals(1, repository.publishCount);
assertEquals(7L, service.readActive("FT546").getTopologyVersion());
}
@Test
void validationNeverWritesRepositoryState() {
InMemoryConfigurationRepository repository = new InMemoryConfigurationRepository();
ConfigurationApplicationService service = service(repository,
definition(7L));
service.validate(new PublishConfigurationCommand("FT546", 7L));
assertEquals(0, repository.publishCount);
}
private static ConfigurationApplicationService service(
InMemoryConfigurationRepository repository,
TopologyDefinition definition) {
return service(repository, (warehouseCode, version) -> definition);
}
private static ConfigurationApplicationService service(
InMemoryConfigurationRepository repository,
TopologyDefinitionProvider provider) {
return new ConfigurationApplicationService(provider,
new TopologyCompiler(), repository);
}
private static TopologyDefinition definition(long version) {
return new TopologyDefinition("FT546", version, true,
Arrays.asList(
new TopologyNode("A", "F1", 0, 0, 1, false, false),
new TopologyNode("B", "F1", 1, 0, 1, false, false)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
}
private static final class InMemoryConfigurationRepository
implements ConfigurationRepository {
private final Map<Long, PublishedConfigurationView> versions =
new LinkedHashMap<Long, PublishedConfigurationView>();
private PublishedConfigurationView active;
private boolean activeReferences;
private int publishCount;
private int partialWriteCount;
@Override
public void publish(PublishConfigurationCommand command,
EffectiveTopology topology) {
PublishedConfigurationView view = PublishedConfigurationView.from(topology);
versions.put(command.getVersion(), view);
active = view;
publishCount++;
}
@Override
public PublishedConfigurationView find(String warehouseCode, long version) {
return versions.get(version);
}
@Override
public PublishedConfigurationView findActive(String warehouseCode) {
return active;
}
@Override
public boolean hasActiveOrderReferences(String warehouseCode, long version) {
return activeReferences && active != null
&& active.getTopologyVersion() == version;
}
}
}

View File

@@ -0,0 +1,96 @@
package com.ruoyi.wcs.domain.topology;
import com.ruoyi.wcs.domain.shared.DomainRuleViolation;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TopologyCompilerTest {
private final TopologyCompiler compiler = new TopologyCompiler();
@Test
void canonicalizesUndirectedEdgeResources() {
EffectiveTopology topology = compiler.compile(definition(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)),
Collections.singletonList(new TopologyEdge("B", "A", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList()));
assertEquals("EDGE:A:B", topology.edge("A", "B").getResourceKey());
assertEquals("EDGE:A:B", topology.edge("B", "A").getResourceKey());
assertEquals(Collections.singletonList("B"), topology.neighbors("A"));
assertEquals(1, topology.resource("NODE:A").getCapacity());
}
@Test
void rejectsDisconnectedLiftPortReference() {
TopologyDefinition definition = definition(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.singletonList("MISSING"), Collections.<String>emptyList());
DomainRuleViolation error = assertThrows(DomainRuleViolation.class,
() -> compiler.compile(definition));
assertEquals("lift port references unknown node: MISSING", error.getMessage());
}
@Test
void rejectsDuplicateCoordinatesOnTheSameFloor() {
TopologyDefinition definition = definition(
Arrays.asList(node("A", 0, 0), node("B", 0, 0)),
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList());
assertThrows(DomainRuleViolation.class, () -> compiler.compile(definition));
}
@Test
void rejectsUnconfirmedConfigurationAndNonPositiveCapacity() {
TopologyDefinition unconfirmed = new TopologyDefinition("FT546", 7L, false,
Collections.singletonList(node("A", 0, 0)),
Collections.<TopologyEdge>emptyList(),
Collections.<ResourceDefinition>emptyList(),
Collections.<String>emptyList(), Collections.<String>emptyList());
TopologyDefinition invalidCapacity = new TopologyDefinition("FT546", 7L, true,
Collections.singletonList(node("A", 0, 0)),
Collections.<TopologyEdge>emptyList(),
Collections.singletonList(new ResourceDefinition("NODE:A", 0)),
Collections.<String>emptyList(), Collections.<String>emptyList());
assertThrows(DomainRuleViolation.class, () -> compiler.compile(unconfirmed));
assertThrows(DomainRuleViolation.class, () -> compiler.compile(invalidCapacity));
}
@Test
void compiledTopologyDoesNotChangeWhenDefinitionCollectionsChange() {
List<TopologyNode> nodes = new ArrayList<TopologyNode>(
Arrays.asList(node("A", 0, 0), node("B", 1, 0)));
EffectiveTopology topology = compiler.compile(definition(nodes,
Collections.singletonList(new TopologyEdge("A", "B", 10, 1)),
Collections.<String>emptyList(), Collections.<String>emptyList()));
nodes.clear();
assertTrue(topology.containsNode("A"));
assertThrows(UnsupportedOperationException.class,
() -> topology.neighbors("A").add("C"));
}
private static TopologyDefinition definition(List<TopologyNode> nodes,
List<TopologyEdge> edges,
List<String> liftPorts,
List<String> conveyorPorts) {
return new TopologyDefinition("FT546", 7L, true, nodes, edges,
Collections.<ResourceDefinition>emptyList(), liftPorts, conveyorPorts);
}
private static TopologyNode node(String id, int x, int y) {
return new TopologyNode(id, "F1", x, y, 1, false, false);
}
}