From 27d00e2b4d0fb2bb81ae23476411f8669ab06871 Mon Sep 17 00:00:00 2001 From: carter Date: Tue, 3 Dec 2024 10:38:23 -0700 Subject: [PATCH] Add a basic mock implementation of generic traits --- Cargo.lock | 27 +++- Cargo.toml | 1 + roslibrust/src/lib.rs | 6 +- roslibrust_mock/Cargo.toml | 16 +++ roslibrust_mock/README.md | 3 + roslibrust_mock/src/lib.rs | 259 +++++++++++++++++++++++++++++++++++++ 6 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 roslibrust_mock/Cargo.toml create mode 100644 roslibrust_mock/README.md create mode 100644 roslibrust_mock/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 6d8217b3..1c4a6b7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "abort-on-drop" @@ -165,6 +165,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1709,6 +1718,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "roslibrust_mock" +version = "0.1.0" +dependencies = [ + "bincode", + "log", + "roslibrust", + "roslibrust_codegen", + "roslibrust_codegen_macro", + "tokio", +] + [[package]] name = "roslibrust_serde_rosmsg" version = "0.3.0" @@ -2227,9 +2248,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index ebfd533d..885ceea8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,6 @@ members = [ "roslibrust_codegen_macro", "roslibrust_genmsg", "roslibrust_test", + "roslibrust_mock", ] resolver = "2" diff --git a/roslibrust/src/lib.rs b/roslibrust/src/lib.rs index 78927e8c..21423a37 100644 --- a/roslibrust/src/lib.rs +++ b/roslibrust/src/lib.rs @@ -100,7 +100,11 @@ mod rosbridge; pub use rosbridge::*; -use roslibrust_codegen::RosServiceType; + +// Re export the codegen traits so that crates that only interact with abstract messages +// don't need to depend on the codegen crate +pub use roslibrust_codegen::RosMessageType; +pub use roslibrust_codegen::RosServiceType; #[cfg(feature = "rosapi")] pub mod rosapi; diff --git a/roslibrust_mock/Cargo.toml b/roslibrust_mock/Cargo.toml new file mode 100644 index 00000000..beba67ca --- /dev/null +++ b/roslibrust_mock/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "roslibrust_mock" +version = "0.1.0" +edition = "2021" + +[dependencies] +roslibrust = { path = "../roslibrust", features = ["topic_provider"] } +tokio = { version = "1.41", features = ["sync", "rt-multi-thread", "macros"] } +# Used for serializing messages +bincode = "1.3" +# We add logging to aid in debugging tests +log = "0.4" + +[dev-dependencies] +roslibrust_codegen = { path = "../roslibrust_codegen" } +roslibrust_codegen_macro = { path = "../roslibrust_codegen_macro" } diff --git a/roslibrust_mock/README.md b/roslibrust_mock/README.md new file mode 100644 index 00000000..774c73a6 --- /dev/null +++ b/roslibrust_mock/README.md @@ -0,0 +1,3 @@ +# RosLibRust Mock + +A mock implementation of roslibrust's generic traits for use in building automated testing of nodes. diff --git a/roslibrust_mock/src/lib.rs b/roslibrust_mock/src/lib.rs new file mode 100644 index 00000000..b8f2ba4c --- /dev/null +++ b/roslibrust_mock/src/lib.rs @@ -0,0 +1,259 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use roslibrust::topic_provider::*; +use roslibrust::RosLibRustError; +use roslibrust::RosLibRustResult; +use roslibrust::RosMessageType; + +use roslibrust::RosServiceType; +use roslibrust::ServiceFn; +use tokio::sync::broadcast as Channel; +use tokio::sync::RwLock; + +use log::*; + +type TypeErasedCallback = Arc< + dyn Fn(Vec) -> Result, Box> + + Send + + Sync + + 'static, +>; + +pub struct MockRos { + // We could probably achieve some fancier type erasure than actually serializing the data + // but this ends up being pretty simple + topics: RwLock>, Channel::Receiver>)>>, + services: RwLock>, +} + +impl MockRos { + pub fn new() -> Self { + Self { + topics: RwLock::new(BTreeMap::new()), + services: RwLock::new(BTreeMap::new()), + } + } +} + +// This is a very basic mocking of sending and receiving messages over topics +// It does not implement automatic shutdown of topics on dropping +impl TopicProvider for MockRos { + type Publisher = MockPublisher; + type Subscriber = MockSubscriber; + + async fn advertise( + &self, + topic: &str, + ) -> RosLibRustResult> { + // Check if we already have this channel + { + let topics = self.topics.read().await; + if let Some((sender, _)) = topics.get(topic) { + debug!("Issued new publisher to existing topic {}", topic); + return Ok(MockPublisher { + sender: sender.clone(), + _marker: Default::default(), + }); + } + } // Drop read lock here + // Create a new channel + let tx_rx = Channel::channel(10); + let tx_copy = tx_rx.0.clone(); + let mut topics = self.topics.write().await; + topics.insert(topic.to_string(), tx_rx); + debug!("Created new publisher and channel for topic {}", topic); + Ok(MockPublisher { + sender: tx_copy, + _marker: Default::default(), + }) + } + + async fn subscribe( + &self, + topic: &str, + ) -> RosLibRustResult> { + // Check if we already have this channel + { + let topics = self.topics.read().await; + if let Some((_, receiver)) = topics.get(topic) { + debug!("Issued new subscriber to existing topic {}", topic); + return Ok(MockSubscriber { + receiver: receiver.resubscribe(), + _marker: Default::default(), + }); + } + } // Drop read lock here + // Create a new channel + let tx_rx = Channel::channel(10); + let rx_copy = tx_rx.1.resubscribe(); + let mut topics = self.topics.write().await; + topics.insert(topic.to_string(), tx_rx); + debug!("Created new subscriber and channel for topic {}", topic); + Ok(MockSubscriber { + receiver: rx_copy, + _marker: Default::default(), + }) + } +} + +pub struct MockServiceClient { + callback: TypeErasedCallback, + _marker: std::marker::PhantomData, +} + +impl Service for MockServiceClient { + async fn call(&self, request: &T::Request) -> RosLibRustResult { + let data = bincode::serialize(request) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + let response = (self.callback)(data) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + let response = bincode::deserialize(&response[..]) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + Ok(response) + } +} + +impl ServiceProvider for MockRos { + type ServiceClient = MockServiceClient; + type ServiceServer = (); + + async fn service_client( + &self, + topic: &str, + ) -> RosLibRustResult> { + let services = self.services.read().await; + if let Some(callback) = services.get(topic) { + return Ok(MockServiceClient { + callback: callback.clone(), + _marker: Default::default(), + }); + } + Err(RosLibRustError::Disconnected) + } + + async fn advertise_service( + &self, + topic: &str, + server: F, + ) -> RosLibRustResult + where + F: ServiceFn, + { + // Type erase the service function here + let erased_closure = + move |message: Vec| -> Result, Box> { + let request = bincode::deserialize(&message[..]) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + let response = server(request)?; + let bytes = bincode::serialize(&response) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + Ok(bytes) + }; + let erased_closure = Arc::new(erased_closure); + let mut services = self.services.write().await; + services.insert(topic.to_string(), erased_closure); + + // We technically need to hand back a token that shuts the service down here + // But we haven't implemented that yet in this mock + Ok(()) + } +} + +pub struct MockPublisher { + sender: Channel::Sender>, + _marker: std::marker::PhantomData, +} + +impl Publish for MockPublisher { + async fn publish(&self, data: &T) -> RosLibRustResult<()> { + let data = bincode::serialize(data) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + self.sender + .send(data) + .map_err(|_e| RosLibRustError::Disconnected)?; + debug!("Sent data on topic {}", T::ROS_TYPE_NAME); + Ok(()) + } +} + +pub struct MockSubscriber { + receiver: Channel::Receiver>, + _marker: std::marker::PhantomData, +} + +impl Subscribe for MockSubscriber { + async fn next(&mut self) -> RosLibRustResult { + let data = self + .receiver + .recv() + .await + .map_err(|_| RosLibRustError::Disconnected)?; + let msg = bincode::deserialize(&data[..]) + .map_err(|e| RosLibRustError::SerializationError(e.to_string()))?; + debug!("Received data on topic {}", T::ROS_TYPE_NAME); + Ok(msg) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + roslibrust_codegen_macro::find_and_generate_ros_messages!( + "assets/ros1_common_interfaces/std_msgs", + "assets/ros1_common_interfaces/ros_comm_msgs/std_srvs" + ); + + #[tokio::test(flavor = "multi_thread")] + async fn test_mock_topics() { + let mock_ros = MockRos::new(); + + let pub_handle = mock_ros + .advertise::("test_topic") + .await + .unwrap(); + let mut sub_handle = mock_ros + .subscribe::("test_topic") + .await + .unwrap(); + + let msg = std_msgs::String { + data: "Hello, world!".to_string(), + }; + + pub_handle.publish(&msg).await.unwrap(); + + let received_msg = sub_handle.next().await.unwrap(); + + assert_eq!(msg, received_msg); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_mock_services() { + let mock_topics = MockRos::new(); + + let server_fn = |request: std_srvs::SetBoolRequest| { + Ok(std_srvs::SetBoolResponse { + success: request.data, + message: "You set my bool!".to_string(), + }) + }; + + mock_topics + .advertise_service::("test_service", server_fn) + .await + .unwrap(); + + let client = mock_topics + .service_client::("test_service") + .await + .unwrap(); + + let request = std_srvs::SetBoolRequest { data: true }; + + let response = client.call(&request).await.unwrap(); + assert_eq!(response.success, true); + assert_eq!(response.message, "You set my bool!"); + } +}