bacnet-types 0.14.0

BACnet protocol types, enums, and primitives per ASHRAE 135-2020
Documentation
use super::*;

#[test]
fn protocol_error_display() {
    let err = Error::Protocol {
        class: ErrorClass::PROPERTY,
        code: ErrorCode::UNKNOWN_OBJECT,
    };
    assert_eq!(err.to_string(), "BACnet error: property / unknown-object");

    // Unknown class/code falls back to numeric
    let err2 = Error::Protocol {
        class: ErrorClass::from_raw(999),
        code: ErrorCode::from_raw(999),
    };
    assert_eq!(err2.to_string(), "BACnet error: class=999 / code=999");
    assert_eq!(
        Error::Protocol {
            class: ErrorClass::PROPERTY,
            code: ErrorCode::from_raw(999)
        }
        .to_string(),
        "BACnet error: property / code=999"
    );
}

#[cfg(feature = "alloc")]
#[test]
fn structured_error_display_names_the_detail() {
    use crate::enums::ObjectType;
    use crate::primitives::ObjectIdentifier;

    let reference = |index| BACnetObjectPropertyReference {
        object_identifier: ObjectIdentifier::new(ObjectType::ANALOG_VALUE, 7).unwrap(),
        property_identifier: PropertyIdentifier::PRESENT_VALUE.to_raw(),
        property_array_index: index,
    };
    let private_transfer = |error_parameters| ErrorDetail::PrivateTransfer {
        vendor_id: 555,
        service_number: 7,
        error_parameters,
    };
    for (class, code, detail, expected) in [
        (
            5,
            81,
            ErrorDetail::FirstFailedElementNumber(2),
            "BACnet error: services / list-element-not-found, first failed element 2",
        ),
        (
            2,
            40,
            ErrorDetail::FirstFailedWriteAttempt(reference(Some(3))),
            "BACnet error: property / write-access-denied, \
             first failed write attempt ANALOG_VALUE,7 PRESENT_VALUE[3]",
        ),
        (
            2,
            44,
            ErrorDetail::FirstFailedSubscription(reference(None)),
            "BACnet error: property / not-cov-property, \
             first failed subscription ANALOG_VALUE,7 PRESENT_VALUE",
        ),
        (
            5,
            0,
            private_transfer(Some(vec![0x21, 1])),
            "BACnet error: services / other, vendor 555 service 7, \
             2 octets of error parameters",
        ),
        (
            5,
            0,
            private_transfer(None),
            "BACnet error: services / other, vendor 555 service 7",
        ),
        (
            5,
            39,
            ErrorDetail::VtSessionIdentifiers(vec![1, 4]),
            "BACnet error: services / vt-session-termination-failure, \
             VT sessions not closed: 1 4",
        ),
    ] {
        assert_eq!(
            Error::protocol(
                ErrorClass::from_raw(class),
                ErrorCode::from_raw(code),
                Some(detail)
            )
            .to_string(),
            expected
        );
    }
    assert!(matches!(
        Error::protocol(ErrorClass::PROPERTY, ErrorCode::UNKNOWN_OBJECT, None),
        Error::Protocol {
            class: ErrorClass::PROPERTY,
            code: ErrorCode::UNKNOWN_OBJECT
        }
    ));
}

#[test]
fn decoding_error_display() {
    let err = Error::decoding(42, "unexpected tag");
    assert_eq!(
        err.to_string(),
        "decoding error at offset 42: unexpected tag"
    );
    assert!(matches!(
        err,
        Error::Decoding(DecodeError {
            offset: 42,
            kind: DecodingKind::InvalidEncoding,
            context: "",
            label: "unexpected tag",
            ..
        })
    ));
}

/// A label with numbers keeps its offset and kind (#1776).
#[test]
fn labelled_values_keep_their_fault() {
    let err = Error::from(
        DecodeError::new(DecodingKind::Trailing, 3, "{} trailing byte(s)").with_value(2),
    );
    assert_eq!(
        err.to_string(),
        "decoding error at offset 3: 2 trailing byte(s)"
    );
    assert_eq!(err.decode_fault(), Some((3, DecodingKind::Trailing)));
    assert_eq!(
        Error::trailing(3, "x").decode_fault(),
        Some((3, DecodingKind::Trailing))
    );
    assert_eq!(Error::buffer_too_short(1, 0).decode_fault(), None);
}

#[test]
fn syntax_faults_name_their_reject_reasons() {
    for (error, reason) in [
        (
            Error::decoding(1, "bad"),
            RejectReason::INVALID_DATA_ENCODING,
        ),
        (
            Error::out_of_range(1, "wide"),
            RejectReason::PARAMETER_OUT_OF_RANGE,
        ),
        (Error::overflow(1, "many"), RejectReason::BUFFER_OVERFLOW),
        (
            Error::decoding_kind(DecodingKind::Unsupported, 1, "DBCS"),
            RejectReason::OTHER,
        ),
        (Error::invalid_tag(1, "tag"), RejectReason::INVALID_TAG),
        (
            Error::missing(1, "gone"),
            RejectReason::MISSING_REQUIRED_PARAMETER,
        ),
        (Error::trailing(1, "more"), RejectReason::TOO_MANY_ARGUMENTS),
        (
            Error::buffer_too_short(9, 4),
            RejectReason::MISSING_REQUIRED_PARAMETER,
        ),
        (
            Error::Reject {
                reason: RejectReason::UNDEFINED_ENUMERATION,
            },
            RejectReason::UNDEFINED_ENUMERATION,
        ),
    ] {
        assert_eq!(error.reject_reason(), Some(reason), "{error}");
    }
    for error in [
        Error::Protocol {
            class: ErrorClass::SERVICES,
            code: ErrorCode::OTHER,
        },
        Error::Range(RangeError::new("x")),
        Error::Abort {
            reason: AbortReason::BUFFER_OVERFLOW,
        },
    ] {
        assert_eq!(error.reject_reason(), None, "{error}");
    }
    // A request's decode error becomes its Reject; any other error stays.
    assert!(matches!(
        Error::trailing(3, "more").into_request_reject(),
        Error::Reject { reason } if reason == RejectReason::TOO_MANY_ARGUMENTS
    ));
    assert!(matches!(
        Error::Range(RangeError::new("x")).into_request_reject(),
        Error::Range(_)
    ));
    // The kind doesn't show in the message.
    assert_eq!(
        Error::trailing(3, "two trailing byte(s)").to_string(),
        "decoding error at offset 3: two trailing byte(s)"
    );
}

/// The alloc-only variants keep their text and take no Reject reason.
#[cfg(feature = "alloc")]
#[test]
fn owned_variants_take_no_reject_reason() {
    for error in [Error::Encoding("x".into()), Error::OutOfRange("x".into())] {
        assert_eq!(error.reject_reason(), None, "{error}");
    }
    assert_eq!(
        Error::trailing(3, "more")
            .into_request_reject()
            .reject_reason(),
        Some(RejectReason::TOO_MANY_ARGUMENTS)
    );
    assert!(matches!(
        Error::OutOfRange("x".into()).into_request_reject(),
        Error::OutOfRange(_)
    ));
}

/// The static encoder and segmenter refusals take no Reject reason either
/// (#1802), with or without `alloc`.
#[test]
fn static_refusals_take_no_reject_reason() {
    for error in [
        Error::encode("x"),
        Error::Segmentation(SegmentationError::ZeroSegmentSize),
    ] {
        assert_eq!(error.reject_reason(), None, "{error:?}");
        assert!(!matches!(error.into_request_reject(), Error::Reject { .. }));
    }
}

#[test]
fn range_error_display() {
    let err = Error::from(RangeError::new("instance number {} exceeds max 4194303").with_value(5));
    assert_eq!(
        err.to_string(),
        "value out of range: instance number 5 exceeds max 4194303"
    );
}

#[test]
fn buffer_too_short_display() {
    let err = Error::buffer_too_short(10, 3);
    assert!(err.to_string().contains("need 10"));
    assert!(err.to_string().contains("have 3"));
}

#[test]
fn timeout_error_display() {
    let err = Error::Timeout(Duration::from_secs(3));
    assert!(err.to_string().contains("3s"));
}

#[test]
fn log_not_advancing_display() {
    assert_eq!(
        Error::LogNotAdvancing {
            requested: 7,
            returned: Some(3)
        }
        .to_string(),
        "log read is not advancing: asked for sequence number 7, the device answered from 3"
    );
    assert_eq!(
        Error::LogNotAdvancing {
            requested: 7,
            returned: None
        }
        .to_string(),
        "log read is not advancing: asked for sequence number 7, which the log's counts \
         say it holds, the device answered with no records"
    );
}

#[test]
fn send_too_long_keeps_the_observed_lengths_without_becoming_an_apdu_reject() {
    let error = Error::SendTooLong {
        length: 1500,
        limit: 480,
    };
    assert_eq!(
        error.to_string(),
        "send refused before transmission: 1500 octets exceed the 480-octet limit"
    );
    assert_eq!(error.reject_reason(), None);
    assert!(matches!(
        error.into_request_reject(),
        Error::SendTooLong {
            length: 1500,
            limit: 480
        }
    ));
}

#[test]
fn routed_path_too_long_display_preserves_dnet() {
    let err = Error::RoutedPathTooLong { dnet: 1234 };
    assert!(err.to_string().contains("1234"));
}

#[test]
fn routed_path_capacity_display_preserves_bound() {
    let err = Error::RoutedPathCapacityExceeded { capacity: 256 };
    assert!(err.to_string().contains("256"));
}

#[test]
fn unsupported_transport_display_names_both_data_links() {
    let err = Error::UnsupportedTransport {
        required: DataLink::Bip,
        actual: DataLink::Mstp,
    };
    assert_eq!(
        err.to_string(),
        "operation requires BACnet/IP; this transport is MS/TP"
    );
}