diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1fac97d..632aabb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,46 @@ jobs: - name: Run tests run: cargo test --verbose + system_tests_with_ros2_humble: + name: System tests with ROS 2 Humble + runs-on: ubuntu-latest + container: + image: rostooling/setup-ros-docker:ubuntu-jammy-ros-humble-ros-base-latest + steps: + - uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: humble + + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Install ACL + run: sudo apt-get -y install libacl1-dev + + - name: Run ROS tests (enable feature ros_test) + shell: bash + run: "source /opt/ros/humble/setup.bash && cargo test --features ros_test --verbose" + + system_tests_with_ros2_jazzy: + name: System tests with ROS 2 Jazzy + runs-on: ubuntu-latest + container: + image: rostooling/setup-ros-docker:ubuntu-noble-ros-jazzy-ros-base-latest + steps: + - uses: ros-tooling/setup-ros@v0.7 + with: + required-ros-distributions: jazzy + + - uses: actions/checkout@v4 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Install ACL + run: sudo apt-get -y install libacl1-dev + + - name: Run ROS tests (enable feature ros_test) + shell: bash + run: "source /opt/ros/jazzy/setup.bash && cargo test --features ros_test --verbose" + # NOTE: In GitHub repository settings, the "Require status checks to pass # before merging" branch protection rule ensures that commits are only merged # from branches where specific status checks have passed. These checks are diff --git a/Cargo.lock b/Cargo.lock index de2b427..f9259fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,6 +570,28 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 1.0.109", + "which", +] + [[package]] name = "bindgen" version = "0.69.5" @@ -956,6 +978,25 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -1008,7 +1049,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e085999238629d13a7a19e4dc7a6e61505bcb2033f6a1bed24bff35035c47c4" dependencies = [ "bincode", - "bindgen", + "bindgen 0.69.5", "cmake", "derivative", "libc", @@ -1258,6 +1299,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "force-send-sync" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32688dcc448aa684426ecc398f9af78c55b0769515ccd12baa565daf6f32feb6" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2180,6 +2227,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" +dependencies = [ + "memchr", +] + [[package]] name = "outref" version = "0.5.1" @@ -2227,6 +2283,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2581,6 +2643,99 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2r" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f7b62c21b28403706844629ce89de2869677d83f33bdef615b32de56f0b5a0" +dependencies = [ + "force-send-sync", + "futures", + "indexmap", + "lazy_static", + "log", + "phf", + "prettyplease", + "proc-macro2", + "quote", + "r2r_actions", + "r2r_common", + "r2r_macros", + "r2r_msg_gen", + "r2r_rcl", + "rayon", + "serde", + "serde_json", + "syn 2.0.77", + "thiserror", + "uuid", +] + +[[package]] +name = "r2r_actions" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4e672304c3e50c75f39b8c97f97b87f7aef89fc43091071d7c9ab2a2ecf057" +dependencies = [ + "bindgen 0.63.0", + "r2r_common", + "r2r_msg_gen", + "r2r_rcl", +] + +[[package]] +name = "r2r_common" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4dee1e660f88ae6f8caf368db2b616f178bb7d57332e4ac157beeb6bbe57fe5" +dependencies = [ + "bindgen 0.63.0", + "os_str_bytes", + "regex", + "sha2 0.10.8", +] + +[[package]] +name = "r2r_macros" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d47d4c56155db63630574a4f0a2426561ff37c18a2021affdf8611b712b7a892" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "r2r_msg_gen" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec7a0473fa69271526ac1eeb5b1279b94e76cbfccec8ba9e32a18f0fb313de4" +dependencies = [ + "bindgen 0.63.0", + "force-send-sync", + "itertools 0.10.5", + "phf", + "proc-macro2", + "quote", + "r2r_common", + "r2r_rcl", + "rayon", + "syn 2.0.77", +] + +[[package]] +name = "r2r_rcl" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d79af52bdec385b7b732e78e5b8c20631c8f6968bd37316a84e6adaae41591" +dependencies = [ + "bindgen 0.63.0", + "paste", + "r2r_common", + "widestring", +] + [[package]] name = "rand" version = "0.7.3" @@ -2652,6 +2807,26 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.4" @@ -3964,6 +4139,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", + "serde", ] [[package]] @@ -4189,6 +4365,12 @@ dependencies = [ "rustix 0.38.37", ] +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + [[package]] name = "winapi" version = "0.3.9" @@ -4779,6 +4961,7 @@ dependencies = [ "git-version", "hex", "lazy_static", + "r2r", "regex", "rustc_version 0.4.1", "serde", @@ -4787,6 +4970,7 @@ dependencies = [ "tokio", "tracing", "zenoh", + "zenoh-config", "zenoh-ext", "zenoh-plugin-trait", ] diff --git a/Cargo.toml b/Cargo.toml index 23ce06a..c88085c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,9 @@ zenoh-plugin-ros2dds = { version = "1.0.0-dev", path = "zenoh-plugin-ros2dds/", zenoh-plugin-rest = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false, features=["static_plugin"]} zenoh-plugin-trait = { version = "1.0.0-dev", git = "https://github.com/eclipse-zenoh/zenoh.git", branch = "main", default-features = false } +# r2r is for tests only. See in in zenoh-plugins-ros2dds/Cargo.toml +r2r = "0.9" + [profile.release] codegen-units = 1 diff --git a/zenoh-plugin-ros2dds/Cargo.toml b/zenoh-plugin-ros2dds/Cargo.toml index 95aae56..7d12da1 100644 --- a/zenoh-plugin-ros2dds/Cargo.toml +++ b/zenoh-plugin-ros2dds/Cargo.toml @@ -31,6 +31,7 @@ default = ["dynamic_plugin"] stats = ["zenoh/stats"] dynamic_plugin = [] dds_shm = ["cyclors/iceoryx"] +ros_test = ["r2r"] [dependencies] async-trait = { workspace = true } @@ -50,9 +51,16 @@ test-case = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } zenoh = { workspace = true } +zenoh-config = { workspace = true } zenoh-ext = { workspace = true } zenoh-plugin-trait = { workspace = true } +# r2r is for tests only. It has to be optional because its building requires a full ROS 2 environement +# However "optional" is not supported for "dev-dependencies". +# Hence it's declared as a dependency but active only with "ros_test" feature +r2r = { workspace = true, optional = true } + + [build-dependencies] rustc_version = { workspace = true } diff --git a/zenoh-plugin-ros2dds/src/ros_discovery.rs b/zenoh-plugin-ros2dds/src/ros_discovery.rs index 170a538..e279871 100644 --- a/zenoh-plugin-ros2dds/src/ros_discovery.rs +++ b/zenoh-plugin-ros2dds/src/ros_discovery.rs @@ -582,21 +582,23 @@ where mod tests { #[test] - #[ignore] - // Test ignored as it cannot be run at the same time than test_serde_after_iron() - // Both need different ROS_DISTRO env var, that cannot be changed between the 2 tests - // Run this test individually or with `cargo test -- --ignored`` fn test_serde_prior_to_iron() { use std::str::FromStr; use super::*; use crate::ros2_utils::get_ros_distro; + let distro = get_ros_distro(); + println!("ROS_DISTRO={}", distro); + if !ros_distro_is_less_than("iron") { + println!("The current ROS Distro is not prior to iron. Skip the test."); + return; + } + // ros_discovery_message sent by a component_container node on Humble started as such: // - ros2 run rclcpp_components component_container --ros-args --remap __ns:=/TEST // - ros2 component load /TEST/ComponentManager composition composition::Listener // - ros2 component load /TEST/ComponentManager composition composition::Talker - std::env::set_var("ROS_DISTRO", "humble"); let ros_discovery_info_cdr: Vec = hex::decode( "000100000110de17b1eaf995400c9ac8000001c1000000000000000003000000\ 060000002f5445535400000011000000436f6d706f6e656e744d616e61676572\ @@ -636,7 +638,6 @@ mod tests { ) .unwrap(); - println!("ROS_DISTRO={}", get_ros_distro()); let part_info: ParticipantEntitiesInfo = cdr::deserialize(&ros_discovery_info_cdr).unwrap(); println!("{:?}", part_info); @@ -675,11 +676,17 @@ mod tests { use super::*; use crate::ros2_utils::get_ros_distro; + let distro = get_ros_distro(); + println!("ROS_DISTRO={}", distro); + if ros_distro_is_less_than("iron") { + println!("The current ROS Distro is prior to iron. Skip the test."); + return; + } + // ros_discovery_message sent by a component_container node on Iron started as such: // - ros2 run rclcpp_components component_container --ros-args --remap __ns:=/TEST // - ros2 component load /TEST/ComponentManager composition composition::Listener // - ros2 component load /TEST/ComponentManager composition composition::Talker - std::env::set_var("ROS_DISTRO", "iron"); let ros_discovery_info_cdr: Vec = hex::decode( "00010000010f20a26b2fbd8000000000000001c103000000060000002f544553\ 5400000011000000436f6d706f6e656e744d616e616765720000000005000000\ @@ -711,7 +718,6 @@ mod tests { ) .unwrap(); - println!("ROS_DISTRO={}", get_ros_distro()); let part_info: ParticipantEntitiesInfo = cdr::deserialize(&ros_discovery_info_cdr).unwrap(); println!("{:?}", part_info); diff --git a/zenoh-plugin-ros2dds/tests/test.rs b/zenoh-plugin-ros2dds/tests/test.rs new file mode 100644 index 0000000..28edbd0 --- /dev/null +++ b/zenoh-plugin-ros2dds/tests/test.rs @@ -0,0 +1,138 @@ +// +// Copyright (c) 2024 ZettaScale Technology +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// +// Contributors: +// ZettaScale Zenoh Team, +// + +#[cfg(feature = "ros_test")] +mod ros_test { + use std::{sync::mpsc::channel, time::Duration}; + + use futures::StreamExt; + use r2r::{self, QosProfile}; + use zenoh::{ + config::Config, + internal::{plugins::PluginsManager, runtime::RuntimeBuilder}, + }; + use zenoh_config::ModeDependentValue; + + // The test topic + const TEST_TOPIC: &str = "test_topic"; + // The test TEST_PAYLOAD + const TEST_PAYLOAD: &str = "Hello World"; + + fn init_env() { + std::env::set_var("RMW_IMPLEMENTATION", "rmw_cyclonedds_cpp"); + } + + async fn create_bridge() { + let mut plugins_mgr = PluginsManager::static_plugins_only(); + plugins_mgr + .declare_static_plugin::("ros2dds", true); + let mut config = Config::default(); + config.insert_json5("plugins/ros2dds", "{}").unwrap(); + config + .timestamping + .set_enabled(Some(ModeDependentValue::Unique(true))) + .unwrap(); + config.adminspace.set_enabled(true).unwrap(); + config.plugins_loading.set_enabled(true).unwrap(); + let mut runtime = RuntimeBuilder::new(config) + .plugins_manager(plugins_mgr) + .build() + .await + .unwrap(); + runtime.start().await.unwrap(); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_zenoh_pub_ros_sub() { + init_env(); + let (tx, rx) = channel(); + + // Create zenoh-bridge-ros2dds + tokio::spawn(create_bridge()); + + // ROS subscriber + let ctx = r2r::Context::create().unwrap(); + let mut node = r2r::Node::create(ctx, "ros_sub", "").unwrap(); + let subscriber = node + .subscribe::( + &format!("/{}", TEST_TOPIC), + QosProfile::default(), + ) + .unwrap(); + + // Zenoh publisher + let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + let publisher = session.declare_publisher(TEST_TOPIC).await.unwrap(); + + // Wait for the environment to be ready + tokio::time::sleep(Duration::from_secs(1)).await; + + // Publish Zenoh message + let buf = cdr::serialize::<_, _, cdr::CdrLe>(TEST_PAYLOAD, cdr::size::Infinite).unwrap(); + publisher.put(buf).await.unwrap(); + + // Check ROS subscriber will receive the data + tokio::spawn(async move { + subscriber + .for_each(|msg| { + tx.send(msg.data).unwrap(); + futures::future::ready(()) + }) + .await + }); + node.spin_once(std::time::Duration::from_millis(100)); + let data = rx + .recv_timeout(Duration::from_secs(3)) + .expect("Receiver timeout"); + assert_eq!(data, TEST_PAYLOAD); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_ros_pub_zenoh_sub() { + init_env(); + // Create zenoh-bridge-ros2dds + tokio::spawn(create_bridge()); + + // Zenoh subscriber + let session = zenoh::open(zenoh::Config::default()).await.unwrap(); + let subscriber = session.declare_subscriber(TEST_TOPIC).await.unwrap(); + + // ROS publisher + let ctx = r2r::Context::create().unwrap(); + let mut node = r2r::Node::create(ctx, "ros_pub", "").unwrap(); + let publisher = node + .create_publisher(&format!("/{}", TEST_TOPIC), QosProfile::default()) + .unwrap(); + let msg = r2r::std_msgs::msg::String { + data: TEST_PAYLOAD.into(), + }; + + // Wait for the environment to be ready + tokio::time::sleep(Duration::from_secs(1)).await; + + // Publish ROS message + publisher.publish(&msg).unwrap(); + + // Check Zenoh subscriber will receive the data + tokio::time::timeout(Duration::from_secs(3), async { + let sample = subscriber.recv_async().await.unwrap(); + let result: Result = + cdr::deserialize_from(sample.payload().reader(), cdr::size::Infinite); + let recv_data = result.expect("Fail to receive data"); + assert_eq!(recv_data, TEST_PAYLOAD); + }) + .await + .expect("Timeout: Zenoh subscriber didn't receive any ROS message."); + } +}