units: Introduce coverage for Display on error types
What changed, and why it matters
This commit only adds new tests that check error messages are non-empty. It does not change any production code, so it cannot introduce a security vulnerability or fix one. It is a routine improvement to test coverage.
No security action needed. Review as a normal test-coverage change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds unit tests across the units crate verifying that Display implementations on error types produce non-empty strings and, where applicable, that Error::source() returns the expected value. No library logic, parsing rules, bounds checks, or public APIs are modified. The diff is purely additive test code in #[cfg(test)] modules.
Changed components
Inspect captured patch +468 / −10
diff --git a/units/src/amount/error.rs b/units/src/amount/error.rs
index a6e0e385..65e44dec 100644
--- a/units/src/amount/error.rs
+++ b/units/src/amount/error.rs
@@ -483,3 +483,173 @@ impl std::error::Error for AmountDecoderError {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+ #[cfg(feature = "alloc")]
+ use core::str::FromStr;
+ #[cfg(feature = "std")]
+ use std::error::Error;
+
+ #[cfg(feature = "encoding")]
+ use encoding::{Decodable as _, Decoder as _};
+
+ #[cfg(feature = "alloc")]
+ use crate::{
+ amount::{Amount, Denomination, ParseDenominationError, ParseError}
+ };
+ #[cfg(feature = "alloc")]
+ use super::{ParseAmountError, ParseAmountErrorInner, ParseErrorInner};
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // A helper macro to break out a ParseAmountErrorInner type and assert display down the chain.
+ macro_rules! assert_amount_err {
+ ($e:expr, $enum_arm:ident, $err_msg:expr) => {
+ assert!(!$e.to_string().is_empty());
+ let ParseError(ParseErrorInner::Amount(err)) = $e
+ else { panic!($err_msg) };
+ assert!(!err.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(err.source().is_some());
+
+ let ParseAmountError(ParseAmountErrorInner::$enum_arm(err)) = err
+ else { panic!($err_msg) };
+ assert!(!err.to_string().is_empty());
+ // The inner-most types have no source
+ #[cfg(feature = "std")]
+ assert!(err.source().is_none());
+ };
+ }
+
+ // InputTooLargeError
+ // one char too long
+ let long_input = alloc::format!("{} BTC", "1".repeat(51));
+ let e = Amount::from_str(&long_input).unwrap_err();
+ assert_amount_err!(e, InputTooLarge, "error should be InputTooLargeError");
+ // n chars too long
+ let long_input = alloc::format!("{} BTC", "1".repeat(52));
+ let e = Amount::from_str(&long_input).unwrap_err();
+ assert_amount_err!(e, InputTooLarge, "error should be InputTooLargeError");
+
+ // InvalidCharacterError
+ // invalid character in amount string
+ let e = Amount::from_str("12x34 BTC").unwrap_err();
+ assert_amount_err!(e, InvalidCharacter, "error should be InvalidCharacterError");
+ // too many decimal points
+ let e = Amount::from_str("12.3.4 BTC").unwrap_err();
+ assert_amount_err!(e, InvalidCharacter, "error should be InvalidCharacterError");
+ // too many minus signs
+ let e = Amount::from_str("--1234 BTC").unwrap_err();
+ assert_amount_err!(e, InvalidCharacter, "error should be InvalidCharacterError");
+
+ // MissingDigitsError
+ // no numeric value
+ let e = Amount::from_str("BTC").unwrap_err();
+ assert_amount_err!(e, MissingDigits, "error should be MissingDigitsError");
+ // Only a minus sign
+ let e = Amount::from_str("- BTC").unwrap_err();
+ assert_amount_err!(e, MissingDigits, "error should be MissingDigitsError");
+
+ // OutOfRangeError
+ // amount too large
+ let e = Amount::from_str("21000001 BTC").unwrap_err();
+ assert_amount_err!(e, OutOfRange, "error should be OutOfRangeError");
+ // less than 0
+ let e = Amount::from_str("-10 BTC").unwrap_err();
+ assert_amount_err!(e, OutOfRange, "error should be OutOfRangeError");
+
+ // TooPreciseError - sub-satoshi precision
+ let e = Amount::from_str("0.000000001 BTC").unwrap_err();
+ assert_amount_err!(e, TooPrecise, "error should be TooPreciseError");
+
+ // BadPositionError
+ // underscore in bad position
+ let e = Amount::from_str("_123 BTC").unwrap_err();
+ assert_amount_err!(e, BadPosition, "error should be BadPositionError");
+ // underscore in bad position (negative)
+ let e = Amount::from_str("-_123 BTC").unwrap_err();
+ assert_amount_err!(e, BadPosition, "error should be BadPositionError");
+ // consecutive underscores
+ let e = Amount::from_str("1__23 BTC").unwrap_err();
+ assert_amount_err!(e, BadPosition, "error should be BadPositionError");
+
+ // ParseAmountError - parent type for the errors above
+ let e = Amount::from_str_in("invalid", Denomination::Bitcoin).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // UnknownDenominationError - amount with unknown denomination string
+ let e = Denomination::from_str("XYZ").unwrap_err();
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ let ParseDenominationError::Unknown(e) = e
+ else { panic!("error should be UnknownDenominationError") };
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // PossiblyConfusingDenominationError - confusing denomination like "MBTC"
+ let e = Denomination::from_str("MBTC").unwrap_err();
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ let ParseDenominationError::PossiblyConfusing(e) = e
+ else { panic!("error should be PossiblyConfusingDenominationError") };
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // ParseDenominationError - parent error for the above *DenominationError types
+ // Unknown type
+ let e = Denomination::from_str("UNKNOWN").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ // Possibly confusing type
+ let e = Denomination::from_str("MBTC").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // ParseError - parent type for all of the above
+ // Amount type
+ let e = "invalid BTC".parse::<Amount>().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ // bad denomination type
+ let e = "123 GBTC".parse::<Amount>().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ // missing denomination type
+ let e = "123".parse::<Amount>().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ #[cfg(feature = "encoding")]
+ {
+ // AmountDecoderError
+ // EOF type
+ let mut decoder = Amount::decoder();
+ let _ = decoder.push_bytes(&mut [0u8; 3].as_slice());
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // Out of range type
+ let mut decoder = Amount::decoder();
+ let _ = decoder.push_bytes(&mut (21_000_001 * 100_000_000_u64).to_le_bytes().as_slice());
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
+ }
+}
diff --git a/units/src/block.rs b/units/src/block.rs
index 623f8004..88f46521 100644
--- a/units/src/block.rs
+++ b/units/src/block.rs
@@ -623,8 +623,13 @@ impl<'a> core::iter::Sum<&'a Self> for BlockMtpInterval {
#[cfg(test)]
mod tests {
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+ #[cfg(feature = "std")]
+ use std::error::Error;
+
#[cfg(feature = "encoding")]
- use encoding::{Decoder as _, UnexpectedEofError};
+ use encoding::{Decodable as _, Decoder as _, UnexpectedEofError};
use super::*;
use crate::relative::{NumberOf512Seconds, TimeOverflowError};
@@ -871,4 +876,26 @@ mod tests {
// Subtracting zero
assert_eq!(BlockHeight(500).saturating_sub(BlockHeightInterval::ZERO), BlockHeight(500),);
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // TooBigForRelativeHeightError - block interval too big for relative height
+ let big_interval = BlockHeightInterval::from_u32(u32::MAX);
+ let e = relative::NumberOfBlocks::try_from(big_interval).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ #[cfg(feature = "encoding")]
+ {
+ // BlockHeightDecoderError
+ let mut decoder = BlockHeight::decoder();
+ let _ = decoder.push_bytes(&mut [0u8; 3].as_slice());
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
+ }
}
diff --git a/units/src/locktime/absolute/error.rs b/units/src/locktime/absolute/error.rs
index c16480da..fa266e49 100644
--- a/units/src/locktime/absolute/error.rs
+++ b/units/src/locktime/absolute/error.rs
@@ -286,17 +286,80 @@ impl fmt::Display for LockTimeUnit {
#[cfg(test)]
mod tests {
- #[test]
#[cfg(feature = "alloc")]
- fn locktime_unit_display() {
- use alloc::format;
+ use alloc::{format, string::ToString};
+ #[cfg(feature = "alloc")]
+ use core::str::FromStr;
+ #[cfg(feature = "std")]
+ use std::error::Error;
+
+ #[cfg(all(feature = "encoding", feature = "alloc"))]
+ use encoding::{Decodable as _, Decoder as _};
+
+ #[cfg(feature = "alloc")]
+ use super::LockTimeUnit;
+ #[cfg(feature = "alloc")]
+ use crate::{
+ BlockHeight,
+ locktime::absolute::{Height, LockTime, MedianTimePast}
+ };
- use super::LockTimeUnit;
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn locktime_unit_display() {
let blocks = LockTimeUnit::Blocks;
let seconds = LockTimeUnit::Seconds;
assert_eq!(format!("{}", blocks), "expected lock-by-height (must be < 500000000)");
assert_eq!(format!("{}", seconds), "expected lock-by-time (must be >= 500000000)");
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // ConversionError - converting BlockHeight to absolute::Height
+ let too_big = BlockHeight::from_u32(u32::MAX);
+ let e = Height::try_from(too_big).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // IncompatibleHeightError - satisfy time lock with height
+ let time_lock = LockTime::from_mtp(MedianTimePast::MIN.to_u32()).unwrap();
+ let e = time_lock.is_satisfied_by_height(Height::MIN).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // IncompatibleTimeError - satisfy height lock with time
+ let height_lock = LockTime::from_height(Height::MIN.to_u32()).unwrap();
+ let e = height_lock.is_satisfied_by_time(MedianTimePast::MIN).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // ParseHeightError - parse invalid height
+ let e = Height::from_str("invalid").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // ParseTimeError - parse invalid time
+ let e = MedianTimePast::from_str("invalid").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ #[cfg(feature = "encoding")]
+ {
+ // LockTimeDecoderError
+ let mut decoder = LockTime::decoder();
+ let _ = decoder.push_bytes(&mut [0u8; 3].as_slice());
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
+ }
}
diff --git a/units/src/locktime/relative/error.rs b/units/src/locktime/relative/error.rs
index 2113fb9c..05948965 100644
--- a/units/src/locktime/relative/error.rs
+++ b/units/src/locktime/relative/error.rs
@@ -179,3 +179,116 @@ impl fmt::Display for InvalidTimeError {
#[cfg(feature = "std")]
impl std::error::Error for InvalidTimeError {}
+
+#[cfg(test)]
+mod tests {
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+ #[cfg(feature = "std")]
+ use std::error::Error;
+
+ #[cfg(feature = "alloc")]
+ use crate::{
+ BlockHeight, BlockMtp, BlockMtpInterval, Sequence,
+ locktime::relative::{LockTime, NumberOf512Seconds, NumberOfBlocks}
+ };
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // DisabledLockTimeError - parse disabled lock time
+ let disabled = Sequence::MAX; // Sequence with disable flag set
+ let e = LockTime::from_sequence(disabled).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // TimeOverflowError - time too large for relative locktime
+ let too_big = BlockMtpInterval::MAX;
+ let e = too_big.to_relative_mtp_interval_floor().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // InvalidHeightError - is_satisfied_by with invalid args
+ let blocks = NumberOfBlocks::from(10u16);
+ let e = blocks.is_satisfied_by(BlockHeight::from_u32(5), BlockHeight::from_u32(10)).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // InvalidTimeError - is_satisfied_by with invalid args
+ let time = NumberOf512Seconds::from_512_second_intervals(10);
+ let e = time.is_satisfied_by(BlockMtp::from_u32(5), BlockMtp::from_u32(10)).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+
+ // IsSatisfiedBy*Error
+ let time_lock = LockTime::from_512_second_intervals(10);
+ let height_lock = LockTime::from_height(10);
+
+ // IsSatisfiedByError - wraps InvalidHeightError or InvalidTimeError
+ // Error when chain_tip < utxo_mined_at (args wrong way around)
+ // blocks type
+ let e = height_lock
+ .is_satisfied_by(
+ BlockHeight::from_u32(5),
+ BlockMtp::ZERO,
+ BlockHeight::from_u32(10),
+ BlockMtp::ZERO,
+ )
+ .unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ // time type
+ let e = time_lock
+ .is_satisfied_by(
+ BlockHeight::ZERO,
+ BlockMtp::from_u32(5),
+ BlockHeight::ZERO,
+ BlockMtp::from_u32(10),
+ )
+ .unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // IsSatisfiedByHeightError
+ // Incompatible type
+ let e = time_lock.is_satisfied_by_height(
+ BlockHeight::from_u32(5),
+ BlockHeight::from_u32(10)
+ ).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+ // Satisfaction type
+ let e = height_lock.is_satisfied_by_height(
+ BlockHeight::from_u32(5),
+ BlockHeight::from_u32(10)
+ ).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // IsSatisfiedByTimeError
+ // Incompatible type
+ let e = height_lock.is_satisfied_by_time(
+ BlockMtp::from_u32(5),
+ BlockMtp::from_u32(10)
+ ).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+ // Satisfaction type
+ let e = time_lock.is_satisfied_by_time(
+ BlockMtp::from_u32(5),
+ BlockMtp::from_u32(10)
+ ).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
+}
diff --git a/units/src/parse_int.rs b/units/src/parse_int.rs
index 2bfd6a33..06a751d7 100644
--- a/units/src/parse_int.rs
+++ b/units/src/parse_int.rs
@@ -526,8 +526,10 @@ impl std::error::Error for ContainsPrefixError {}
#[cfg(test)]
mod tests {
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
#[cfg(feature = "std")]
- use std::panic;
+ use std::{error::Error, panic};
use super::*;
@@ -691,4 +693,48 @@ mod tests {
assert!(hex_u128_unchecked("deadbeefabcdffffdeadbeefabcdffff").is_ok());
assert!(hex_u128_unchecked("deadbeefabcdffffdeadbeefabcdffff1").is_err());
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // ParseIntError - parse invalid integer
+ let e = int_from_str::<u32>("not_a_number").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // PrefixedHexError
+ // missing prefix type
+ let e = hex_u32_prefixed("abc").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ let PrefixedHexError(PrefixedHexErrorInner::MissingPrefix(e)) = e
+ else { panic!("should be a MissingPrefixError") };
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+ // bad number type
+ let e = hex_u32_prefixed("0xgabc").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+
+ // UnprefixedHexError
+ // has prefix type
+ let e = hex_u32_unprefixed("0xabc").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ let UnprefixedHexError(UnprefixedHexErrorInner::ContainsPrefix(e)) = e
+ else { panic!("should be a ContainsPrefixError") };
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+ // bad number type
+ let e = hex_u32_unprefixed("gabc").unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
}
diff --git a/units/src/result.rs b/units/src/result.rs
index d8019760..358aba19 100644
--- a/units/src/result.rs
+++ b/units/src/result.rs
@@ -439,8 +439,13 @@ impl<'a> Arbitrary<'a> for MathOp {
#[cfg(test)]
mod tests {
- use super::{MathOp, NumOpError, NumOpResult};
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+ #[cfg(feature = "std")]
+ use std::error::Error;
+
use crate::{Amount, FeeRate, Weight};
+ use crate::result::{MathOp, NumOpError, NumOpResult};
#[test]
fn mathop_predicates() {
@@ -605,4 +610,14 @@ mod tests {
});
assert_eq!(res_err, res);
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn error_display_is_non_empty() {
+ // NumOpError - math operation error
+ let e = (Amount::MAX + Amount::MAX).unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_none());
+ }
}
diff --git a/units/src/sequence.rs b/units/src/sequence.rs
index ba73189d..1c8b67a3 100644
--- a/units/src/sequence.rs
+++ b/units/src/sequence.rs
@@ -383,9 +383,13 @@ impl<'a> Arbitrary<'a> for Sequence {
mod tests {
#[cfg(feature = "alloc")]
use alloc::format;
+ #[cfg(all(feature = "encoding", feature = "alloc"))]
+ use alloc::string::ToString;
+ #[cfg(all(feature = "encoding", feature = "std"))]
+ use std::error::Error;
#[cfg(feature = "encoding")]
- use encoding::Decoder as _;
+ use encoding::{Decodable as _, Decoder as _};
#[cfg(all(feature = "encoding", feature = "alloc"))]
use encoding::UnexpectedEofError;
@@ -504,4 +508,19 @@ mod tests {
let error = decoder.end().unwrap_err();
assert!(matches!(error, SequenceDecoderError(UnexpectedEofError { .. })));
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn decoder_error_display_is_non_empty() {
+ #[cfg(feature = "encoding")]
+ {
+ // SequenceDecoderError
+ let mut decoder = Sequence::decoder();
+ let _ = decoder.push_bytes(&mut [0u8; 3].as_slice());
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
+ }
+ }
}
diff --git a/units/src/time.rs b/units/src/time.rs
index 71a68dd8..d48a3c99 100644
--- a/units/src/time.rs
+++ b/units/src/time.rs
@@ -177,6 +177,8 @@ impl<'a> Arbitrary<'a> for BlockTime {
mod tests {
#[cfg(feature = "alloc")]
use alloc::string::ToString;
+ #[cfg(all(feature = "encoding", feature = "std"))]
+ use std::error::Error;
#[cfg(feature = "encoding")]
use encoding::Decoder as _;
@@ -229,8 +231,11 @@ mod tests {
let bytes = [0xb0, 0x52, 0x39]; // 3 bytes is an EOF error
let mut decoder = BlockTimeDecoder::default();
- assert!(decoder.push_bytes(&mut bytes.as_slice()).unwrap());
+ let _ = decoder.push_bytes(&mut bytes.as_slice());
- assert_ne!(decoder.end().unwrap_err().to_string(), "");
+ let e = decoder.end().unwrap_err();
+ assert!(!e.to_string().is_empty());
+ #[cfg(feature = "std")]
+ assert!(e.source().is_some());
}
}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.