lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit cd6fd08a5a557bf0744ab4e75bf956ae642fb8ce
parent b1f45c374f4914aa691ba15e4e34c475ebf559fc
Author: triesap <tyson@radroots.org>
Date:   Sat, 21 Feb 2026 17:48:05 +0000

core: add branch-complete tests for unit and quantity paths


- expand unit tests to cover code helpers predicates display and serde roundtrips
- expand quantity tests to cover zero canonical scale display and operator impl paths
- cover quantity invariant error display and same-unit checked add-sub success paths
- run cargo check -q -p `radroots-core` cargo test -q -p `radroots-core` and core coverage remeasure

Diffstat:
Mcrates/core/tests/quantity.rs | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/core/tests/unit.rs | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 144 insertions(+), 0 deletions(-)

diff --git a/crates/core/tests/quantity.rs b/crates/core/tests/quantity.rs @@ -3,6 +3,17 @@ mod common; use radroots_core::{RadrootsCoreQuantityInvariantError, RadrootsCoreUnit}; #[test] +fn zero_helpers_and_scale_paths_are_exercised() { + let zero = radroots_core::RadrootsCoreQuantity::zero(RadrootsCoreUnit::MassKg); + assert!(zero.is_zero()); + assert_eq!(zero.canonical_unit(), RadrootsCoreUnit::MassG); + assert!(!zero.is_canonical()); + + let scaled = common::qty("1.2300", RadrootsCoreUnit::Each).with_scale(1); + assert_eq!(scaled.amount, common::dec("1.2")); +} + +#[test] fn label_helpers_set_and_clear() { let q = common::qty("1", RadrootsCoreUnit::Each).with_label("box"); assert_eq!(q.label.as_deref(), Some("box")); @@ -55,6 +66,19 @@ fn checked_add_and_sub_return_none_on_mismatch() { } #[test] +fn checked_add_and_sub_return_some_on_matching_units() { + let a = common::qty("5", RadrootsCoreUnit::Each).with_label("lhs"); + let b = common::qty("2", RadrootsCoreUnit::Each); + let added = a.checked_add(&b).expect("added quantity"); + assert_eq!(added.amount, common::dec("7")); + assert_eq!(added.label.as_deref(), Some("lhs")); + + let subbed = a.checked_sub(&b).expect("subbed quantity"); + assert_eq!(subbed.amount, common::dec("3")); + assert_eq!(subbed.label.as_deref(), Some("lhs")); +} + +#[test] fn mul_and_div_preserve_unit_and_label() { let q = common::qty("2", RadrootsCoreUnit::Each).with_label("unit"); let scaled = q.clone().mul_decimal(common::dec("2.5")); @@ -69,12 +93,39 @@ fn mul_and_div_preserve_unit_and_label() { } #[test] +fn mul_and_div_operator_impls_are_exercised() { + let qty = common::qty("4", RadrootsCoreUnit::Each).with_label("bag"); + let mul = qty.clone() * common::dec("1.5"); + assert_eq!(mul.amount, common::dec("6")); + assert_eq!(mul.label.as_deref(), Some("bag")); + + let div = qty / common::dec("2"); + assert_eq!(div.amount, common::dec("2")); + assert_eq!(div.label.as_deref(), Some("bag")); +} + +#[test] fn display_includes_label_when_present() { let q = common::qty("1.5", RadrootsCoreUnit::Each).with_label("bag"); assert_eq!(q.to_string(), "1.5 each (bag)"); } #[test] +fn display_without_label_and_error_display_are_exercised() { + let q = common::qty("1.5", RadrootsCoreUnit::Each); + assert_eq!(q.to_string(), "1.5 each"); + + assert_eq!( + RadrootsCoreQuantityInvariantError::NegativeAmount.to_string(), + "quantity amount must be ≥ 0" + ); + assert_eq!( + RadrootsCoreQuantityInvariantError::UnitMismatch.to_string(), + "quantity unit mismatch" + ); +} + +#[test] fn try_convert_to_changes_unit_and_amount() { let q = common::qty("1", RadrootsCoreUnit::MassKg); let converted = q.try_convert_to(RadrootsCoreUnit::MassG).unwrap(); @@ -102,3 +153,10 @@ fn try_convert_to_rejects_mismatched_dimensions() { } ); } + +#[test] +fn try_convert_to_same_unit_returns_self_clone() { + let q = common::qty("2", RadrootsCoreUnit::MassG).with_label("x"); + let converted = q.try_convert_to(RadrootsCoreUnit::MassG).expect("same unit"); + assert_eq!(converted, q); +} diff --git a/crates/core/tests/unit.rs b/crates/core/tests/unit.rs @@ -61,6 +61,25 @@ fn canonical_unit_maps_dimensions() { } #[test] +fn code_and_predicate_helpers_cover_all_variants() { + use RadrootsCoreUnit::*; + assert_eq!(Each.code(), "each"); + assert_eq!(MassKg.code(), "kg"); + assert_eq!(MassG.code(), "g"); + assert_eq!(MassOz.code(), "oz"); + assert_eq!(MassLb.code(), "lb"); + assert_eq!(VolumeL.code(), "l"); + assert_eq!(VolumeMl.code(), "ml"); + + assert!(Each.is_count()); + assert!(!Each.is_mass()); + assert!(!Each.is_volume()); + assert!(MassLb.is_mass()); + assert!(!MassLb.is_count()); + assert!(VolumeMl.is_volume()); +} + +#[test] fn parse_mass_unit_enforces_mass_only() { assert_eq!(parse_mass_unit("kg"), Ok(RadrootsCoreUnit::MassKg)); assert_eq!( @@ -117,6 +136,23 @@ fn convert_volume_decimal_converts_between_volume_units() { } #[test] +fn convert_volume_decimal_rejects_non_volume_units() { + let err = convert_volume_decimal( + common::dec("1"), + RadrootsCoreUnit::Each, + RadrootsCoreUnit::VolumeMl, + ) + .unwrap_err(); + assert_eq!( + err, + RadrootsCoreUnitConvertError::NotVolumeUnit { + from: RadrootsCoreUnit::Each, + to: RadrootsCoreUnit::VolumeMl + } + ); +} + +#[test] fn convert_unit_decimal_converts_matching_dimensions() { use RadrootsCoreUnit::*; let kg_to_g = convert_unit_decimal(common::dec("1"), MassKg, MassG).unwrap(); @@ -128,6 +164,47 @@ fn convert_unit_decimal_converts_matching_dimensions() { } #[test] +fn display_paths_for_unit_and_errors_are_exercised() { + assert_eq!(RadrootsCoreUnit::MassOz.to_string(), "oz"); + assert_eq!( + RadrootsCoreUnitParseError::UnknownUnit.to_string(), + "unknown unit string" + ); + assert_eq!( + RadrootsCoreUnitParseError::NotAMassUnit.to_string(), + "unit is not a mass unit" + ); + assert_eq!( + RadrootsCoreUnitParseError::NotAVolumeUnit.to_string(), + "unit is not a volume unit" + ); + assert_eq!( + RadrootsCoreUnitConvertError::NotMassUnit { + from: RadrootsCoreUnit::Each, + to: RadrootsCoreUnit::MassG + } + .to_string(), + "unit conversion requires mass units: each -> g" + ); + assert_eq!( + RadrootsCoreUnitConvertError::NotVolumeUnit { + from: RadrootsCoreUnit::Each, + to: RadrootsCoreUnit::VolumeL + } + .to_string(), + "unit conversion requires volume units: each -> l" + ); + assert_eq!( + RadrootsCoreUnitConvertError::NotConvertibleUnits { + from: RadrootsCoreUnit::Each, + to: RadrootsCoreUnit::MassG + } + .to_string(), + "unit conversion requires matching dimensions: each -> g" + ); +} + +#[test] fn convert_unit_decimal_rejects_mismatched_dimensions() { let err = convert_unit_decimal( common::dec("1"), @@ -143,3 +220,12 @@ fn convert_unit_decimal_rejects_mismatched_dimensions() { } ); } + +#[cfg(feature = "serde")] +#[test] +fn serde_roundtrip_for_unit_paths() { + let json = serde_json::to_string(&RadrootsCoreUnit::VolumeL).unwrap(); + assert_eq!(json, "\"l\""); + let back: RadrootsCoreUnit = serde_json::from_str("\"kg\"").unwrap(); + assert_eq!(back, RadrootsCoreUnit::MassKg); +}