Skip to content

Commit

Permalink
Improve test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
sophokles73 committed Jul 30, 2024
1 parent f6d51d2 commit bb3705e
Show file tree
Hide file tree
Showing 6 changed files with 315 additions and 15 deletions.
61 changes: 58 additions & 3 deletions src/communication/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ impl dyn RpcClient {
///
/// * `method` - The URI representing the method to invoke.
/// * `call_options` - Options to include in the request message.
/// * `proto_message` - The protobuf `Message` to include in the request message.
/// * `request_message` - The protobuf `Message` to include in the request message.
///
/// # Returns
///
Expand All @@ -184,13 +184,13 @@ impl dyn RpcClient {
&self,
method: UUri,
call_options: CallOptions,
proto_message: T,
request_message: T,
) -> Result<R, ServiceInvocationError>
where
T: MessageFull,
R: MessageFull,
{
let payload = UPayload::try_from_protobuf(proto_message)
let payload = UPayload::try_from_protobuf(request_message)
.map_err(|e| ServiceInvocationError::InvalidArgument(e.to_string()))?;

let result = self
Expand Down Expand Up @@ -286,3 +286,58 @@ pub trait RpcServer {
request_handler: Arc<dyn RequestHandler>,
) -> Result<(), RegistrationError>;
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use protobuf::well_known_types::wrappers::StringValue;

use crate::{communication::CallOptions, UUri};

use super::*;

#[tokio::test]
async fn test_invoke_proto_method_fails_for_unexpected_return_type() {
let mut rpc_client = MockRpcClient::new();
rpc_client
.expect_invoke_method()
.once()
.returning(|_method, _options, _payload| {
let error = UStatus::fail_with_code(UCode::INTERNAL, "internal error");
let response_payload = UPayload::try_from_protobuf(error).unwrap();
Ok(Some(response_payload))
});
let client: Arc<dyn RpcClient> = Arc::new(rpc_client);
let mut request = StringValue::new();
request.value = "hello".to_string();
let result = client
.invoke_proto_method::<StringValue, StringValue>(
UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(),
CallOptions::for_rpc_request(5_000, None, None, None),
request,
)
.await;
assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_))));
}

#[tokio::test]
async fn test_invoke_proto_method_fails_for_missing_response_payload() {
let mut rpc_client = MockRpcClient::new();
rpc_client
.expect_invoke_method()
.once()
.return_const(Ok(None));
let client: Arc<dyn RpcClient> = Arc::new(rpc_client);
let mut request = StringValue::new();
request.value = "hello".to_string();
let result = client
.invoke_proto_method::<StringValue, StringValue>(
UUri::try_from_parts("", 0x1000, 0x01, 0x0001).unwrap(),
CallOptions::for_rpc_request(5_000, None, None, None),
request,
)
.await;
assert!(result.is_err_and(|e| matches!(e, ServiceInvocationError::InvalidArgument(_))));
}
}
42 changes: 42 additions & 0 deletions src/core/usubscription.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,35 @@ pub use crate::up_core_api::usubscription::{
use crate::{UStatus, UUri};

impl Hash for SubscriberInfo {
/// Creates a hash value based on the URI property.
///
/// # Examples
///
/// ```rust
/// use std::hash::{DefaultHasher, Hash, Hasher};
/// use up_rust::UUri;
/// use up_rust::core::usubscription::SubscriberInfo;
///
/// let mut hasher = DefaultHasher::new();
/// let info = SubscriberInfo {
/// uri: Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into(),
/// ..Default::default()
/// };
///
/// info.hash(&mut hasher);
/// let hash_one = hasher.finish();
///
/// let mut hasher = DefaultHasher::new();
/// let info = SubscriberInfo {
/// uri: Some(UUri::try_from_parts("", 0x1000, 0x02, 0xf100).unwrap()).into(),
/// ..Default::default()
/// };
///
/// info.hash(&mut hasher);
/// let hash_two = hasher.finish();
///
/// assert_ne!(hash_one, hash_two);
/// ```
fn hash<H: Hasher>(&self, state: &mut H) {
self.uri.hash(state);
}
Expand All @@ -39,6 +68,19 @@ impl Eq for SubscriberInfo {}
/// # Returns
///
/// `true` if the given instance is equal to [`SubscriberInfo::default`], `false` otherwise.
///
/// # Examples
///
/// ```rust
/// use up_rust::UUri;
/// use up_rust::core::usubscription::SubscriberInfo;
///
/// let mut info = SubscriberInfo::default();
/// assert!(info.is_empty());
///
/// info.uri = Some(UUri::try_from_parts("", 0x1000, 0x01, 0x9a00).unwrap()).into();
/// assert!(!info.is_empty());
/// ```
impl SubscriberInfo {
pub fn is_empty(&self) -> bool {
self.eq(&SubscriberInfo::default())
Expand Down
103 changes: 100 additions & 3 deletions src/umessage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,23 +220,32 @@ pub(crate) fn deserialize_protobuf_bytes<T: MessageFull + Default>(

#[cfg(test)]
mod test {
use protobuf::well_known_types::{any::Any, wrappers::StringValue};
use std::io;

use crate::UStatus;
use protobuf::well_known_types::{any::Any, duration::Duration, wrappers::StringValue};

use crate::{UAttributes, UStatus};

use super::*;

#[test]
fn test_deserialize_protobuf_bytes_succeeds() {
let mut data = StringValue::new();
data.value = "hello world".to_string();
let any = Any::pack(&data).unwrap();
let any = Any::pack(&data.clone()).unwrap();
let buf: Bytes = any.write_to_bytes().unwrap().into();

let result = deserialize_protobuf_bytes::<StringValue>(
&buf,
&UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY,
);
assert!(result.is_ok_and(|v| v.value == *"hello world"));

let result = deserialize_protobuf_bytes::<StringValue>(
&data.write_to_bytes().unwrap().into(),
&UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF,
);
assert!(result.is_ok_and(|v| v.value == *"hello world"));
}

#[test]
Expand All @@ -251,4 +260,92 @@ mod test {
);
assert!(result.is_err_and(|e| matches!(e, UMessageError::PayloadError(_))));
}

#[test]
fn test_deserialize_protobuf_bytes_fails_for_unsupported_format() {
let result = deserialize_protobuf_bytes::<UStatus>(
&"hello".into(),
&UPayloadFormat::UPAYLOAD_FORMAT_TEXT,
);
assert!(result.is_err_and(|e| matches!(e, UMessageError::PayloadError(_))));
}

#[test]
fn test_deserialize_protobuf_bytes_fails_for_invalid_encoding() {
let any = Any {
type_url: "type.googleapis.com/google.protobuf.Duration".to_string(),
value: vec![0x0A],
..Default::default()
};
let buf = any.write_to_bytes().unwrap();
let result = deserialize_protobuf_bytes::<Duration>(
&buf.into(),
&UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY,
);
assert!(result.is_err_and(|e| matches!(e, UMessageError::DataSerializationError(_))))
}

#[test]
fn extract_payload_succeeds() {
let payload = StringValue {
value: "hello".to_string(),
..Default::default()
};
let buf = Any::pack(&payload)
.and_then(|a| a.write_to_bytes())
.unwrap();
let msg = UMessage {
attributes: Some(UAttributes {
payload_format: UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY.into(),
..Default::default()
})
.into(),
payload: Some(buf.into()),
..Default::default()
};
assert!(msg
.extract_protobuf::<StringValue>()
.is_ok_and(|v| v.value == *"hello"));
}

#[test]
fn extract_payload_fails_for_no_payload() {
let msg = UMessage {
attributes: Some(UAttributes {
payload_format: UPayloadFormat::UPAYLOAD_FORMAT_PROTOBUF_WRAPPED_IN_ANY.into(),
..Default::default()
})
.into(),
..Default::default()
};
assert!(msg
.extract_protobuf::<StringValue>()
.is_err_and(|e| matches!(e, UMessageError::PayloadError(_))));
}

#[test]
fn test_from_attributes_error() {
let attributes_error = UAttributesError::validation_error("failed to validate");
let message_error = UMessageError::from(attributes_error);
assert!(matches!(
message_error,
UMessageError::AttributesValidationError(UAttributesError::ValidationError(_))
));
}

#[test]
fn test_from_protobuf_error() {
let protobuf_error = protobuf::Error::from(io::Error::last_os_error());
let message_error = UMessageError::from(protobuf_error);
assert!(matches!(
message_error,
UMessageError::DataSerializationError(_)
));
}

#[test]
fn test_from_error_msg() {
let message_error = UMessageError::from("an error occurred");
assert!(matches!(message_error, UMessageError::PayloadError(_)));
}
}
60 changes: 56 additions & 4 deletions src/uri.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ impl From<&UUri> for String {
/// # Returns
///
/// The output of [`UUri::to_uri`] without inlcuding the uProtocol scheme.
///
/// # Examples
///
/// ```rust
/// use up_rust::UUri;
///
/// let uuri = UUri {
/// authority_name: String::from("VIN.vehicles"),
/// ue_id: 0x0000_800A,
/// ue_version_major: 0x02,
/// resource_id: 0x0000_1a50,
/// ..Default::default()
/// };
///
/// let uri_string = String::from(&uuri);
/// assert_eq!(uri_string, "//VIN.vehicles/800A/2/1A50");
/// ````
fn from(uri: &UUri) -> Self {
UUri::to_uri(uri, false)
}
Expand Down Expand Up @@ -222,6 +239,23 @@ impl TryFrom<String> for UUri {
/// # Returns
///
/// A `Result` containing either the `UUri` representation of the URI or a `SerializationError`.
///
/// # Examples
///
/// ```rust
/// use up_rust::UUri;
///
/// let uri = UUri {
/// authority_name: "".to_string(),
/// ue_id: 0x001A_8000,
/// ue_version_major: 0x02,
/// resource_id: 0x0000_1a50,
/// ..Default::default()
/// };
///
/// let uri_from = UUri::try_from("/1A8000/2/1A50".to_string()).unwrap();
/// assert_eq!(uri, uri_from);
/// ````
fn try_from(uri: String) -> Result<Self, Self::Error> {
UUri::from_str(uri.as_str())
}
Expand Down Expand Up @@ -328,7 +362,7 @@ impl UUri {
/// ```rust
/// use up_rust::UUri;
///
/// assert!(UUri::try_from_parts("vin", 0x5a6b, 0x01, 0x0001).is_ok());
/// assert!(UUri::try_from_parts("vin", 0x0000_5a6b, 0x01, 0x0001).is_ok());
/// ```
// [impl->dsn~uri-authority-name-length~1]
// [impl->dsn~uri-host-only~2]
Expand Down Expand Up @@ -390,6 +424,16 @@ impl UUri {
/// # Returns
///
/// 'true' if this URI is equal to `UUri::default()`, `false` otherwise.
///
/// # Examples
///
/// ```rust
/// use up_rust::UUri;
///
/// let uuri = UUri::try_from_parts("MYVIN", 0xa13b, 0x01, 0x7f4e).unwrap();
/// assert!(!uuri.is_empty());
/// assert!(UUri::default().is_empty());
/// ```
pub fn is_empty(&self) -> bool {
self.eq(&UUri::default())
}
Expand Down Expand Up @@ -786,6 +830,13 @@ mod tests {
#[test_case("up://MYVIN/1A23/1/a13#foobar"; "for URI with fragement")]
#[test_case("up://MYVIN:1000/1A23/1/A13"; "for authority with port")]
#[test_case("up://user:pwd@MYVIN/1A23/1/A13"; "for authority with userinfo")]
#[test_case("5up://MYVIN/55A1/1/1"; "for invalid scheme")]
#[test_case("up://MY#VIN/55A1/1/1"; "for invalid authority")]
#[test_case("up://MYVIN/55T1/1/1"; "for non-hex entity ID")]
#[test_case("up://MYVIN/55A1//1"; "for empty version")]
#[test_case("up://MYVIN/55A1/T/1"; "for non-hex version")]
#[test_case("up://MYVIN/55A1/1/"; "for empty resource ID")]
#[test_case("up://MYVIN/55A1/1/1T"; "for non-hex resource ID")]
fn test_from_string_fails(string: &str) {
let parsing_result = UUri::from_str(string);
assert!(parsing_result.is_err());
Expand Down Expand Up @@ -903,9 +954,10 @@ mod tests {
}

// [utest->dsn~uri-host-only~2]
#[test_case("MYVIN:1000"; "for authority with port")]
#[test_case("user:pwd@MYVIN"; "for authority with userinfo")]
fn test_try_from_parts_fails(authority: &str) {
#[test_case("MYVIN:1000"; "with port")]
#[test_case("user:pwd@MYVIN"; "with userinfo")]
#[test_case("MY%VIN"; "with reserved character")]
fn test_try_from_parts_fails_for_invalid_authority(authority: &str) {
assert!(UUri::try_from_parts(authority, 0xa100, 0x01, 0x6501).is_err());
}

Expand Down
Loading

0 comments on commit bb3705e

Please sign in to comment.