From 8c0a0e45faeb6e87ff2aaed10b06f653fd326833 Mon Sep 17 00:00:00 2001 From: Chet Nichols III Date: Sat, 20 Jun 2026 08:20:34 -0700 Subject: [PATCH] test(mqttea,dpf): fold per-case error and node-label tests into tables `mqttea`'s error tests had grown to ~38 near-identical functions. They now collapse into two tables -- one over the category predicates each error answers, one over `Display`/`source` rendering -- with the genuinely distinct constructor, equality, and `Default` tests kept as they were. The same pass drops tests that exercised the stdlib rather than our error type (the `Result`/`Option` ergonomics tests) and two timing assertions that belong in a bench, not the suite; the `Send + Sync` compile guard stays, since it pins a real property of the error type rather than stdlib behavior. `dpf`'s six `verify_node_labels_*` tests become one `check_cases_async` table seeded by a small `Seeded` enum, and two `init_config` tests go -- one duplicated the defaults table in `types.rs`, the other only re-read fields it had just set. The `dpu_phase` `Display`-matches-`as_ref` check folds into the table that already enumerates every variant. A net reduction of ~200 test lines, no production change. The predicate table asserts the full category vector per row, so coverage is a touch stronger than before; verified per crate with `cargo test`. Signed-off-by: Chet Nichols III --- crates/dpf/src/sdk.rs | 335 ++++++++----------- crates/dpf/src/types.rs | 42 +-- crates/mqttea/tests/errors.rs | 609 ++++++++++++++-------------------- crates/mqttea/tests/stats.rs | 29 -- 4 files changed, 404 insertions(+), 611 deletions(-) diff --git a/crates/dpf/src/sdk.rs b/crates/dpf/src/sdk.rs index b11c19efe7..c879530d30 100644 --- a/crates/dpf/src/sdk.rs +++ b/crates/dpf/src/sdk.rs @@ -2668,30 +2668,6 @@ mod tests { assert_eq!(result, "old-password"); } - #[tokio::test] - async fn test_init_config_defaults() { - let config = InitDpfResourcesConfig::default(); - assert!(config.bfb_url.is_empty()); - assert_eq!(config.deployment_name, "dpu-deployment"); - assert_eq!(config.flavor_name, crate::flavor::DEFAULT_FLAVOR_NAME); - assert!(config.services.is_empty()); - } - - #[tokio::test] - async fn test_init_config_custom() { - let config = InitDpfResourcesConfig { - bfb_url: "http://example.com/test.bfb".to_string(), - deployment_name: "my-deployment".to_string(), - flavor_name: "my-flavor".to_string(), - services: vec![], - proxy: None, - }; - - assert_eq!(config.bfb_url, "http://example.com/test.bfb"); - assert_eq!(config.deployment_name, "my-deployment"); - assert_eq!(config.flavor_name, "my-flavor"); - } - fn terminating_timestamp() -> k8s_openapi::apimachinery::pkg::apis::meta::v1::Time { k8s_openapi::apimachinery::pkg::apis::meta::v1::Time( k8s_openapi::jiff::Timestamp::UNIX_EPOCH, @@ -3128,186 +3104,149 @@ mod tests { ); } + /// `verify_node_labels` against a `TestLabeler` (which requires the single + /// label `test/node=true`): a node carries the current labels only when its + /// `metadata.labels` is a superset of the labeler's `node_labels()`. A + /// missing node verifies as `true` because it will be (re)created with the + /// current labels. Each row seeds one node state and asserts the verdict. + /// + /// Folds the six former `test_verify_node_labels_*` cases. #[tokio::test] - async fn test_verify_node_labels_current_labels_returns_true() { - let mock = SdkMock::new(); - let sdk = DpfSdkBuilder::new(mock.clone(), TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); - - let info = DpuNodeInfo { - node_id: "host-001".to_string(), - host_bmc_ip: "10.0.0.1".parse().unwrap(), - device_ids: vec!["dpu-001".to_string()], - }; - sdk.register_dpu_node(info).await.unwrap(); - - assert!(sdk.verify_node_labels("node-host-001").await.unwrap()); - } - - #[tokio::test] - async fn test_verify_node_labels_missing_node_returns_true() { - let mock = SdkMock::new(); - let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); - - assert!( - sdk.verify_node_labels("node-does-not-exist").await.unwrap(), - "non-existent node should return true (will be created with current labels)" - ); - } - - #[tokio::test] - async fn test_verify_node_labels_stale_labels_returns_false() { - let mock = SdkMock::new(); - - let stale_node = DPUNode { - metadata: ObjectMeta { - name: Some("node-host-001".to_string()), - namespace: Some(TEST_NAMESPACE.to_string()), - labels: Some(BTreeMap::from([( - "old/stale-label".to_string(), - "true".to_string(), - )])), - ..Default::default() - }, - spec: DpuNodeSpec { - dpus: Some(vec![]), - node_dms_address: None, - node_reboot_method: None, - }, - status: None, - }; - mock.nodes - .write() - .unwrap() - .insert(SdkMock::key(&stale_node), stale_node); - - let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); - - assert!( - !sdk.verify_node_labels("node-host-001").await.unwrap(), - "node with stale labels should return false" - ); - } - - #[tokio::test] - async fn test_verify_node_labels_no_labels_returns_false() { - let mock = SdkMock::new(); - - let bare_node = DPUNode { - metadata: ObjectMeta { - name: Some("node-host-001".to_string()), - namespace: Some(TEST_NAMESPACE.to_string()), - labels: None, - ..Default::default() - }, - spec: DpuNodeSpec { - dpus: Some(vec![]), - node_dms_address: None, - node_reboot_method: None, - }, - status: None, - }; - mock.nodes - .write() - .unwrap() - .insert(SdkMock::key(&bare_node), bare_node); - - let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); - - assert!( - !sdk.verify_node_labels("node-host-001").await.unwrap(), - "node with no labels should return false when labeler expects labels" - ); - } - - #[tokio::test] - async fn test_verify_node_labels_superset_returns_true() { - let mock = SdkMock::new(); - - let superset_node = DPUNode { - metadata: ObjectMeta { - name: Some("node-host-001".to_string()), - namespace: Some(TEST_NAMESPACE.to_string()), - labels: Some(BTreeMap::from([ - ("test/node".to_string(), "true".to_string()), - ("extra/label".to_string(), "extra-value".to_string()), - ])), - ..Default::default() - }, - spec: DpuNodeSpec { - dpus: Some(vec![]), - node_dms_address: None, - node_reboot_method: None, - }, - status: None, - }; - mock.nodes - .write() - .unwrap() - .insert(SdkMock::key(&superset_node), superset_node); + async fn verify_node_labels_against_seeded_node() { + use carbide_test_support::Outcome::Yields; + use carbide_test_support::{Case, check_cases_async}; + + /// What the mock's node store holds before the check runs. + enum Seeded { + /// No node at all under the queried name. + Absent, + /// A node created through `register_dpu_node`, so it carries + /// whatever labels the labeler currently produces. + RegisteredByLabeler, + /// A node inserted directly with these `metadata.labels` + /// (`None` means the labels field is absent entirely). + WithLabels(Option>), + } - let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); + struct Row { + /// Pre-existing node state in the mock. + seeded: Seeded, + /// Node name passed to `verify_node_labels`. + query: &'static str, + } - assert!( - sdk.verify_node_labels("node-host-001").await.unwrap(), - "node with a superset of expected labels should return true" - ); - } + // Build the per-row mock + SDK, seed the node, run the check. + let run = |row: Row| async move { + let mock = SdkMock::new(); + // A node seeded with explicit labels is inserted before the SDK is + // built; `RegisteredByLabeler` is handled after the build (it needs + // the SDK to apply the labeler); `Absent` seeds nothing. + if let Seeded::WithLabels(labels) = &row.seeded { + let node = DPUNode { + metadata: ObjectMeta { + name: Some("node-host-001".to_string()), + namespace: Some(TEST_NAMESPACE.to_string()), + labels: labels.clone(), + ..Default::default() + }, + spec: DpuNodeSpec { + dpus: Some(vec![]), + node_dms_address: None, + node_reboot_method: None, + }, + status: None, + }; + mock.nodes + .write() + .unwrap() + .insert(SdkMock::key(&node), node); + } - #[tokio::test] - async fn test_verify_node_labels_wrong_value_returns_false() { - let mock = SdkMock::new(); + let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) + .with_labeler(TestLabeler) + .build_without_resources() + .await + .unwrap(); + + if matches!(row.seeded, Seeded::RegisteredByLabeler) { + sdk.register_dpu_node(DpuNodeInfo { + node_id: "host-001".to_string(), + host_bmc_ip: "10.0.0.1".parse().unwrap(), + device_ids: vec!["dpu-001".to_string()], + }) + .await + .unwrap(); + } - let wrong_value_node = DPUNode { - metadata: ObjectMeta { - name: Some("node-host-001".to_string()), - namespace: Some(TEST_NAMESPACE.to_string()), - labels: Some(BTreeMap::from([( - "test/node".to_string(), - "false".to_string(), - )])), - ..Default::default() - }, - spec: DpuNodeSpec { - dpus: Some(vec![]), - node_dms_address: None, - node_reboot_method: None, - }, - status: None, + // DpfError isn't PartialEq, so render it to a String for the + // table's Outcome comparison; these rows all expect success anyway. + sdk.verify_node_labels(row.query) + .await + .map_err(|e| e.to_string()) }; - mock.nodes - .write() - .unwrap() - .insert(SdkMock::key(&wrong_value_node), wrong_value_node); - - let sdk = DpfSdkBuilder::new(mock, TEST_NAMESPACE, String::new()) - .with_labeler(TestLabeler) - .build_without_resources() - .await - .unwrap(); - assert!( - !sdk.verify_node_labels("node-host-001").await.unwrap(), - "node with correct key but wrong value should return false" - ); + check_cases_async( + [ + Case { + scenario: "node registered by labeler has current labels", + input: Row { + seeded: Seeded::RegisteredByLabeler, + query: "node-host-001", + }, + expect: Yields(true), + }, + Case { + scenario: "missing node verifies true (created with current labels)", + input: Row { + seeded: Seeded::Absent, + query: "node-does-not-exist", + }, + expect: Yields(true), + }, + Case { + scenario: "stale labels (none of the required keys) -> false", + input: Row { + seeded: Seeded::WithLabels(Some(BTreeMap::from([( + "old/stale-label".to_string(), + "true".to_string(), + )]))), + query: "node-host-001", + }, + expect: Yields(false), + }, + Case { + scenario: "no labels field at all -> false", + input: Row { + seeded: Seeded::WithLabels(None), + query: "node-host-001", + }, + expect: Yields(false), + }, + Case { + scenario: "superset of required labels -> true", + input: Row { + seeded: Seeded::WithLabels(Some(BTreeMap::from([ + ("test/node".to_string(), "true".to_string()), + ("extra/label".to_string(), "extra-value".to_string()), + ]))), + query: "node-host-001", + }, + expect: Yields(true), + }, + Case { + scenario: "required key present but wrong value -> false", + input: Row { + seeded: Seeded::WithLabels(Some(BTreeMap::from([( + "test/node".to_string(), + "false".to_string(), + )]))), + query: "node-host-001", + }, + expect: Yields(false), + }, + ], + run, + ) + .await; } } diff --git a/crates/dpf/src/types.rs b/crates/dpf/src/types.rs index e08ccd07c5..5bb9e6d38f 100644 --- a/crates/dpf/src/types.rs +++ b/crates/dpf/src/types.rs @@ -541,10 +541,18 @@ mod tests { /// `AsRef` for `DpuPhase` renders each variant to its canonical name; /// a provisioning phase renders its detail string verbatim. Covers all six /// `DpuPhase` variants, including an empty-detail provisioning phase. + /// + /// `Display` delegates to `AsRef`, so each row also asserts that + /// `to_string()` agrees with `as_ref()` before yielding the rendered name — + /// folding in the former `dpu_phase_display_matches_as_ref`. #[test] fn dpu_phase_as_ref_renders_each_variant() { value_scenarios!( - run = |phase| phase.as_ref().to_string(); + run = |phase: DpuPhase| { + let as_ref = phase.as_ref().to_string(); + assert_eq!(phase.to_string(), as_ref, "Display must match AsRef"); + as_ref + }; "provisioning renders its detail" { DpuPhase::Provisioning("OsInstalling".into()) => "OsInstalling".to_string(), } @@ -575,38 +583,6 @@ mod tests { ); } - /// `Display` for `DpuPhase` delegates to `AsRef`, so `to_string()` - /// yields the same canonical name for every variant. - #[test] - fn dpu_phase_display_matches_as_ref() { - value_scenarios!( - run = |phase| phase.to_string(); - "provisioning renders its detail" { - DpuPhase::Provisioning("Pending".into()) => "Pending".to_string(), - } - - "node effect" { - DpuPhase::NodeEffect => "NodeEffect".to_string(), - } - - "rebooting" { - DpuPhase::Rebooting => "Rebooting".to_string(), - } - - "ready" { - DpuPhase::Ready => "Ready".to_string(), - } - - "error" { - DpuPhase::Error => "Error".to_string(), - } - - "deleting" { - DpuPhase::Deleting => "Deleting".to_string(), - } - ); - } - /// `PartialEq` for `DpuPhase`: same variant compares equal, different /// variants differ, and `Provisioning` discriminates on its detail string. /// Folds the old `test_dpu_phase_equality`. diff --git a/crates/mqttea/tests/errors.rs b/crates/mqttea/tests/errors.rs index cf72371cf2..16f8c3ef44 100644 --- a/crates/mqttea/tests/errors.rs +++ b/crates/mqttea/tests/errors.rs @@ -44,66 +44,277 @@ fn create_test_yaml_error() -> serde_yaml::Error { serde_yaml::from_str::("{ invalid: yaml: }}}").unwrap_err() } -// Tests for error creation and conversion -#[test] -fn test_connection_error_from_client_error() { - let client_error = create_test_connection_error(); - let mqtt_error = MqtteaClientError::from(client_error); - - match mqtt_error { - MqtteaClientError::ConnectionError(_) => {} // Expected - _ => panic!("Should be ConnectionError"), +// The five category predicates an `MqtteaClientError` answers, captured as one +// value so a row can assert an error's entire categorization at once. +#[derive(Debug, PartialEq, Eq)] +struct Predicates { + connection: bool, + deserialization: bool, + serialization: bool, + topic: bool, + registry: bool, +} + +impl Predicates { + fn of(error: &MqtteaClientError) -> Self { + Self { + connection: error.is_connection_error(), + deserialization: error.is_deserialization_error(), + serialization: error.is_serialization_error(), + topic: error.is_topic_error(), + registry: error.is_registry_error(), + } } - - assert!(mqtt_error.is_connection_error()); - assert!(!mqtt_error.is_deserialization_error()); - assert!(!mqtt_error.is_serialization_error()); } +// `MqtteaClientError` must stay `Send + Sync` so async callers can hold it across +// `.await` points and move it between tasks; a `!Send` / `!Sync` field would +// silently break them, so guard the bound at compile time. #[test] -fn test_protobuf_deserialization_error() { - let decode_error = create_test_decode_error(); - let mqtt_error = MqtteaClientError::from(decode_error); - - match mqtt_error { - MqtteaClientError::ProtobufDeserializationError(_) => {} // Expected - _ => panic!("Should be ProtobufDeserializationError"), - } - - assert!(mqtt_error.is_deserialization_error()); - assert!(!mqtt_error.is_connection_error()); - assert!(!mqtt_error.is_serialization_error()); +fn error_is_send_and_sync() { + fn assert_send_sync() {} + assert_send_sync::(); } +/// Concern (a): `from`-conversion and categorization. Each row builds an error — +/// via the `From` impl where one exists, otherwise via a constructor or variant +/// literal — and asserts the full set of `is_*` category predicates it answers. +/// Because each predicate is `matches!` over a fixed variant set, asserting the +/// whole `Predicates` vector pins down both the conversion target variant and +/// its category. Folds the old `from`-conversion and `categorization` tests. #[test] -fn test_json_serialization_error() { - let json_error = create_test_json_error(); - let mqtt_error = MqtteaClientError::from(json_error); +fn error_categorization_predicates() { + use carbide_test_support::{Check, check_values}; - match mqtt_error { - MqtteaClientError::JsonSerializationError(_) => {} // Expected - _ => panic!("Should be JsonSerializationError"), + // Shorthand: only the named categories are true. + fn only(connection: bool, deserialization: bool, serialization: bool) -> Predicates { + Predicates { + connection, + deserialization, + serialization, + topic: false, + registry: false, + } } - assert!(mqtt_error.is_serialization_error()); - assert!(!mqtt_error.is_connection_error()); - assert!(!mqtt_error.is_deserialization_error()); -} - -#[test] -fn test_yaml_serialization_error() { - let yaml_error = create_test_yaml_error(); - let mqtt_error = MqtteaClientError::from(yaml_error); + check_values( + [ + // From-conversions land on the expected variant/category. + Check { + scenario: "from ClientError -> connection", + input: MqtteaClientError::from(create_test_connection_error()), + expect: only(true, false, false), + }, + Check { + scenario: "from DecodeError -> protobuf deserialization", + input: MqtteaClientError::from(create_test_decode_error()), + expect: only(false, true, false), + }, + Check { + scenario: "from serde_json::Error -> json serialization", + input: MqtteaClientError::from(create_test_json_error()), + expect: only(false, false, true), + }, + Check { + scenario: "from serde_yaml::Error -> yaml serialization", + input: MqtteaClientError::from(create_test_yaml_error()), + expect: only(false, false, true), + }, + // Deserialization variants without a From impl. + Check { + scenario: "json deserialization", + input: MqtteaClientError::JsonDeserializationError(create_test_json_error()), + expect: only(false, true, false), + }, + Check { + scenario: "yaml deserialization", + input: MqtteaClientError::YamlDeserializationError(create_test_yaml_error()), + expect: only(false, true, false), + }, + // Topic category. + Check { + scenario: "unknown message type is a topic error", + input: MqtteaClientError::unknown_message_type("/pets/lizard/unknown"), + expect: Predicates { + topic: true, + ..only(false, false, false) + }, + }, + Check { + scenario: "topic parsing is a topic error", + input: MqtteaClientError::topic_parsing_error("Bad topic format"), + expect: Predicates { + topic: true, + ..only(false, false, false) + }, + }, + // Registry category. + Check { + scenario: "unregistered type is a registry error", + input: MqtteaClientError::unregistered_type("UnknownType"), + expect: Predicates { + registry: true, + ..only(false, false, false) + }, + }, + Check { + scenario: "pattern compilation is a registry error", + input: MqtteaClientError::pattern_compilation_error("Bad regex"), + expect: Predicates { + registry: true, + ..only(false, false, false) + }, + }, + // Raw message belongs to no category. + Check { + scenario: "raw message belongs to no category", + input: MqtteaClientError::raw_message_error("Failed to process bird song data"), + expect: only(false, false, false), + }, + ], + |error| Predicates::of(&error), + ); +} + +// One row of the Display/Debug/source table (concern b). Rows carry several +// expected values (substrings, a source presence flag), so this follows the +// local-named-struct convention rather than an equality table. +struct RenderCase { + scenario: &'static str, + error: MqtteaClientError, + /// Substrings the `Display` rendering must contain. + display_contains: &'static [&'static str], + /// Whether `Error::source` should yield an underlying cause. + has_source: bool, +} + +/// Concern (b): `Display` rendering, `Debug` rendering, and error `source`. +/// Each row asserts that an error renders with the expected human-readable +/// fragments and reports a source iff it wraps an inner error. Folds the old +/// `display`, `debug_format`, `source`, owned-values, special-characters, and +/// the display halves of the deserialization-creation tests. +#[test] +fn error_display_debug_and_source() { + let cases = [ + RenderCase { + scenario: "connection", + error: MqtteaClientError::ConnectionError(create_test_connection_error()), + // Specific inner message depends on rumqttc internals. + display_contains: &["MQTT connection error"], + has_source: true, + }, + RenderCase { + scenario: "protobuf deserialization has a source", + error: MqtteaClientError::ProtobufDeserializationError(create_test_decode_error()), + display_contains: &["Protobuf deserialization error"], + has_source: true, + }, + RenderCase { + scenario: "json deserialization", + error: MqtteaClientError::JsonDeserializationError(create_test_json_error()), + display_contains: &["JSON deserialization error"], + has_source: true, + }, + RenderCase { + scenario: "yaml deserialization", + error: MqtteaClientError::YamlDeserializationError(create_test_yaml_error()), + display_contains: &["YAML deserialization error"], + has_source: true, + }, + RenderCase { + scenario: "json serialization has a source", + error: MqtteaClientError::JsonSerializationError(create_test_json_error()), + display_contains: &["JSON serialization error"], + has_source: true, + }, + RenderCase { + scenario: "yaml serialization has a source", + error: MqtteaClientError::YamlSerializationError(create_test_yaml_error()), + display_contains: &["YAML serialization error"], + has_source: true, + }, + RenderCase { + scenario: "unknown message type echoes the topic", + error: MqtteaClientError::unknown_message_type("/pets/parrot/songs"), + display_contains: &["Unknown message type", "/pets/parrot/songs"], + has_source: false, + }, + RenderCase { + scenario: "topic parsing echoes the message", + error: MqtteaClientError::topic_parsing_error("Topic must start with /"), + display_contains: &["Topic parsing error", "Topic must start with /"], + has_source: false, + }, + RenderCase { + scenario: "raw message echoes the message", + error: MqtteaClientError::raw_message_error("Failed to decode turtle sensor data"), + display_contains: &["Raw message error", "turtle sensor data"], + has_source: false, + }, + RenderCase { + scenario: "unregistered type echoes the type name", + error: MqtteaClientError::unregistered_type("FishMessage"), + display_contains: &["Type not registered", "FishMessage"], + has_source: false, + }, + RenderCase { + scenario: "invalid utf8 echoes the message", + error: MqtteaClientError::invalid_utf8("Contains invalid UTF-8 bytes"), + display_contains: &["Invalid UTF-8", "invalid UTF-8 bytes"], + has_source: false, + }, + RenderCase { + scenario: "pattern compilation echoes the message", + error: MqtteaClientError::pattern_compilation_error("Missing closing bracket in regex"), + display_contains: &["Pattern compilation error", "closing bracket"], + has_source: false, + }, + RenderCase { + scenario: "display preserves special characters", + error: MqtteaClientError::unknown_message_type("/pets/🐱/data/emoji-test"), + display_contains: &["🐱", "emoji-test"], + has_source: false, + }, + ]; + + for case in cases { + let RenderCase { + scenario, + error, + display_contains, + has_source, + } = case; + + let display = format!("{error}"); + for fragment in display_contains { + assert!( + display.contains(fragment), + "{scenario}: Display {display:?} should contain {fragment:?}" + ); + } - match mqtt_error { - MqtteaClientError::YamlSerializationError(_) => {} // Expected - _ => panic!("Should be YamlSerializationError"), + assert_eq!( + std::error::Error::source(&error).is_some(), + has_source, + "{scenario}: source presence" + ); } - assert!(mqtt_error.is_serialization_error()); + // Debug rendering names the variant and echoes its payload. + let debug_error = MqtteaClientError::unknown_message_type("/debug/test"); + let debug = format!("{debug_error:?}"); + assert!( + debug.contains("UnknownMessageType"), + "debug names the variant" + ); + assert!(debug.contains("/debug/test"), "debug echoes the payload"); } -// Tests for convenience error constructors +// Tests for convenience error constructors. +// +// Each constructor populates a distinct variant with the caller's string; this +// asserts the variant/field round-trip (the categorization those variants +// answer lives in `error_categorization_predicates`). #[test] fn test_unknown_message_type_constructor() { let error = MqtteaClientError::unknown_message_type("/pets/fluffy/unknown-data"); @@ -114,8 +325,6 @@ fn test_unknown_message_type_constructor() { } _ => panic!("Should be UnknownMessageType"), } - - assert!(error.is_topic_error()); } #[test] @@ -128,8 +337,6 @@ fn test_topic_parsing_error_constructor() { } _ => panic!("Should be TopicParsingError"), } - - assert!(error.is_topic_error()); } #[test] @@ -154,8 +361,6 @@ fn test_unregistered_type_constructor() { } _ => panic!("Should be UnregisteredType"), } - - assert!(error.is_registry_error()); } #[test] @@ -180,146 +385,6 @@ fn test_pattern_compilation_error_constructor() { } _ => panic!("Should be PatternCompilationError"), } - - assert!(error.is_registry_error()); -} - -// Tests for error categorization methods -#[test] -fn test_error_categorization_connection() { - let connection_error = MqtteaClientError::ConnectionError(create_test_connection_error()); - - assert!(connection_error.is_connection_error()); - assert!(!connection_error.is_deserialization_error()); - assert!(!connection_error.is_serialization_error()); - assert!(!connection_error.is_topic_error()); - assert!(!connection_error.is_registry_error()); -} - -#[test] -fn test_error_categorization_deserialization() { - let protobuf_error = - MqtteaClientError::ProtobufDeserializationError(create_test_decode_error()); - let json_error = MqtteaClientError::JsonDeserializationError(create_test_json_error()); - let yaml_error = MqtteaClientError::YamlDeserializationError(create_test_yaml_error()); - - assert!(protobuf_error.is_deserialization_error()); - assert!(json_error.is_deserialization_error()); - assert!(yaml_error.is_deserialization_error()); - - assert!(!protobuf_error.is_connection_error()); - assert!(!json_error.is_serialization_error()); - assert!(!yaml_error.is_topic_error()); -} - -#[test] -fn test_error_categorization_serialization() { - let json_error = MqtteaClientError::JsonSerializationError(create_test_json_error()); - let yaml_error = MqtteaClientError::YamlSerializationError(create_test_yaml_error()); - - assert!(json_error.is_serialization_error()); - assert!(yaml_error.is_serialization_error()); - - assert!(!json_error.is_connection_error()); - assert!(!yaml_error.is_topic_error()); -} - -#[test] -fn test_error_categorization_topic() { - let unknown_type = MqtteaClientError::unknown_message_type("/pets/lizard/unknown"); - let parsing_error = MqtteaClientError::topic_parsing_error("Bad topic format"); - - assert!(unknown_type.is_topic_error()); - assert!(parsing_error.is_topic_error()); - - assert!(!unknown_type.is_connection_error()); - assert!(!parsing_error.is_serialization_error()); -} - -#[test] -fn test_error_categorization_registry() { - let unregistered = MqtteaClientError::unregistered_type("UnknownType"); - let pattern_error = MqtteaClientError::pattern_compilation_error("Bad regex"); - - assert!(unregistered.is_registry_error()); - assert!(pattern_error.is_registry_error()); - - assert!(!unregistered.is_topic_error()); - assert!(!pattern_error.is_connection_error()); -} - -// Tests for error display and formatting -#[test] -fn test_error_display_connection() { - let error = MqtteaClientError::ConnectionError(create_test_connection_error()); - let display = format!("{error}"); - - assert!(display.contains("MQTT connection error")); - // Note: Specific error message depends on rumqttc internals -} - -#[test] -fn test_error_display_unknown_message_type() { - let error = MqtteaClientError::unknown_message_type("/pets/parrot/songs"); - let display = format!("{error}"); - - assert!(display.contains("Unknown message type")); - assert!(display.contains("/pets/parrot/songs")); -} - -#[test] -fn test_error_display_topic_parsing() { - let error = MqtteaClientError::topic_parsing_error("Topic must start with /"); - let display = format!("{error}"); - - assert!(display.contains("Topic parsing error")); - assert!(display.contains("Topic must start with /")); -} - -#[test] -fn test_error_display_raw_message() { - let error = MqtteaClientError::raw_message_error("Failed to decode turtle sensor data"); - let display = format!("{error}"); - - assert!(display.contains("Raw message error")); - assert!(display.contains("turtle sensor data")); -} - -#[test] -fn test_error_display_unregistered_type() { - let error = MqtteaClientError::unregistered_type("FishMessage"); - let display = format!("{error}"); - - assert!(display.contains("Type not registered")); - assert!(display.contains("FishMessage")); -} - -#[test] -fn test_error_display_invalid_utf8() { - let error = MqtteaClientError::invalid_utf8("Contains invalid UTF-8 bytes"); - let display = format!("{error}"); - - assert!(display.contains("Invalid UTF-8")); - assert!(display.contains("invalid UTF-8 bytes")); -} - -#[test] -fn test_error_display_pattern_compilation() { - let error = MqtteaClientError::pattern_compilation_error("Missing closing bracket in regex"); - let display = format!("{error}"); - - assert!(display.contains("Pattern compilation error")); - assert!(display.contains("closing bracket")); -} - -// Tests for error debug formatting -#[test] -fn test_error_debug_format() { - let error = MqtteaClientError::unknown_message_type("/debug/test"); - let debug = format!("{error:?}"); - - assert!(debug.contains("UnknownMessageType")); - assert!(debug.contains("/debug/test")); } // Tests for unregistered_type_error helper function @@ -350,25 +415,6 @@ fn test_unregistered_type_error_custom_type() { } } -// Tests for error chaining and source -#[test] -fn test_error_source_connection() { - let client_error = create_test_connection_error(); - let mqtt_error = MqtteaClientError::from(client_error); - - // Should have a source error - assert!(std::error::Error::source(&mqtt_error).is_some()); -} - -#[test] -fn test_error_source_protobuf() { - let decode_error = create_test_decode_error(); - let mqtt_error = MqtteaClientError::from(decode_error); - - // Should have a source error - assert!(std::error::Error::source(&mqtt_error).is_some()); -} - // Tests for error equality and comparison (for test assertions) #[test] fn test_error_equality() { @@ -404,78 +450,8 @@ fn test_error_default() { } } -// Test error with owned values to avoid borrow issues -#[test] -fn test_error_display_with_owned_values() { - let error = MqtteaClientError::unknown_message_type("/pets/fluffy/unknown-data"); - - // Test that we can format the error without borrowing issues - let display = format!("{error}"); - assert!(display.contains("Unknown message type")); - assert!(display.contains("/pets/fluffy/unknown-data")); - - // Test that we can still use the error after formatting - match error { - MqtteaClientError::UnknownMessageType(ref topic) => { - assert_eq!(topic, "/pets/fluffy/unknown-data"); - } - _ => panic!("Should be UnknownMessageType"), - } -} - -// Tests for JSON deserialization error creation -#[test] -fn test_json_deserialization_error_creation() { - let json_result: Result = serde_json::from_str("invalid json"); - assert!(json_result.is_err()); - - let json_error = json_result.unwrap_err(); - - // Convert to our MQTT error - let mqtt_error = MqtteaClientError::JsonDeserializationError(json_error); - - // Verify properties - assert!(mqtt_error.is_deserialization_error()); - - let display = format!("{mqtt_error}"); - assert!(display.contains("JSON deserialization error")); -} - -// Tests for YAML deserialization error creation -#[test] -fn test_yaml_deserialization_error_creation() { - let yaml_result: Result = serde_yaml::from_str("{ invalid: yaml: }}}"); - assert!(yaml_result.is_err()); - - let yaml_error = yaml_result.unwrap_err(); - - // Convert to our MQTT error - let mqtt_error = MqtteaClientError::YamlDeserializationError(yaml_error); - - // Verify properties - assert!(mqtt_error.is_deserialization_error()); - - let display = format!("{mqtt_error}"); - assert!(display.contains("YAML deserialization error")); -} - -// Performance test - error creation should be fast -#[test] -fn test_error_creation_performance() { - let start = std::time::Instant::now(); - - // Create many errors quickly - for i in 0..10_000 { - let _error = MqtteaClientError::unknown_message_type(format!("/pets/animal-{i}/data")); - } - - let elapsed = start.elapsed(); - - // Error creation should be very fast - assert!(elapsed.as_millis() < 100, "Error creation should be fast"); -} - -// Test error with very long messages (edge case) +// Test error with very long messages (edge case): the payload round-trips +// untruncated and Display renders the whole thing. #[test] fn test_error_with_long_message() { let long_topic = "/pets/".to_string() + &"a".repeat(10_000) + "/data"; @@ -494,72 +470,3 @@ fn test_error_with_long_message() { let display = format!("{error}"); assert!(display.len() > 1000); } - -// Test error with special characters -#[test] -fn test_error_with_special_characters() { - let special_topic = "/pets/🐱/data/emoji-test"; - let error = MqtteaClientError::unknown_message_type(special_topic); - - let display = format!("{error}"); - assert!(display.contains("🐱")); - assert!(display.contains("emoji-test")); -} - -// Test error Send + Sync traits (important for async code) -#[test] -fn test_error_send_sync() { - fn assert_send() {} - fn assert_sync() {} - - assert_send::(); - assert_sync::(); -} - -// Test that errors can be used in Results and Options -#[test] -fn test_error_in_result() { - fn might_fail(should_fail: bool) -> Result { - if should_fail { - Err(MqtteaClientError::unknown_message_type( - "/pets/ferret/unknown", - )) - } else { - Ok("success".to_string()) - } - } - - let success = might_fail(false); - assert!(success.is_ok()); - assert_eq!(success.unwrap(), "success"); - - let failure = might_fail(true); - assert!(failure.is_err()); - assert!(failure.unwrap_err().is_topic_error()); -} - -#[test] -fn test_error_in_option() { - fn maybe_error(include_error: bool) -> Option { - if include_error { - Some(MqtteaClientError::raw_message_error( - "Chinchilla sensor malfunction", - )) - } else { - None - } - } - - assert!(maybe_error(false).is_none()); - - let error_opt = maybe_error(true); - assert!(error_opt.is_some()); - - let error = error_opt.unwrap(); - match error { - MqtteaClientError::RawMessageError(msg) => { - assert!(msg.contains("Chinchilla")); - } - _ => panic!("Should be RawMessageError"), - } -} diff --git a/crates/mqttea/tests/stats.rs b/crates/mqttea/tests/stats.rs index 227526dc73..df76f62767 100644 --- a/crates/mqttea/tests/stats.rs +++ b/crates/mqttea/tests/stats.rs @@ -513,32 +513,3 @@ fn test_stats_clone() { assert_eq!(stats1.total_processed, stats2.total_processed); assert_eq!(stats1.total_bytes_processed, stats2.total_bytes_processed); } - -// Performance-oriented tests -#[test] -fn test_stats_performance_no_contention() { - let tracker = QueueStatsTracker::new(); - let start = std::time::Instant::now(); - - // Perform many operations quickly - for i in 0..10_000 { - tracker.increment_pending(i % 1000); - if i % 2 == 0 { - tracker.decrement_pending_increment_processed(i % 1000); - } else { - tracker.decrement_pending_increment_failed(i % 1000); - } - } - - let elapsed = start.elapsed(); - - // Should complete very quickly (atomic operations are fast) - assert!( - elapsed.as_millis() < 100, - "Stats operations should be very fast" - ); - - // Verify final state is consistent - let stats = tracker.to_stats(); - assert_eq!(stats.total_processed + stats.total_failed, 10_000); -}