quantity.rs (6954B)
1 mod common; 2 3 use radroots_core::{RadrootsCoreQuantityInvariantError, RadrootsCoreUnit}; 4 5 #[test] 6 fn zero_helpers_and_scale_paths_are_exercised() { 7 let zero = radroots_core::RadrootsCoreQuantity::zero(RadrootsCoreUnit::MassKg); 8 assert!(zero.is_zero()); 9 assert_eq!(zero.canonical_unit(), RadrootsCoreUnit::MassG); 10 assert!(!zero.is_canonical()); 11 12 let scaled = common::qty("1.2300", RadrootsCoreUnit::Each).with_scale(1); 13 assert_eq!(scaled.amount, common::dec("1.2")); 14 } 15 16 #[test] 17 fn label_helpers_set_and_clear() { 18 let q = common::qty("1", RadrootsCoreUnit::Each).with_label("box"); 19 assert_eq!(q.label.as_deref(), Some("box")); 20 21 let q = q.clear_label(); 22 assert!(q.label.is_none()); 23 24 let q = common::qty("1", RadrootsCoreUnit::Each).with_optional_label(Some("case")); 25 assert_eq!(q.label.as_deref(), Some("case")); 26 27 let q = q.with_optional_label::<&str>(None); 28 assert!(q.label.is_none()); 29 } 30 31 #[test] 32 fn ensure_non_negative_rejects_negative_amount() { 33 let q = common::qty("-1", RadrootsCoreUnit::Each); 34 assert_eq!( 35 q.ensure_non_negative(), 36 Err(RadrootsCoreQuantityInvariantError::NegativeAmount) 37 ); 38 } 39 40 #[test] 41 fn ensure_non_negative_accepts_non_negative_amount() { 42 let q = common::qty("0", RadrootsCoreUnit::Each); 43 assert_eq!(q.ensure_non_negative(), Ok(())); 44 } 45 46 #[test] 47 fn try_add_and_try_sub_require_matching_units() { 48 let a = common::qty("1", RadrootsCoreUnit::Each).with_label("lhs"); 49 let b = common::qty("2", RadrootsCoreUnit::Each); 50 let c = common::qty("1", RadrootsCoreUnit::MassKg); 51 52 let sum = a.try_add(&b).unwrap(); 53 assert_eq!(sum.amount, common::dec("3")); 54 assert_eq!(sum.label.as_deref(), Some("lhs")); 55 56 assert_eq!( 57 a.try_add(&c), 58 Err(RadrootsCoreQuantityInvariantError::UnitMismatch) 59 ); 60 assert_eq!( 61 b.try_sub(&c), 62 Err(RadrootsCoreQuantityInvariantError::UnitMismatch) 63 ); 64 } 65 66 #[test] 67 fn try_sub_success_path_is_exercised() { 68 let a = common::qty("4", RadrootsCoreUnit::Each).with_label("lhs"); 69 let b = common::qty("1", RadrootsCoreUnit::Each); 70 let out = a.try_sub(&b).expect("sub result"); 71 assert_eq!(out.amount, common::dec("3")); 72 assert_eq!(out.label.as_deref(), Some("lhs")); 73 } 74 75 #[test] 76 fn checked_add_and_sub_return_none_on_mismatch() { 77 let a = common::qty("1", RadrootsCoreUnit::Each); 78 let b = common::qty("2", RadrootsCoreUnit::MassG); 79 assert!(a.checked_add(&b).is_none()); 80 assert!(a.checked_sub(&b).is_none()); 81 } 82 83 #[test] 84 fn checked_add_and_sub_return_some_on_matching_units() { 85 let a = common::qty("5", RadrootsCoreUnit::Each).with_label("lhs"); 86 let b = common::qty("2", RadrootsCoreUnit::Each); 87 let added = a.checked_add(&b).expect("added quantity"); 88 assert_eq!(added.amount, common::dec("7")); 89 assert_eq!(added.label.as_deref(), Some("lhs")); 90 91 let subbed = a.checked_sub(&b).expect("subbed quantity"); 92 assert_eq!(subbed.amount, common::dec("3")); 93 assert_eq!(subbed.label.as_deref(), Some("lhs")); 94 } 95 96 #[test] 97 fn mul_and_div_preserve_unit_and_label() { 98 let q = common::qty("2", RadrootsCoreUnit::Each).with_label("unit"); 99 let scaled = q.clone().mul_decimal(common::dec("2.5")); 100 assert_eq!(scaled.amount, common::dec("5")); 101 assert_eq!(scaled.unit, RadrootsCoreUnit::Each); 102 assert_eq!(scaled.label.as_deref(), Some("unit")); 103 104 let divided = q.div_decimal(common::dec("2")); 105 assert_eq!(divided.amount, common::dec("1")); 106 assert_eq!(divided.unit, RadrootsCoreUnit::Each); 107 assert_eq!(divided.label.as_deref(), Some("unit")); 108 } 109 110 #[test] 111 fn mul_and_div_operator_impls_are_exercised() { 112 let qty = common::qty("4", RadrootsCoreUnit::Each).with_label("bag"); 113 let mul = qty.clone() * common::dec("1.5"); 114 assert_eq!(mul.amount, common::dec("6")); 115 assert_eq!(mul.label.as_deref(), Some("bag")); 116 117 let div = qty / common::dec("2"); 118 assert_eq!(div.amount, common::dec("2")); 119 assert_eq!(div.label.as_deref(), Some("bag")); 120 } 121 122 #[test] 123 fn display_includes_label_when_present() { 124 let q = common::qty("1.5", RadrootsCoreUnit::Each).with_label("bag"); 125 assert_eq!(q.to_string(), "1.5 each (bag)"); 126 } 127 128 #[test] 129 fn display_without_label_and_error_display_are_exercised() { 130 let q = common::qty("1.5", RadrootsCoreUnit::Each); 131 assert_eq!(q.to_string(), "1.5 each"); 132 133 assert_eq!( 134 RadrootsCoreQuantityInvariantError::NegativeAmount.to_string(), 135 "quantity amount must be ≥ 0" 136 ); 137 assert_eq!( 138 RadrootsCoreQuantityInvariantError::UnitMismatch.to_string(), 139 "quantity unit mismatch" 140 ); 141 } 142 143 #[test] 144 fn display_propagates_formatter_errors() { 145 use core::fmt::{self, Write}; 146 147 struct FailWriter { 148 fail_on_paren: bool, 149 fail_on_call: usize, 150 calls: usize, 151 } 152 153 impl Write for FailWriter { 154 fn write_str(&mut self, s: &str) -> fmt::Result { 155 self.calls += 1; 156 if self.fail_on_paren && s.contains('(') { 157 return Err(fmt::Error); 158 } 159 if self.calls == self.fail_on_call { 160 Err(fmt::Error) 161 } else { 162 Ok(()) 163 } 164 } 165 } 166 167 let with_label = common::qty("1.5", RadrootsCoreUnit::Each).with_label("bag"); 168 let mut first_write_fails = FailWriter { 169 fail_on_paren: false, 170 fail_on_call: 1, 171 calls: 0, 172 }; 173 assert!(fmt::write(&mut first_write_fails, format_args!("{with_label}")).is_err()); 174 175 let mut second_write_fails = FailWriter { 176 fail_on_paren: true, 177 fail_on_call: usize::MAX, 178 calls: 0, 179 }; 180 assert!(fmt::write(&mut second_write_fails, format_args!("{with_label}")).is_err()); 181 } 182 183 #[test] 184 fn try_convert_to_changes_unit_and_amount() { 185 let q = common::qty("1", RadrootsCoreUnit::MassKg); 186 let converted = q.try_convert_to(RadrootsCoreUnit::MassG).unwrap(); 187 assert_eq!(converted.amount, common::dec("1000")); 188 assert_eq!(converted.unit, RadrootsCoreUnit::MassG); 189 } 190 191 #[test] 192 fn to_canonical_converts_mass_and_volume() { 193 let q = common::qty("2", RadrootsCoreUnit::VolumeL); 194 let canonical = q.to_canonical().unwrap(); 195 assert_eq!(canonical.unit, RadrootsCoreUnit::VolumeMl); 196 assert_eq!(canonical.amount, common::dec("2000")); 197 } 198 199 #[test] 200 fn try_convert_to_rejects_mismatched_dimensions() { 201 let q = common::qty("1", RadrootsCoreUnit::Each); 202 let err = q.try_convert_to(RadrootsCoreUnit::MassG).unwrap_err(); 203 assert_eq!( 204 err, 205 radroots_core::RadrootsCoreUnitConvertError::NotConvertibleUnits { 206 from: RadrootsCoreUnit::Each, 207 to: RadrootsCoreUnit::MassG 208 } 209 ); 210 } 211 212 #[test] 213 fn try_convert_to_same_unit_returns_self_clone() { 214 let q = common::qty("2", RadrootsCoreUnit::MassG).with_label("x"); 215 let converted = q 216 .try_convert_to(RadrootsCoreUnit::MassG) 217 .expect("same unit"); 218 assert_eq!(converted, q); 219 }