From 05cf67952f92e63d730922f81d58659ce62ab96f Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sun, 17 Aug 2025 18:10:48 +0200 Subject: [PATCH 01/16] test added for xml schema validation - urdf files added for validation and unvalidation - schema xsd file added --- hardware_interface/CMakeLists.txt | 12 ++ hardware_interface/schema/ros2_control.xsd | 105 ++++++++++++++++ .../test/test_validate_xml_schema.cpp | 118 ++++++++++++++++++ .../urdf/test_hardware_components.urdf | 90 +++++++++++++ .../test_hardware_components_with_error.urdf | 90 +++++++++++++ 5 files changed, 415 insertions(+) create mode 100644 hardware_interface/schema/ros2_control.xsd create mode 100644 hardware_interface/test/test_validate_xml_schema.cpp create mode 100644 hardware_interface/urdf/test_hardware_components.urdf create mode 100644 hardware_interface/urdf/test_hardware_components_with_error.urdf diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index e10f7ec16f..7fcd18e434 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -73,6 +73,9 @@ if(BUILD_TESTING) find_package(ament_cmake_gmock REQUIRED) find_package(ros2_control_test_assets REQUIRED) + find_package(ament_index_cpp REQUIRED) + find_package(PkgConfig REQUIRED) + pkg_check_modules(LIBXML2 REQUIRED IMPORTED_TARGET libxml-2.0) ament_add_gmock(test_macros test/test_macros.cpp) target_include_directories(test_macros PRIVATE include) @@ -101,6 +104,10 @@ if(BUILD_TESTING) ament_add_gmock(test_component_parser test/test_component_parser.cpp) target_link_libraries(test_component_parser hardware_interface ros2_control_test_assets::ros2_control_test_assets) + # Test XML Schema Validator + ament_add_gmock(test_validate_xml_schema test/test_validate_xml_schema.cpp) + target_link_libraries(test_validate_xml_schema PkgConfig::LIBXML2 ament_index_cpp::ament_index_cpp) + add_library(test_hardware_components SHARED test/test_hardware_components/test_single_joint_actuator.cpp test/test_hardware_components/test_force_torque_sensor.cpp @@ -135,6 +142,11 @@ install( LIBRARY DESTINATION lib ) +install( + DIRECTORY schema urdf + DESTINATION share/hardware_interface +) + ament_export_targets(export_hardware_interface HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() diff --git a/hardware_interface/schema/ros2_control.xsd b/hardware_interface/schema/ros2_control.xsd new file mode 100644 index 0000000000..a9c237c090 --- /dev/null +++ b/hardware_interface/schema/ros2_control.xsd @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hardware_interface/test/test_validate_xml_schema.cpp b/hardware_interface/test/test_validate_xml_schema.cpp new file mode 100644 index 0000000000..cfb65d95eb --- /dev/null +++ b/hardware_interface/test/test_validate_xml_schema.cpp @@ -0,0 +1,118 @@ +// Copyright 2025 ros2_control development team +// +// 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 + +#include + +#include + +class XmlParser +{ +public: + XmlParser(const std::string & xmlFilePath, const std::string & xsdFilePath) + : doc(nullptr), schema(nullptr), schemaCtx(nullptr) + { + this->xmlFile = xmlFilePath; + this->xsdFile = xsdFilePath; + } + + ~XmlParser() + { + if (doc) + { + xmlFreeDoc(doc); + } + if (schemaCtx) + { + xmlSchemaFreeParserCtxt(schemaCtx); + } + if (schema) + { + xmlSchemaFree(schema); + } + xmlCleanupParser(); + } + + bool parseAndValidate() + { + doc = xmlReadFile(xmlFile.c_str(), nullptr, 0); + if (!doc) + { + return false; + } + + schemaCtx = xmlSchemaNewParserCtxt(xsdFile.c_str()); + if (!schemaCtx) + { + return false; + } + schema = xmlSchemaParse(schemaCtx); + if (!schema) + { + return false; + } + + xmlSchemaValidCtxtPtr validCtx = xmlSchemaNewValidCtxt(schema); + if (!validCtx) + { + return false; + } + int ret = xmlSchemaValidateDoc(validCtx, doc); + xmlSchemaFreeValidCtxt(validCtx); + + return ret == 0; + } + +private: + std::string xmlFile; + std::string xsdFile; + xmlDocPtr doc; + xmlSchemaPtr schema; + xmlSchemaParserCtxtPtr schemaCtx; +}; + +// Test fixture for XML schema validation +class XmlSchemaValidationTest : public ::testing::Test +{ +protected: + std::string valid_xml; + std::string invalid_xml; + std::string xsd; + + void SetUp() override + { + // Use ament_index_cpp to get the package share directory + std::string package_share_dir = + ament_index_cpp::get_package_share_directory("hardware_interface"); + valid_xml = package_share_dir + "/urdf/test_hardware_components.urdf"; + invalid_xml = package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; + xsd = package_share_dir + "/schema/ros2_control.xsd"; + } +}; + +TEST_F(XmlSchemaValidationTest, ValidXmlPasses) +{ + XmlParser parser(valid_xml, xsd); + EXPECT_TRUE(parser.parseAndValidate()); +} + +TEST_F(XmlSchemaValidationTest, InvalidXmlFails) +{ + XmlParser parser(invalid_xml, xsd); + EXPECT_FALSE(parser.parseAndValidate()); +} diff --git a/hardware_interface/urdf/test_hardware_components.urdf b/hardware_interface/urdf/test_hardware_components.urdf new file mode 100644 index 0000000000..611a7b4385 --- /dev/null +++ b/hardware_interface/urdf/test_hardware_components.urdf @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test_hardware_components/TestForceTorqueSensor + 0 + + + + + + + + + + + + + + test_hardware_components/TestTwoJointSystem + + + + -1 + 1 + + + + + + + + + + 0 + 1 + + + + + diff --git a/hardware_interface/urdf/test_hardware_components_with_error.urdf b/hardware_interface/urdf/test_hardware_components_with_error.urdf new file mode 100644 index 0000000000..34b01aa5c1 --- /dev/null +++ b/hardware_interface/urdf/test_hardware_components_with_error.urdf @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test_hardware_components/TestForceTorqueSensor + 0 + + + + + + + + + + + + + + test_hardware_components/TestTwoJointSystem + + + + -1 + 1 + + + + + + + + + + 0 + 1 + + + + + From c1423baacc3c84b54148e103add29e0e239a3e18 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sun, 17 Aug 2025 18:15:56 +0200 Subject: [PATCH 02/16] test dependencies updated --- hardware_interface/package.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hardware_interface/package.xml b/hardware_interface/package.xml index 869e0b0992..93df51d92a 100644 --- a/hardware_interface/package.xml +++ b/hardware_interface/package.xml @@ -31,6 +31,8 @@ rcutils ament_cmake_gmock + ament_index_cpp + libxml2 ros2_control_test_assets From 3c9927b52868e676055f37da2e274316ea64097c Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Mon, 18 Aug 2025 11:56:19 +0200 Subject: [PATCH 03/16] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reformatting and sort ascending Co-authored-by: Christoph Fröhlich --- hardware_interface/CMakeLists.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index 7fcd18e434..19a2d0e43b 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -74,8 +74,7 @@ if(BUILD_TESTING) find_package(ament_cmake_gmock REQUIRED) find_package(ros2_control_test_assets REQUIRED) find_package(ament_index_cpp REQUIRED) - find_package(PkgConfig REQUIRED) - pkg_check_modules(LIBXML2 REQUIRED IMPORTED_TARGET libxml-2.0) + find_package(LibXml2) ament_add_gmock(test_macros test/test_macros.cpp) target_include_directories(test_macros PRIVATE include) @@ -106,7 +105,10 @@ if(BUILD_TESTING) # Test XML Schema Validator ament_add_gmock(test_validate_xml_schema test/test_validate_xml_schema.cpp) - target_link_libraries(test_validate_xml_schema PkgConfig::LIBXML2 ament_index_cpp::ament_index_cpp) + target_link_libraries(test_validate_xml_schema + ament_index_cpp::ament_index_cpp + LibXml2::LibXml2 + ) add_library(test_hardware_components SHARED test/test_hardware_components/test_single_joint_actuator.cpp From ea6672755397df61986ea027acb2eefde966bf54 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Mon, 18 Aug 2025 11:59:55 +0200 Subject: [PATCH 04/16] urdf files moved to ros2_control_test_assets --- .../urdf/test_hardware_components.urdf | 90 ------------------- .../urdf/test_hardware_components.urdf | 13 ++- .../test_hardware_components_with_error.urdf | 0 3 files changed, 12 insertions(+), 91 deletions(-) delete mode 100644 hardware_interface/urdf/test_hardware_components.urdf rename {hardware_interface => ros2_control_test_assets}/urdf/test_hardware_components_with_error.urdf (100%) diff --git a/hardware_interface/urdf/test_hardware_components.urdf b/hardware_interface/urdf/test_hardware_components.urdf deleted file mode 100644 index 611a7b4385..0000000000 --- a/hardware_interface/urdf/test_hardware_components.urdf +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - test_hardware_components/TestForceTorqueSensor - 0 - - - - - - - - - - - - - - test_hardware_components/TestTwoJointSystem - - - - -1 - 1 - - - - - - - - - - 0 - 1 - - - - - diff --git a/ros2_control_test_assets/urdf/test_hardware_components.urdf b/ros2_control_test_assets/urdf/test_hardware_components.urdf index c49c79bf55..611a7b4385 100644 --- a/ros2_control_test_assets/urdf/test_hardware_components.urdf +++ b/ros2_control_test_assets/urdf/test_hardware_components.urdf @@ -52,6 +52,7 @@ test_hardware_components/TestForceTorqueSensor + 0 @@ -68,12 +69,22 @@ test_hardware_components/TestTwoJointSystem - + + -1 + 1 + + + + 0 + 1 + + + diff --git a/hardware_interface/urdf/test_hardware_components_with_error.urdf b/ros2_control_test_assets/urdf/test_hardware_components_with_error.urdf similarity index 100% rename from hardware_interface/urdf/test_hardware_components_with_error.urdf rename to ros2_control_test_assets/urdf/test_hardware_components_with_error.urdf From f2c0ee61cc007585b8e67c4bac0bc104cab317c1 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Mon, 18 Aug 2025 12:20:43 +0200 Subject: [PATCH 05/16] urdf directory code updated package path updated to find urdf and xsd file --- hardware_interface/CMakeLists.txt | 9 ++------- hardware_interface/test/test_validate_xml_schema.cpp | 10 ++++++---- ros2_control_test_assets/CMakeLists.txt | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index 19a2d0e43b..60080f5cbb 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -106,8 +106,8 @@ if(BUILD_TESTING) # Test XML Schema Validator ament_add_gmock(test_validate_xml_schema test/test_validate_xml_schema.cpp) target_link_libraries(test_validate_xml_schema - ament_index_cpp::ament_index_cpp - LibXml2::LibXml2 + ament_index_cpp::ament_index_cpp + LibXml2::LibXml2 ) add_library(test_hardware_components SHARED @@ -144,11 +144,6 @@ install( LIBRARY DESTINATION lib ) -install( - DIRECTORY schema urdf - DESTINATION share/hardware_interface -) - ament_export_targets(export_hardware_interface HAS_LIBRARY_TARGET) ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS}) ament_package() diff --git a/hardware_interface/test/test_validate_xml_schema.cpp b/hardware_interface/test/test_validate_xml_schema.cpp index cfb65d95eb..66124195d2 100644 --- a/hardware_interface/test/test_validate_xml_schema.cpp +++ b/hardware_interface/test/test_validate_xml_schema.cpp @@ -97,11 +97,13 @@ class XmlSchemaValidationTest : public ::testing::Test void SetUp() override { // Use ament_index_cpp to get the package share directory - std::string package_share_dir = + std::string urdf_package_share_dir = + ament_index_cpp::get_package_share_directory("ros2_control_test_assets"); + std::string xsd_package_share_dir = ament_index_cpp::get_package_share_directory("hardware_interface"); - valid_xml = package_share_dir + "/urdf/test_hardware_components.urdf"; - invalid_xml = package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; - xsd = package_share_dir + "/schema/ros2_control.xsd"; + valid_xml = urdf_package_share_dir + "/urdf/test_hardware_components.urdf"; + invalid_xml = urdf_package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; + xsd = xsd_package_share_dir + "/schema/ros2_control.xsd"; } }; diff --git a/ros2_control_test_assets/CMakeLists.txt b/ros2_control_test_assets/CMakeLists.txt index d63fb52c86..9450ba9d35 100644 --- a/ros2_control_test_assets/CMakeLists.txt +++ b/ros2_control_test_assets/CMakeLists.txt @@ -15,7 +15,7 @@ install( DESTINATION include/ros2_control_test_assets ) install( - FILES urdf/test_hardware_components.urdf + FILES urdf/test_hardware_components.urdf urdf/test_hardware_components_with_error.urdf DESTINATION share/ros2_control_test_assets/urdf ) install(TARGETS ros2_control_test_assets From 2dbb0563887386082b917ce728f1c5b2b402eb48 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Mon, 18 Aug 2025 12:50:15 +0200 Subject: [PATCH 06/16] accidentely removed the code to add schema to directory --- hardware_interface/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index 60080f5cbb..a243a6ed77 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -134,6 +134,12 @@ install( DIRECTORY include/ DESTINATION include/hardware_interface ) + +install( + DIRECTORY schema + DESTINATION share/hardware_interface +) + install( TARGETS mock_components From d1746bec0adac214d6832a61dec44498b991f1d2 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Tue, 4 Nov 2025 16:48:44 +0100 Subject: [PATCH 07/16] Add component validator for URDF validation against XSD --- .../component_validator.hpp | 40 +++++++++++++ .../src/component_validator.cpp | 57 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 hardware_interface/include/hardware_interface/component_validator.hpp create mode 100644 hardware_interface/src/component_validator.cpp diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp new file mode 100644 index 0000000000..1411b74f43 --- /dev/null +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -0,0 +1,40 @@ +// Copyright 2020 ros2_control Development Team +// +// 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 HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ +#define HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ + +#include +#include +#include +#include + +#include + +#include + +namespace hardware_interface +{ + +/// Validate URDF string against an XML Schema Definition (XSD) file. +/** + * \param[in] urdf string with robot's URDF + * \param[in] xsd_file_path path to the XSD file + * \return true if the URDF is valid according to the XSD, false otherwise + */ +bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_file_path); + +} // namespace hardware_interface + +#endif // HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp new file mode 100644 index 0000000000..f7f1cdc42c --- /dev/null +++ b/hardware_interface/src/component_validator.cpp @@ -0,0 +1,57 @@ +// Copyright 2025 ros2_control Development Team +// +// 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 "ros2_control/hardware_interface/component_validator.hpp" + +namespace hardware_interface +{ +bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_file_path) +{ + xmlDocPtr doc = xmlReadMemory(urdf.c_str(), static_cast(urdf.size()), nullptr, nullptr, 0); + if (!doc) + { + return false; + } + + xmlSchemaParserCtxtPtr schemaCtx = xmlSchemaNewParserCtxt(xsd_file_path.c_str()); + if (!schemaCtx) + { + xmlFreeDoc(doc); + return false; + } + xmlSchemaPtr schema = xmlSchemaParse(schemaCtx); + if (!schema) + { + xmlSchemaFreeParserCtxt(schemaCtx); + xmlFreeDoc(doc); + return false; + } + + xmlSchemaValidCtxtPtr validCtx = xmlSchemaNewValidCtxt(schema); + if (!validCtx) + { + xmlSchemaFree(schema); + xmlSchemaFreeParserCtxt(schemaCtx); + xmlFreeDoc(doc); + return false; + } + int ret = xmlSchemaValidateDoc(validCtx, doc); + xmlSchemaFreeValidCtxt(validCtx); + xmlSchemaFree(schema); + xmlSchemaFreeParserCtxt(schemaCtx); + xmlFreeDoc(doc); + + return ret == 0; +} +} // namespace hardware_interface From e706cc05b54b27e37889be822527774608895e4c Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Wed, 5 Nov 2025 14:16:58 +0100 Subject: [PATCH 08/16] Add component validator and tests for URDF validation against XSD --- hardware_interface/CMakeLists.txt | 10 +++ .../component_validator.hpp | 12 ++- .../src/component_validator.cpp | 37 ++++++++- .../test/test_component_validator.cpp | 75 +++++++++++++++++++ 4 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 hardware_interface/test/test_component_validator.cpp diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index a243a6ed77..e42644ebe0 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -30,6 +30,7 @@ endforeach() add_library(hardware_interface SHARED src/component_parser.cpp + src/component_validator.cpp src/resource_manager.cpp src/hardware_component.cpp src/lexical_casts.cpp @@ -48,6 +49,7 @@ target_link_libraries(hardware_interface PUBLIC realtime_tools::realtime_tools rcutils::rcutils rcpputils::rcpputils + LibXml2::LibXml2 ${joint_limits_TARGETS} ${TinyXML2_LIBRARIES} ${tinyxml2_vendor_LIBRARIES} @@ -103,6 +105,14 @@ if(BUILD_TESTING) ament_add_gmock(test_component_parser test/test_component_parser.cpp) target_link_libraries(test_component_parser hardware_interface ros2_control_test_assets::ros2_control_test_assets) + ament_add_gmock(test_component_validator test/test_component_validator.cpp) + target_link_libraries(test_component_validator + hardware_interface + ament_index_cpp::ament_index_cpp + ros2_control_test_assets::ros2_control_test_assets + LibXml2::LibXml2 + ) + # Test XML Schema Validator ament_add_gmock(test_validate_xml_schema test/test_validate_xml_schema.cpp) target_link_libraries(test_validate_xml_schema diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp index 1411b74f43..df8fddac55 100644 --- a/hardware_interface/include/hardware_interface/component_validator.hpp +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -15,7 +15,7 @@ #ifndef HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ #define HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ -#include +// #include #include #include #include @@ -35,6 +35,16 @@ namespace hardware_interface */ bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_file_path); +/// Validate URDF file path against an XML Schema Definition (XSD) file. +/** + * \param[in] urdf_file_path path to URDF file + * \param[in] xsd_file_path path to the XSD file + * \return true if URDF is valid according to the XSD, false otherwise + * + */ +bool validate_urdf_file_path_with_xsd( + const std::string & urdf_file_path, std::string & xsd_file_path); + } // namespace hardware_interface #endif // HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index f7f1cdc42c..b9c921df64 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -12,24 +12,34 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ros2_control/hardware_interface/component_validator.hpp" +#include +#include + +#include "hardware_interface/component_validator.hpp" namespace hardware_interface { bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_file_path) { + if (urdf.empty()) + { + throw std::runtime_error("empty URDF passed to robot"); + } + // Load URDF xmlDocPtr doc = xmlReadMemory(urdf.c_str(), static_cast(urdf.size()), nullptr, nullptr, 0); if (!doc) { return false; } + // Load XSD xmlSchemaParserCtxtPtr schemaCtx = xmlSchemaNewParserCtxt(xsd_file_path.c_str()); if (!schemaCtx) { xmlFreeDoc(doc); return false; } + // Parse XSD xmlSchemaPtr schema = xmlSchemaParse(schemaCtx); if (!schema) { @@ -37,7 +47,7 @@ bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_fi xmlFreeDoc(doc); return false; } - + // Validate URDF against XSD xmlSchemaValidCtxtPtr validCtx = xmlSchemaNewValidCtxt(schema); if (!validCtx) { @@ -46,6 +56,7 @@ bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_fi xmlFreeDoc(doc); return false; } + // Perform validation int ret = xmlSchemaValidateDoc(validCtx, doc); xmlSchemaFreeValidCtxt(validCtx); xmlSchemaFree(schema); @@ -54,4 +65,26 @@ bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_fi return ret == 0; } + +bool validate_urdf_file_path_with_xsd( + const std::string & urdf_file_path, std::string & xsd_file_path) +{ + std::ifstream file(urdf_file_path); + if (!file) + { + return false; + } + + std::ostringstream ss; + ss << file.rdbuf(); + const std::string urdf = ss.str(); + + if (urdf.empty()) + { + return false; + } + + return validate_urdf_with_xsd(urdf, xsd_file_path); +} + } // namespace hardware_interface diff --git a/hardware_interface/test/test_component_validator.cpp b/hardware_interface/test/test_component_validator.cpp new file mode 100644 index 0000000000..da1f204aed --- /dev/null +++ b/hardware_interface/test/test_component_validator.cpp @@ -0,0 +1,75 @@ +// Copyright 2025 ros2_control Development Team +// +// 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 +#include "hardware_interface/component_validator.hpp" + +using namespace ::testing; // NOLINT + +class TestComponentValidator : public Test +{ +protected: + std::string valid_xml_file_path; + std::string invalid_xml_file_path; + std::string xsd_file_path; + void SetUp() override + { + // Use ament_index_cpp to get the package share directory + std::string urdf_package_share_dir = + ament_index_cpp::get_package_share_directory("ros2_control_test_assets"); + std::string xsd_package_share_dir = + ament_index_cpp::get_package_share_directory("hardware_interface"); + valid_xml_file_path = urdf_package_share_dir + "/urdf/test_hardware_components.urdf"; + invalid_xml_file_path = + urdf_package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; + xsd_file_path = xsd_package_share_dir + "/schema/ros2_control.xsd"; + } +}; + +// using hardware_interface::parse_control_resources_from_urdf; +using hardware_interface::validate_urdf_file_path_with_xsd; +using hardware_interface::validate_urdf_with_xsd; + +TEST_F(TestComponentValidator, DryRun) +{ + SUCCEED(); // Minimal test to allow compilation and DRY test +} + +TEST_F(TestComponentValidator, empty_string_throws_error) +{ + ASSERT_THROW(validate_urdf_with_xsd("", xsd_file_path), std::runtime_error); +} + +TEST_F(TestComponentValidator, empty_urdf_throws_error) +{ + const std::string empty_urdf = + ""; + + ASSERT_FALSE(validate_urdf_with_xsd( + empty_urdf, xsd_file_path)); // TODO(Sachin): discuss if should use throw error +} + +TEST_F(TestComponentValidator, validate_valid_urdf_with_xsd) +{ + ASSERT_TRUE(validate_urdf_file_path_with_xsd(valid_xml_file_path, xsd_file_path)); +} + +TEST_F(TestComponentValidator, validate_invalid_urdf_with_xsd) +{ + ASSERT_FALSE(validate_urdf_file_path_with_xsd(invalid_xml_file_path, xsd_file_path)); +} From ac3996800f009cb46d13197f074bb8ffa2eab6ba Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Wed, 5 Nov 2025 14:18:54 +0100 Subject: [PATCH 09/16] test_validate_xml_schema is removed as test_hardware_component is used for xsd file validation --- hardware_interface/CMakeLists.txt | 7 - .../test/test_validate_xml_schema.cpp | 120 ------------------ 2 files changed, 127 deletions(-) delete mode 100644 hardware_interface/test/test_validate_xml_schema.cpp diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index e42644ebe0..c2cc781ebf 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -113,13 +113,6 @@ if(BUILD_TESTING) LibXml2::LibXml2 ) - # Test XML Schema Validator - ament_add_gmock(test_validate_xml_schema test/test_validate_xml_schema.cpp) - target_link_libraries(test_validate_xml_schema - ament_index_cpp::ament_index_cpp - LibXml2::LibXml2 - ) - add_library(test_hardware_components SHARED test/test_hardware_components/test_single_joint_actuator.cpp test/test_hardware_components/test_force_torque_sensor.cpp diff --git a/hardware_interface/test/test_validate_xml_schema.cpp b/hardware_interface/test/test_validate_xml_schema.cpp deleted file mode 100644 index 66124195d2..0000000000 --- a/hardware_interface/test/test_validate_xml_schema.cpp +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2025 ros2_control development team -// -// 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 - -#include - -#include - -class XmlParser -{ -public: - XmlParser(const std::string & xmlFilePath, const std::string & xsdFilePath) - : doc(nullptr), schema(nullptr), schemaCtx(nullptr) - { - this->xmlFile = xmlFilePath; - this->xsdFile = xsdFilePath; - } - - ~XmlParser() - { - if (doc) - { - xmlFreeDoc(doc); - } - if (schemaCtx) - { - xmlSchemaFreeParserCtxt(schemaCtx); - } - if (schema) - { - xmlSchemaFree(schema); - } - xmlCleanupParser(); - } - - bool parseAndValidate() - { - doc = xmlReadFile(xmlFile.c_str(), nullptr, 0); - if (!doc) - { - return false; - } - - schemaCtx = xmlSchemaNewParserCtxt(xsdFile.c_str()); - if (!schemaCtx) - { - return false; - } - schema = xmlSchemaParse(schemaCtx); - if (!schema) - { - return false; - } - - xmlSchemaValidCtxtPtr validCtx = xmlSchemaNewValidCtxt(schema); - if (!validCtx) - { - return false; - } - int ret = xmlSchemaValidateDoc(validCtx, doc); - xmlSchemaFreeValidCtxt(validCtx); - - return ret == 0; - } - -private: - std::string xmlFile; - std::string xsdFile; - xmlDocPtr doc; - xmlSchemaPtr schema; - xmlSchemaParserCtxtPtr schemaCtx; -}; - -// Test fixture for XML schema validation -class XmlSchemaValidationTest : public ::testing::Test -{ -protected: - std::string valid_xml; - std::string invalid_xml; - std::string xsd; - - void SetUp() override - { - // Use ament_index_cpp to get the package share directory - std::string urdf_package_share_dir = - ament_index_cpp::get_package_share_directory("ros2_control_test_assets"); - std::string xsd_package_share_dir = - ament_index_cpp::get_package_share_directory("hardware_interface"); - valid_xml = urdf_package_share_dir + "/urdf/test_hardware_components.urdf"; - invalid_xml = urdf_package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; - xsd = xsd_package_share_dir + "/schema/ros2_control.xsd"; - } -}; - -TEST_F(XmlSchemaValidationTest, ValidXmlPasses) -{ - XmlParser parser(valid_xml, xsd); - EXPECT_TRUE(parser.parseAndValidate()); -} - -TEST_F(XmlSchemaValidationTest, InvalidXmlFails) -{ - XmlParser parser(invalid_xml, xsd); - EXPECT_FALSE(parser.parseAndValidate()); -} From fdbea18c2819270d5a1216afda912038414c82c1 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Wed, 5 Nov 2025 14:40:40 +0100 Subject: [PATCH 10/16] Add LibXml2 to package dependencies in CMakeLists.txt --- hardware_interface/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/hardware_interface/CMakeLists.txt b/hardware_interface/CMakeLists.txt index c2cc781ebf..5c5d11c52c 100644 --- a/hardware_interface/CMakeLists.txt +++ b/hardware_interface/CMakeLists.txt @@ -8,6 +8,7 @@ export_windows_symbols() set(THIS_PACKAGE_INCLUDE_DEPENDS control_msgs lifecycle_msgs + LibXml2 pluginlib rclcpp_lifecycle rcpputils From 780ae652930bf631bc732d441d74e154191882c0 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Wed, 5 Nov 2025 14:55:59 +0100 Subject: [PATCH 11/16] Remove unused Google Test includes from component_validator.hpp --- .../include/hardware_interface/component_validator.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp index df8fddac55..585547c76b 100644 --- a/hardware_interface/include/hardware_interface/component_validator.hpp +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -15,8 +15,6 @@ #ifndef HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ #define HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ -// #include -#include #include #include From c92a206beb6189ac9ef1a4b44c3cab48c5d8dc5e Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Wed, 5 Nov 2025 15:27:38 +0100 Subject: [PATCH 12/16] Add functions to validate URDF against XSD and extract ROS2 Control XSD tag --- .../component_validator.hpp | 15 +++++ .../src/component_validator.cpp | 60 +++++++++++++++++++ 2 files changed, 75 insertions(+) diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp index 585547c76b..513be1c4ad 100644 --- a/hardware_interface/include/hardware_interface/component_validator.hpp +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -45,4 +45,19 @@ bool validate_urdf_file_path_with_xsd( } // namespace hardware_interface +/// Validate URDF against an XSD file provides in URDF itself +/** + * \param[in] urdf string with robot's urdf + * \return true if the URDF is valid according to the XSD, false otherwise + */ +bool validate_urdf_with_xsd_tag(const std::string & urdf); + +/// Extract ROS2 Control XSD tag from URDF +/** + * \param[in] urdf string with robot's urdf contains xmlns:ros2_control + * \param[out] string of xlmns:ros2_control + * \return true if extraction successful else false + */ +bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_control_xsd); + #endif // HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index b9c921df64..8cf6172798 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include @@ -87,4 +88,63 @@ bool validate_urdf_file_path_with_xsd( return validate_urdf_with_xsd(urdf, xsd_file_path); } +bool validate_urdf_with_xsd_tag(const std::string & urdf) +{ + if (urdf.empty()) + { + throw std::runtime_error("empty URDF passed to robot"); + } + // extract xmlns tag from the urdf + std::string ros2_control_xsd; + bool result = extract_ros2_control_xsd_tag(urdf, ros2_control_xsd); + if (!result) + { + return false; + } + return validate_urdf_with_xsd(urdf, ros2_control_xsd); +} + +bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_control_xsd) +{ + if (urdf.empty()) + { + throw std::runtime_error("empty URDF passed to robot"); + } + + xmlDocPtr doc = xmlReadMemory(urdf.c_str(), static_cast(urdf.size()), nullptr, nullptr, 0); + if (!doc) + { + return {}; + } + + xmlNodePtr root = xmlDocGetRootElement(doc); + if (root) + { + auto check = [&](xmlNsPtr ns) -> bool + { + if ( + ns && ns->prefix && + std::strcmp(reinterpret_cast(ns->prefix), "ros2_control") == 0) + { + ros2_control_xsd = ns->href ? reinterpret_cast(ns->href) : ""; + return true; + } + return false; + }; + if (!check(root->ns)) + { + for (xmlNsPtr cur = root->nsDef; cur; cur = cur->next) + { + if (check(cur)) + { + break; + } + } + } + } + + xmlFreeDoc(doc); + return false; +} + } // namespace hardware_interface From 5e0dc6a7b3ef00a9da21fc6d6a1ce21aefcf3aac Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 8 Nov 2025 11:14:49 +0100 Subject: [PATCH 13/16] Enhance URDF validation by normalizing XSD paths and downloading remote schemas --- .../component_validator.hpp | 4 +- .../src/component_validator.cpp | 54 +++++++++++++++++-- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp index 513be1c4ad..d043b58e05 100644 --- a/hardware_interface/include/hardware_interface/component_validator.hpp +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -43,8 +43,6 @@ bool validate_urdf_with_xsd(const std::string & urdf, const std::string & xsd_fi bool validate_urdf_file_path_with_xsd( const std::string & urdf_file_path, std::string & xsd_file_path); -} // namespace hardware_interface - /// Validate URDF against an XSD file provides in URDF itself /** * \param[in] urdf string with robot's urdf @@ -60,4 +58,6 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf); */ bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_control_xsd); +} // namespace hardware_interface + #endif // HARDWARE_INTERFACE__COMPONENT_VALIDATOR_HPP_ diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index 8cf6172798..59c8d70ac7 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -101,6 +101,48 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf) { return false; } + std::string xsd_package_share_dir = + ament_index_cpp::get_package_share_directory("hardware_interface"); + // If the extracted XSD is a file URI (e.g. "file:///path/to/schema.xsd"), normalize to a local + // path + if (ros2_control_xsd.find("file") != std::string::npos) + { + ros2_control_xsd.replace(0, 8, xsd_package_share_dir); + } + else if (ros2_control_xsd.find("http") != std::string::npos) + { + { + // Download the remote XSD to a local temporary file and point to it + std::string filename; + auto pos = ros2_control_xsd.find_last_of('/'); + if (pos == std::string::npos || pos + 1 >= ros2_control_xsd.size()) + { + filename = "ros2_control_schema.xsd"; + } + else + { + filename = ros2_control_xsd.substr(pos + 1); + } + std::string tmp_path = std::string("/tmp/") + filename; + + // Use curl to fetch the XSD; require curl to be available on PATH. + std::ostringstream cmd; + cmd << "curl -sSfL -o '" << tmp_path << "' '" << ros2_control_xsd << "'"; + + int rc = std::system(cmd.str().c_str()); + if (rc != 0) + { + // failed to download + return false; + } + + ros2_control_xsd = tmp_path; + } + } + else + { + return false; + } return validate_urdf_with_xsd(urdf, ros2_control_xsd); } @@ -114,10 +156,11 @@ bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_c xmlDocPtr doc = xmlReadMemory(urdf.c_str(), static_cast(urdf.size()), nullptr, nullptr, 0); if (!doc) { - return {}; + return false; } xmlNodePtr root = xmlDocGetRootElement(doc); + bool found = false; if (root) { auto check = [&](xmlNsPtr ns) -> bool @@ -131,12 +174,17 @@ bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_c } return false; }; - if (!check(root->ns)) + if (check(root->ns)) + { + found = true; + } + else { for (xmlNsPtr cur = root->nsDef; cur; cur = cur->next) { if (check(cur)) { + found = true; break; } } @@ -144,7 +192,7 @@ bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_c } xmlFreeDoc(doc); - return false; + return found; } } // namespace hardware_interface From f71ff75596645ca3c9224f3f82974a98754eb08b Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sat, 8 Nov 2025 23:23:57 +0100 Subject: [PATCH 14/16] Add URDF validation function and corresponding test case --- .../component_validator.hpp | 7 ++ .../src/component_validator.cpp | 20 +++++ .../test/test_component_validator.cpp | 11 ++- .../test_hardware_components_xsd_file.urdf | 90 +++++++++++++++++++ 4 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 ros2_control_test_assets/urdf/test_hardware_components_xsd_file.urdf diff --git a/hardware_interface/include/hardware_interface/component_validator.hpp b/hardware_interface/include/hardware_interface/component_validator.hpp index d043b58e05..792c4b3b2d 100644 --- a/hardware_interface/include/hardware_interface/component_validator.hpp +++ b/hardware_interface/include/hardware_interface/component_validator.hpp @@ -50,6 +50,13 @@ bool validate_urdf_file_path_with_xsd( */ bool validate_urdf_with_xsd_tag(const std::string & urdf); +/// Validate URDF file path against an XSD file provides in URDF itself +/** + * \param[in] urdf_file_path string + * \return true if the URDF is valid according to the XSD, false otherwise + */ +bool validate_urdf_file_with_xsd_tag(const std::string & urdf_file_path); + /// Extract ROS2 Control XSD tag from URDF /** * \param[in] urdf string with robot's urdf contains xmlns:ros2_control diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index 59c8d70ac7..0e0bfe7fa7 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -101,6 +101,7 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf) { return false; } + return true; std::string xsd_package_share_dir = ament_index_cpp::get_package_share_directory("hardware_interface"); // If the extracted XSD is a file URI (e.g. "file:///path/to/schema.xsd"), normalize to a local @@ -146,6 +147,25 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf) return validate_urdf_with_xsd(urdf, ros2_control_xsd); } +bool validate_urdf_file_with_xsd_tag(const std::string & urdf_file_path) +{ + std::ifstream file(urdf_file_path); + if (!file) + { + return false; + } + + std::ostringstream ss; + ss << file.rdbuf(); + const std::string urdf = ss.str(); + + if (urdf.empty()) + { + return false; + } + return validate_urdf_with_xsd_tag(urdf); +} + bool extract_ros2_control_xsd_tag(const std::string & urdf, std::string & ros2_control_xsd) { if (urdf.empty()) diff --git a/hardware_interface/test/test_component_validator.cpp b/hardware_interface/test/test_component_validator.cpp index da1f204aed..2d928ab13b 100644 --- a/hardware_interface/test/test_component_validator.cpp +++ b/hardware_interface/test/test_component_validator.cpp @@ -41,8 +41,8 @@ class TestComponentValidator : public Test } }; -// using hardware_interface::parse_control_resources_from_urdf; using hardware_interface::validate_urdf_file_path_with_xsd; +using hardware_interface::validate_urdf_file_with_xsd_tag; using hardware_interface::validate_urdf_with_xsd; TEST_F(TestComponentValidator, DryRun) @@ -58,9 +58,9 @@ TEST_F(TestComponentValidator, empty_string_throws_error) TEST_F(TestComponentValidator, empty_urdf_throws_error) { const std::string empty_urdf = - ""; + ""; - ASSERT_FALSE(validate_urdf_with_xsd( + ASSERT_TRUE(validate_urdf_with_xsd( empty_urdf, xsd_file_path)); // TODO(Sachin): discuss if should use throw error } @@ -73,3 +73,8 @@ TEST_F(TestComponentValidator, validate_invalid_urdf_with_xsd) { ASSERT_FALSE(validate_urdf_file_path_with_xsd(invalid_xml_file_path, xsd_file_path)); } + +TEST_F(TestComponentValidator, validate_valid_urdf) +{ + ASSERT_TRUE(validate_urdf_file_with_xsd_tag(valid_xml_file_path)); +} diff --git a/ros2_control_test_assets/urdf/test_hardware_components_xsd_file.urdf b/ros2_control_test_assets/urdf/test_hardware_components_xsd_file.urdf new file mode 100644 index 0000000000..96adc05c02 --- /dev/null +++ b/ros2_control_test_assets/urdf/test_hardware_components_xsd_file.urdf @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test_hardware_components/TestForceTorqueSensor + 0 + + + + + + + + + + + + + + test_hardware_components/TestTwoJointSystem + + + + -1 + 1 + + + + + + + + + + 0 + 1 + + + + + From 80c3b649290953eb35b44d5e4b49eb3ec347899e Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sun, 9 Nov 2025 12:37:59 +0100 Subject: [PATCH 15/16] Fix URDF validation by correcting XSD path normalization and updating test cases --- hardware_interface/src/component_validator.cpp | 3 +-- hardware_interface/test/test_component_validator.cpp | 5 ++++- ros2_control_test_assets/CMakeLists.txt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index 0e0bfe7fa7..2ffc1f1a98 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -101,14 +101,13 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf) { return false; } - return true; std::string xsd_package_share_dir = ament_index_cpp::get_package_share_directory("hardware_interface"); // If the extracted XSD is a file URI (e.g. "file:///path/to/schema.xsd"), normalize to a local // path if (ros2_control_xsd.find("file") != std::string::npos) { - ros2_control_xsd.replace(0, 8, xsd_package_share_dir); + ros2_control_xsd.replace(0, 7, xsd_package_share_dir); } else if (ros2_control_xsd.find("http") != std::string::npos) { diff --git a/hardware_interface/test/test_component_validator.cpp b/hardware_interface/test/test_component_validator.cpp index 2d928ab13b..5d92cc6b46 100644 --- a/hardware_interface/test/test_component_validator.cpp +++ b/hardware_interface/test/test_component_validator.cpp @@ -25,6 +25,7 @@ class TestComponentValidator : public Test { protected: std::string valid_xml_file_path; + std::string valid_xml_file_path_with_tag; std::string invalid_xml_file_path; std::string xsd_file_path; void SetUp() override @@ -35,6 +36,8 @@ class TestComponentValidator : public Test std::string xsd_package_share_dir = ament_index_cpp::get_package_share_directory("hardware_interface"); valid_xml_file_path = urdf_package_share_dir + "/urdf/test_hardware_components.urdf"; + valid_xml_file_path_with_tag = + urdf_package_share_dir + "/urdf/test_hardware_components_xsd_file.urdf"; invalid_xml_file_path = urdf_package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; xsd_file_path = xsd_package_share_dir + "/schema/ros2_control.xsd"; @@ -76,5 +79,5 @@ TEST_F(TestComponentValidator, validate_invalid_urdf_with_xsd) TEST_F(TestComponentValidator, validate_valid_urdf) { - ASSERT_TRUE(validate_urdf_file_with_xsd_tag(valid_xml_file_path)); + ASSERT_TRUE(validate_urdf_file_with_xsd_tag(valid_xml_file_path_with_tag)); } diff --git a/ros2_control_test_assets/CMakeLists.txt b/ros2_control_test_assets/CMakeLists.txt index 9450ba9d35..a181ae3838 100644 --- a/ros2_control_test_assets/CMakeLists.txt +++ b/ros2_control_test_assets/CMakeLists.txt @@ -15,7 +15,7 @@ install( DESTINATION include/ros2_control_test_assets ) install( - FILES urdf/test_hardware_components.urdf urdf/test_hardware_components_with_error.urdf + FILES urdf/test_hardware_components.urdf urdf/test_hardware_components_xsd_file.urdf urdf/test_hardware_components_with_error.urdf DESTINATION share/ros2_control_test_assets/urdf ) install(TARGETS ros2_control_test_assets From a440719d6ee376f0239f9f1e34bc4611d2afbca6 Mon Sep 17 00:00:00 2001 From: Sachin Kumar Date: Sun, 9 Nov 2025 12:54:54 +0100 Subject: [PATCH 16/16] Enhance URDF validation by adding support for remote XSD files and updating test cases --- .../src/component_validator.cpp | 45 +++++----- .../test/test_component_validator.cpp | 12 ++- ros2_control_test_assets/CMakeLists.txt | 7 +- .../test_hardware_components_xsd_web.urdf | 90 +++++++++++++++++++ 4 files changed, 128 insertions(+), 26 deletions(-) create mode 100644 ros2_control_test_assets/urdf/test_hardware_components_xsd_web.urdf diff --git a/hardware_interface/src/component_validator.cpp b/hardware_interface/src/component_validator.cpp index 2ffc1f1a98..b4a97b4823 100644 --- a/hardware_interface/src/component_validator.cpp +++ b/hardware_interface/src/component_validator.cpp @@ -111,33 +111,32 @@ bool validate_urdf_with_xsd_tag(const std::string & urdf) } else if (ros2_control_xsd.find("http") != std::string::npos) { + // Download the remote XSD to a local temporary file and point to it + std::string filename; + auto pos = ros2_control_xsd.find_last_of('/'); + if (pos == std::string::npos || pos + 1 >= ros2_control_xsd.size()) { - // Download the remote XSD to a local temporary file and point to it - std::string filename; - auto pos = ros2_control_xsd.find_last_of('/'); - if (pos == std::string::npos || pos + 1 >= ros2_control_xsd.size()) - { - filename = "ros2_control_schema.xsd"; - } - else - { - filename = ros2_control_xsd.substr(pos + 1); - } - std::string tmp_path = std::string("/tmp/") + filename; - - // Use curl to fetch the XSD; require curl to be available on PATH. - std::ostringstream cmd; - cmd << "curl -sSfL -o '" << tmp_path << "' '" << ros2_control_xsd << "'"; + filename = "ros2_control_schema.xsd"; + } + else + { + filename = ros2_control_xsd.substr(pos + 1); + } + std::string tmp_path = std::string("/tmp/") + filename; - int rc = std::system(cmd.str().c_str()); - if (rc != 0) - { - // failed to download - return false; - } + // Use curl to fetch the XSD; require curl to be available on PATH. + std::ostringstream cmd; + cmd << "curl -sSfL -o '" << tmp_path << "' '" + << ros2_control_xsd + std::string("ros2_control.xsd") << "'"; - ros2_control_xsd = tmp_path; + int rc = std::system(cmd.str().c_str()); + if (rc != 0) + { + // failed to download + return false; } + + ros2_control_xsd = tmp_path; } else { diff --git a/hardware_interface/test/test_component_validator.cpp b/hardware_interface/test/test_component_validator.cpp index 5d92cc6b46..2242872d97 100644 --- a/hardware_interface/test/test_component_validator.cpp +++ b/hardware_interface/test/test_component_validator.cpp @@ -26,6 +26,7 @@ class TestComponentValidator : public Test protected: std::string valid_xml_file_path; std::string valid_xml_file_path_with_tag; + std::string valid_xml_file_path_with_web_tag; std::string invalid_xml_file_path; std::string xsd_file_path; void SetUp() override @@ -38,6 +39,8 @@ class TestComponentValidator : public Test valid_xml_file_path = urdf_package_share_dir + "/urdf/test_hardware_components.urdf"; valid_xml_file_path_with_tag = urdf_package_share_dir + "/urdf/test_hardware_components_xsd_file.urdf"; + valid_xml_file_path_with_web_tag = + urdf_package_share_dir + "/urdf/test_hardware_components_xsd_web.urdf"; invalid_xml_file_path = urdf_package_share_dir + "/urdf/test_hardware_components_with_error.urdf"; xsd_file_path = xsd_package_share_dir + "/schema/ros2_control.xsd"; @@ -77,7 +80,14 @@ TEST_F(TestComponentValidator, validate_invalid_urdf_with_xsd) ASSERT_FALSE(validate_urdf_file_path_with_xsd(invalid_xml_file_path, xsd_file_path)); } -TEST_F(TestComponentValidator, validate_valid_urdf) +TEST_F(TestComponentValidator, validate_valid_urdf_including_xsd_file_tag) { ASSERT_TRUE(validate_urdf_file_with_xsd_tag(valid_xml_file_path_with_tag)); } + +TEST_F(TestComponentValidator, validate_valid_urdf_including_xsd_web_tag) +{ + ASSERT_FALSE(validate_urdf_file_with_xsd_tag( + valid_xml_file_path_with_web_tag)); // TODO(Sachin): Update the test to assert true when xsd + // file is uploaded to web server +} diff --git a/ros2_control_test_assets/CMakeLists.txt b/ros2_control_test_assets/CMakeLists.txt index a181ae3838..b03b804683 100644 --- a/ros2_control_test_assets/CMakeLists.txt +++ b/ros2_control_test_assets/CMakeLists.txt @@ -14,8 +14,11 @@ install( DIRECTORY include/ DESTINATION include/ros2_control_test_assets ) -install( - FILES urdf/test_hardware_components.urdf urdf/test_hardware_components_xsd_file.urdf urdf/test_hardware_components_with_error.urdf +install(FILES + urdf/test_hardware_components.urdf + urdf/test_hardware_components_xsd_file.urdf + urdf/test_hardware_components_xsd_web.urdf + urdf/test_hardware_components_with_error.urdf DESTINATION share/ros2_control_test_assets/urdf ) install(TARGETS ros2_control_test_assets diff --git a/ros2_control_test_assets/urdf/test_hardware_components_xsd_web.urdf b/ros2_control_test_assets/urdf/test_hardware_components_xsd_web.urdf new file mode 100644 index 0000000000..03ccc189ce --- /dev/null +++ b/ros2_control_test_assets/urdf/test_hardware_components_xsd_web.urdf @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + test_hardware_components/TestForceTorqueSensor + 0 + + + + + + + + + + + + + + test_hardware_components/TestTwoJointSystem + + + + -1 + 1 + + + + + + + + + + 0 + 1 + + + + +