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:
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);
+}