diff --git a/vda5050_core/CMakeLists.txt b/vda5050_core/CMakeLists.txt index 2981a28..99f2ee3 100644 --- a/vda5050_core/CMakeLists.txt +++ b/vda5050_core/CMakeLists.txt @@ -34,7 +34,9 @@ target_include_directories(logger $ ) -add_library(client src/vda5050_core/state_manager/state_manager.cpp) +add_library(client + src/vda5050_core/state_manager/state_manager.cpp + src/vda5050_core/client/order/order_graph_validator.cpp) target_link_libraries(client vda5050_types::vda5050_types) target_include_directories(client @@ -79,7 +81,7 @@ install( ) ament_export_targets(export_vda5050_core HAS_LIBRARY_TARGET) -ament_export_dependencies(PahoMqttCpp fmt) +ament_export_dependencies(PahoMqttCpp fmt vda5050_types) if(BUILD_TESTING) find_package(ament_cmake_cppcheck REQUIRED) @@ -106,6 +108,7 @@ if(BUILD_TESTING) test/unit/mqtt_client/test_mqtt_client_interface.cpp test/unit/logger/test_logger.cpp test/unit/logger/test_default_logger.cpp + test/unit/order_graph_validator/test_order_graph_validator.cpp test/unit/state_manager/test_state_manager.cpp test/integration/mqtt_client/test_paho_mqtt_client.cpp ) diff --git a/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp b/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp new file mode 100644 index 0000000..8a517f1 --- /dev/null +++ b/vda5050_core/include/vda5050_core/client/order/order_graph_validator.hpp @@ -0,0 +1,63 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ +#define VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ + +#include + +#include "vda5050_core/client/order/validation_result.hpp" +#include "vda5050_types/order.hpp" + +namespace vda5050_core { +namespace order { + +/// \brief Utility class with functions to perform validity checks on the graph +/// contained in a VDA5050 Order message +class OrderGraphValidator +{ +public: + OrderGraphValidator() = delete; + + /// \brief Checks that the nodes and edges in a VDA5050 Order form a valid + /// graph according to the VDA5050 specification sheet. + /// + /// \param order The order to be checked. + /// \return ValidationResult containing if the order being checked is valid, + /// and any errors if it is not. + /// + /// \return True if nodes and edges create a valid graph, false otherwise + static ValidationResult is_valid_graph( + const vda5050_types::Order& order) noexcept(false); + + /// \brief Checks if order update is valid for order stitching + /// + /// \param base_order The base order. + /// + /// \param next_order the update order. + /// + /// \return True new order is valid for stitching, false otherwise + static ValidationResult is_valid_order_update( + const vda5050_types::Order& base_order, + const vda5050_types::Order& next_order) noexcept(false); +}; + +} // namespace order +} // namespace vda5050_core + +#endif // VDA5050_CORE__CLIENT__ORDER__ORDER_GRAPH_VALIDATOR_HPP_ diff --git a/vda5050_core/include/vda5050_core/client/order/validation_result.hpp b/vda5050_core/include/vda5050_core/client/order/validation_result.hpp new file mode 100644 index 0000000..8701da0 --- /dev/null +++ b/vda5050_core/include/vda5050_core/client/order/validation_result.hpp @@ -0,0 +1,48 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ +#define VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ + +#include + +#include "vda5050_types/error.hpp" + +namespace vda5050_core { +namespace order { + +/// \brief Struct that details the validity of an order +struct ValidationResult +{ + /// \brief A vector of error(s) that resulted in an invalid order. Empty if + /// order is valid. + std::vector errors; + + /// \brief Allows use in boolean contexts + /// + /// \return True if the order is valid, false otherwise. + explicit operator bool() const + { + return errors.empty(); + } +}; + +} // namespace order +} // namespace vda5050_core + +#endif // VDA5050_CORE__CLIENT__ORDER__VALIDATION_RESULT_HPP_ diff --git a/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp b/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp new file mode 100644 index 0000000..a8da929 --- /dev/null +++ b/vda5050_core/src/vda5050_core/client/order/order_graph_validator.cpp @@ -0,0 +1,285 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "vda5050_core/client/order/order_graph_validator.hpp" +#include "vda5050_core/logger/logger.hpp" + +namespace vda5050_core { +namespace order { + +//============================================================================= +ValidationResult OrderGraphValidator::is_valid_graph( + const vda5050_types::Order& order) noexcept(false) +{ + ValidationResult res; + const std::string order_id = order.order_id; + const std::string update_id = std::to_string(order.order_update_id); + + auto add_error = + [&]( + const std::string& msg, + const std::vector& refs = {}) { + std::vector all_refs = { + {"order.order_id", order_id}, {"order.order_update_id", update_id}}; + all_refs.insert(all_refs.end(), refs.begin(), refs.end()); + VDA5050_ERROR("Order Validation Error: {}", msg); + res.errors.push_back(vda5050_types::Error{ + "Order Validation Error", std::move(all_refs), msg, + vda5050_types::ErrorLevel::WARNING}); + }; + + // check if there are exusting nodes, stop if empty + if (order.nodes.empty()) + { + add_error("order does not have any nodes!"); + return res; + } + + // check if number of order is n, and number of edge is n-1 + if (order.nodes.size() != order.edges.size() + 1) + { + add_error( + "Invalid graph. Found " + std::to_string(order.nodes.size()) + + " nodes and " + std::to_string(order.edges.size()) + + " edges. Expected exactly " + std::to_string(order.nodes.size() - 1) + + " edges."); + return res; + } + + // check if order_update_id is 0, (means first update of its order) + // reject if first node sequence id is > 0 + if (order.order_update_id == 0 && order.nodes.front().sequence_id != 0) + { + add_error( + "Initial order (update_id=0) must start with sequence id 0, but found " + + std::to_string(order.nodes.front().sequence_id), + {{"node.node_id", order.nodes.front().node_id}, + {"node.sequence_id", std::to_string(order.nodes.front().sequence_id)}}); + return res; + } + + bool horizon_reached = false; + std::optional last_base_seq; + + // iterate through nodes and edges + for (size_t i(0u); i < order.nodes.size(); ++i) + { + const auto& curr_node = order.nodes[i]; + + // check if node sequence id is even + if (curr_node.sequence_id & 1u) + { + add_error( + "Order Node sequence contains an odd sequence id", + {{"node.sequence_id", std::to_string(curr_node.sequence_id)}}); + } + + // check if base/horizon separation + if (curr_node.released) + { + if (horizon_reached) + add_error( + "Order contains a base sequence id after a horizon sequence id"); + last_base_seq = curr_node.sequence_id; + } + else + { + horizon_reached = true; + } + + // validate edges + // check an edge if curr_node is not last + if (i < order.edges.size()) + { + const auto& edge = order.edges[i]; + + // continuity check: edge sequence_id must be node sequence_id + 1 + if (edge.sequence_id != curr_node.sequence_id + 1) + { + add_error( + "Missing sequence id or unsorted data. Expected edge " + + std::to_string(curr_node.sequence_id + 1) + " but found " + + std::to_string(edge.sequence_id)); + } + + // check if edge is odd + if (!(edge.sequence_id & 1u)) + { + add_error( + "Order Edge sequence contains an even sequence id", + {{"edge.sequence_id", std::to_string(edge.sequence_id)}}); + } + + // check if start node id of edge is the current node_id + if (edge.start_node_id != curr_node.node_id) + { + add_error( + "Edge start_node_id does not match the preceding node ID", + {{"edge.edge_id", edge.edge_id}, + {"node.node_id", curr_node.node_id}}); + } + + // check if end node id of edge is the next node_id + const auto& next_node = order.nodes[i + 1]; + + if (edge.end_node_id != next_node.node_id) + { + add_error( + "Edge end_node_id does not match the following node ID", + {{"edge.edge_id", edge.edge_id}, + {"next_node.node_id", next_node.node_id}}); + } + + // next node sequence_id must be Edge sequence_id + 1 + if (next_node.sequence_id != edge.sequence_id + 1) + { + add_error( + "Missing sequence id or unsorted data. Expected node " + + std::to_string(edge.sequence_id + 1) + " but found " + + std::to_string(next_node.sequence_id)); + } + + // edge base/horizon check, cannot have base (released node) after + // horizon (unreleased node) + if (edge.released) + { + if (horizon_reached) + add_error( + "Order contains a base sequence id after a horizon sequence id"); + last_base_seq = edge.sequence_id; + } + else + { + horizon_reached = true; + } + } + } + + // If the last thing released was edge, order is invalid. + if (last_base_seq && (*last_base_seq & 1u)) + { + add_error( + "The base (released graph) ends with an edge; it must end with a node."); + } + + return res; +} + +//============================================================================= +ValidationResult OrderGraphValidator::is_valid_order_update( + const vda5050_types::Order& base_order, + const vda5050_types::Order& next_order) noexcept(false) +{ + ValidationResult res; + + const std::string base_order_id = base_order.order_id; + const std::string base_order_update_id = + std::to_string(base_order.order_update_id); + + const std::string next_order_id = next_order.order_id; + const std::string next_order_update_id = + std::to_string(next_order.order_update_id); + + auto add_error = + [&]( + const std::string& msg, + const std::vector& refs = {}) { + std::vector all_refs = { + {"base_order.order_id", base_order_id}, + {"next_order.id", next_order_id}}; + all_refs.insert(all_refs.end(), refs.begin(), refs.end()); + VDA5050_ERROR("Order Validation Error: {}", msg); + res.errors.push_back(vda5050_types::Error{ + "Order Validation Error", std::move(all_refs), msg, + vda5050_types::ErrorLevel::WARNING}); + }; + // check validity of next order + ValidationResult next_res = OrderGraphValidator::is_valid_graph(next_order); + if (!next_res.errors.empty()) + { + // Append errors from the sub-validation to the result + res.errors.insert( + res.errors.end(), next_res.errors.begin(), next_res.errors.end()); + add_error("The incoming update order itself is invalid."); + return res; + } + // check validitiy of current order + ValidationResult base_res = OrderGraphValidator::is_valid_graph(base_order); + if (!base_res.errors.empty()) + { + add_error("The internal base order is invalid state. Cannot append."); + return res; + } + + // check if order id matches + if (base_order.order_id != next_order.order_id) + { + add_error( + "Order IDs do not match (" + base_order.order_id + " vs " + + next_order.order_id + ")"); + return res; + } + + // order update id must be increasing + if (next_order.order_update_id < base_order.order_update_id) + { + add_error( + "Update ID must be greater than base. Base: " + + std::to_string(base_order.order_update_id) + + ", Next: " + std::to_string(next_order.order_update_id)); + return res; + } + + // The first node of the order update corresponds + // to the last shared base node of the previous order message. + // @ VDA 5050 Version 2.1.0, January 2025 (Page 16) + + // find the last released node of base order + auto it = std::find_if( + base_order.nodes.rbegin(), base_order.nodes.rend(), + [](const auto& node) { return node.released; }); + + // check if there's a released node + // maybe put to is_valid_graph() + if (it == base_order.nodes.rend()) + { + add_error("Base order has no released nodes to stitch onto."); + return res; + } + + const auto& last_released_node = *it; + const auto& next_first_node = next_order.nodes.front(); + + // nnode must match (node_id and sequence_id) + if (last_released_node != next_first_node) + { + add_error( + "Graph Discontinuity: Base last released node is '" + + last_released_node.node_id + "' but Update starts at node '" + + next_first_node.node_id + "'"); + } + + return res; +} +//============================================================================= + +} // namespace order +} // namespace vda5050_core diff --git a/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp b/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp new file mode 100644 index 0000000..f84953a --- /dev/null +++ b/vda5050_core/test/unit/order_graph_validator/test_order_graph_validator.cpp @@ -0,0 +1,527 @@ +/** + * Copyright (C) 2025 ROS-Industrial Consortium Asia Pacific + * Advanced Remanufacturing and Technology Centre + * A*STAR Research Entities (Co. Registration No. 199702110H) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include "vda5050_core/client/order/order_graph_validator.hpp" +#include "vda5050_core/client/order/validation_result.hpp" + +class OrderGraphValidatorTest : public testing::Test +{ +protected: + vda5050_types::Node n0_{"node0", 0, true, {}, std::nullopt, std::nullopt}; + vda5050_types::Edge e1_{"edge1", 1, + "node0", "node2", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + vda5050_types::Node n2_{"node2", 2, true, {}, std::nullopt, std::nullopt}; + vda5050_types::Edge e3_{"edge3", 3, + "node2", "node4", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + vda5050_types::Node n4_{"node4", 4, true, {}, std::nullopt, std::nullopt}; + + vda5050_types::Order order_{}; + std::vector nodes; + std::vector edges; +}; + +//============================================================================= +/// \brief Tests that graph validator returns true on a valid graph +TEST_F(OrderGraphValidatorTest, ValidGraphTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + nodes.push_back(n4_); + + order_.order_id = "ValidGraphTest"; + order_.order_update_id = 0; + order_.edges = edges; + order_.nodes = nodes; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_TRUE(res); +} + +//============================================================================= +// /// \brief Tests that graph validator returns false when nodes and edges are +// not in traversal order +TEST_F(OrderGraphValidatorTest, NotInTraversalOrderTest) +{ + nodes.push_back(n0_); + edges.push_back(e3_); + nodes.push_back(n2_); + edges.push_back(e1_); + nodes.push_back(n4_); + + order_.order_id = "NotInTraversalOrderTest"; + order_.order_update_id = 0; + order_.edges = edges; + order_.nodes = nodes; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that graph validator returns false if there are more nodes +/// than edges +TEST_F(OrderGraphValidatorTest, MoreNodesThanEdgesTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + nodes.push_back(n4_); + + vda5050_types::Node n6{"node6", 6, true, {}, std::nullopt, std::nullopt}; + nodes.push_back(n6); + + order_.order_id = "MoreNodesThanEdgesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that validation fails if there are more edges +/// than nodes +TEST_F(OrderGraphValidatorTest, MoreEdgesThanNodesTest) +{ + nodes.push_back(n0_); + edges.push_back(e1_); + nodes.push_back(n2_); + edges.push_back(e3_); + + vda5050_types::Edge e5{"edge5", 5, + "node4", "node6", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + edges.push_back(e5); + + order_.order_id = "MoreEdgesThanNodesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that an edge in the right sequenceId order causes validation +/// to fail if its startNodeId and endNodeId do not match the nodeIds of +/// its neighbouring nodes +TEST_F(OrderGraphValidatorTest, ValidEdgesTest) +{ + nodes.push_back(n0_); + + e1_.start_node_id = "foo"; + e1_.end_node_id = "bar"; + edges.push_back(e1_); + + nodes.push_back(n2_); + + order_.order_id = "ValidEdgesTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that an order with odd node sequenceIds and even edge +/// sequenceIds causes validation to fail +TEST_F(OrderGraphValidatorTest, NodeWithOddSequenceIdTest) +{ + vda5050_types::Node odd_node1{"oddNode1", 1, true, {}, + std::nullopt, std::nullopt}; + nodes.push_back(odd_node1); + vda5050_types::Node odd_node2{"oddNode2", 3, true, {}, + std::nullopt, std::nullopt}; + nodes.push_back(odd_node2); + + vda5050_types::Edge even_edge{"evenEdge", 2, + "node4", "node6", + true, {}, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt, std::nullopt, + std::nullopt}; + edges.push_back(even_edge); + + order_.order_id = "NodeWithOddSequenceIdTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that no two nodes share the same sequenceId +TEST_F(OrderGraphValidatorTest, DuplicateNodeSequenceIdTest) +{ + n2_.sequence_id = 0; + + nodes.push_back(n0_); + nodes.push_back(n2_); + + edges.push_back(e1_); + + order_.order_id = "DuplicateNodeSequenceIdTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that there is only one base in the order +TEST_F(OrderGraphValidatorTest, MultipleBaseTest) +{ + /// n0_, e1_, n2_, and n4_ all released. Create a gap by setting e3_ to + /// unreleased. + e3_.released = false; + + nodes.push_back(n0_); + nodes.push_back(n2_); + nodes.push_back(n4_); + + edges.push_back(e1_); + edges.push_back(e3_); + + order_.order_id = "MultipleBaseTest"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_graph(order_); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief Tests that there is only one base in the order +TEST_F(OrderGraphValidatorTest, ValidOrderUpdate) +{ + // Base order: node0 -> node2 + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order continues from node2 -> node4 + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_TRUE(res); +} + +//============================================================================= +/// \brief test if base order is invalid +TEST_F(OrderGraphValidatorTest, InvalidBaseOrder) +{ + // Invalid base order: missing edge + nodes = {n0_, n2_}; + edges = {}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Valid next order + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if next order is invalid +TEST_F(OrderGraphValidatorTest, InvalidNextOrder) +{ + // Valid base order + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Invalid next order: wrong edge count + nodes = {n2_, n4_}; + edges = {}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for order id mismatch between base order and next order +TEST_F(OrderGraphValidatorTest, OrderIdMismatch) +{ + // Base order + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 0; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order with different order_id + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_id = "OrderB"; // mismatch + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if order update id of next order is greater than base order +TEST_F(OrderGraphValidatorTest, OrderUpdateIdRegression) +{ + // Base order update_id = 2 + nodes = {n0_, n2_}; + edges = {e1_}; + + order_.order_id = "OrderA"; + order_.order_update_id = 2; + order_.nodes = nodes; + order_.edges = edges; + + auto base_order = order_; + + // Next order update_id = 1 (invalid) + nodes = {n2_, n4_}; + edges = {e3_}; + + order_.order_update_id = 1; + order_.nodes = nodes; + order_.edges = edges; + + auto next_order = order_; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + base_order, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test if base order and next order is valid for stitching +TEST_F(OrderGraphValidatorTest, ValidOrderStitching) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node2(Rel) -> Edge3(Rel) -> Node4(Rel) + + // Path: [Node0] -> [Edge1] -> [Node2] (All Released) + order_.order_id = "StitchTest"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Path: [Node2] -> [Edge3] -> [Node4] + vda5050_types::Order next_order; + next_order.order_id = "StitchTest"; + next_order.order_update_id = 1; + next_order.nodes = {n2_, n4_}; // Starts at n2_ + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_TRUE(res); +} + +//============================================================================= +/// \brief test for invalid stitch +TEST_F(OrderGraphValidatorTest, InvalidOrderStitching) +{ + // CASE 1: Gap in Graph + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node4(Rel) -> ... + order_.order_id = "StitchFailNode"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node4 (The robot is at Node2, cannot jump to Node4) + vda5050_types::Order next_order; + next_order.order_id = "StitchFailNode"; + next_order.order_update_id = 1; + next_order.nodes = {n4_}; // Error: Should start at n2_ + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_FALSE(res); + + // CASE 2: Backtracking / Discontinuity + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) + // Next Order: Node0(Rel) -> ... + order_.order_id = "StitchFailNode"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node4 (The robot is at Node2, next order cant jump to Node0) + next_order.order_id = "StitchFailNode"; + next_order.order_update_id = 1; + next_order.nodes = {n0_}; // Error: Should start at n2_ + next_order.edges = {e3_}; + + res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for sequence mismatch +TEST_F(OrderGraphValidatorTest, SequenceMismatch) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel, Seq=2) + // Next Order: Node2(Rel, Seq=0) -> Edge3 -> ... + // Ends at Node2 (Sequence ID 2) + + order_.order_id = "StitchFailSeq"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_}; + order_.edges = {e1_}; + + // Starts at Node2, BUT with Sequence ID 0 + vda5050_types::Node n2_wrong_seq = n2_; + n2_wrong_seq.sequence_id = 0; + + vda5050_types::Order next_order; + next_order.order_id = "StitchFailSeq"; + next_order.order_update_id = 1; + next_order.nodes = {n2_wrong_seq, n4_}; + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + + EXPECT_FALSE(res); +} + +//============================================================================= +/// \brief test for the "Last Released Node" logic. +TEST_F(OrderGraphValidatorTest, StitchingReplacesHorizon) +{ + // Base Order: Node0(Rel) -> Edge1(Rel) -> Node2(Rel) -> Edge3(Unreleased) -> Node4(Unreleased) + // Next Order: Node2(Rel) -> Edge3(Rel) -> Node4(Rel) + + vda5050_types::Edge e3_horizon = e3_; + e3_horizon.released = false; + vda5050_types::Node n4_horizon = n4_; + n4_horizon.released = false; + + order_.order_id = "StitchingReplacesHorizon"; + order_.order_update_id = 0; + order_.nodes = {n0_, n2_, n4_horizon}; + order_.edges = {e1_, e3_horizon}; + + vda5050_types::Order next_order; + next_order.order_id = "StitchingReplacesHorizon"; + next_order.order_update_id = 1; + next_order.nodes = { + n2_, n4_}; // Stitching at n2 (Last released), IGNORING n4_horizon + next_order.edges = {e3_}; + + auto res = vda5050_core::order::OrderGraphValidator::is_valid_order_update( + order_, next_order); + EXPECT_TRUE(res); +}