From fdf5299d42755ccfa2fbf56d3ff74127857e91e2 Mon Sep 17 00:00:00 2001 From: "Lan, Jian" Date: Sun, 1 Feb 2026 16:32:20 +0800 Subject: [PATCH 1/2] refactor: Empty patchable pacakge default Cargo feature --- patchable/Cargo.toml | 4 +++- patchable/src/lib.rs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/patchable/Cargo.toml b/patchable/Cargo.toml index e7b80db..94accf5 100644 --- a/patchable/Cargo.toml +++ b/patchable/Cargo.toml @@ -21,7 +21,9 @@ serde_json.workspace = true anyhow.workspace = true [features] -default = ["serde", "cloneable"] +default = [] +common = ["serde", "cloneable"] +full = ["serde", "cloneable", "impl_from"] serde = ["patchable-macro/serde"] cloneable = ["patchable-macro/cloneable"] impl_from = ["patchable-macro/impl_from"] diff --git a/patchable/src/lib.rs b/patchable/src/lib.rs index 7e0788b..9e3d979 100644 --- a/patchable/src/lib.rs +++ b/patchable/src/lib.rs @@ -213,6 +213,7 @@ pub(crate) mod test { current_base: MeasurementResult, } + #[cfg(feature = "common")] #[test] fn test_scoped_peek() -> anyhow::Result<()> { fn identity(x: &i32) -> i32 { @@ -246,6 +247,7 @@ pub(crate) mod test { val: i32, } + #[cfg(feature = "common")] #[test] fn test_try_patch_blanket_impl() { let mut s = SimpleStruct { val: 10 }; @@ -299,6 +301,7 @@ pub(crate) mod test { #[derive(Clone, Debug, PartialEq, Eq)] struct TupleStruct(i32, u32); + #[cfg(feature = "common")] #[test] fn test_tuple_struct_patch() { let mut s = TupleStruct(1, 2); @@ -311,6 +314,7 @@ pub(crate) mod test { #[derive(Clone, Debug, PartialEq, Eq)] struct UnitStruct; + #[cfg(feature = "common")] #[test] fn test_unit_struct_patch() { let mut s = UnitStruct; @@ -327,6 +331,7 @@ pub(crate) mod test { value: i32, } + #[cfg(feature = "common")] #[test] fn test_skip_serializing_field_is_excluded() { let mut s = SkipSerializingStruct { From af05b775d4ac5e65fd03dd71c201f5410c4b8be9 Mon Sep 17 00:00:00 2001 From: "Lan, Jian" Date: Sun, 1 Feb 2026 20:27:05 +0800 Subject: [PATCH 2/2] refactor: move tests from `lib.rs` into dedicated test files. --- patchable/Cargo.toml | 12 ++ patchable/src/lib.rs | 221 ----------------------------------- patchable/tests/basic.rs | 61 ++++++++++ patchable/tests/impl_from.rs | 32 +++++ patchable/tests/serde.rs | 115 ++++++++++++++++++ 5 files changed, 220 insertions(+), 221 deletions(-) create mode 100644 patchable/tests/basic.rs create mode 100644 patchable/tests/impl_from.rs create mode 100644 patchable/tests/serde.rs diff --git a/patchable/Cargo.toml b/patchable/Cargo.toml index 94accf5..f25eddd 100644 --- a/patchable/Cargo.toml +++ b/patchable/Cargo.toml @@ -30,3 +30,15 @@ impl_from = ["patchable-macro/impl_from"] [dev-dependencies] thiserror.workspace = true +serde.workspace = true + +[[test]] +name = "basic" + +[[test]] +name = "serde" +required-features = ["serde"] + +[[test]] +name = "impl_from" +required-features = ["impl_from"] diff --git a/patchable/src/lib.rs b/patchable/src/lib.rs index 9e3d979..e5ee832 100644 --- a/patchable/src/lib.rs +++ b/patchable/src/lib.rs @@ -184,224 +184,3 @@ impl TryPatch for T { Ok(()) } } - -#[cfg(test)] -pub(crate) mod test { - use std::fmt::Debug; - - use super::*; - use patchable_macro::patchable_model; - use serde::{Deserialize, Serialize}; - - #[patchable_model] - #[derive(Clone, Default, Debug, PartialEq)] - struct FakeMeasurement { - v: T, - #[patchable(skip)] - how: ClosureType, - } - - #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] - struct MeasurementResult(pub T); - - #[patchable_model] - #[derive(Clone, Debug)] - struct ScopedMeasurement { - current_control_level: ScopeType, - #[patchable] - inner: MeasurementType, - current_base: MeasurementResult, - } - - #[cfg(feature = "common")] - #[test] - fn test_scoped_peek() -> anyhow::Result<()> { - fn identity(x: &i32) -> i32 { - *x - } - - let fake_measurement: FakeMeasurement i32> = FakeMeasurement { - v: 42, - how: identity, - }; - let scoped_peek0 = ScopedMeasurement { - current_control_level: 33u32, - inner: fake_measurement.clone(), - current_base: MeasurementResult(20i32), - }; - let mut scoped_peek1 = ScopedMeasurement { - current_control_level: 0u32, - inner: fake_measurement.clone(), - current_base: MeasurementResult(0i32), - }; - let state0 = serde_json::to_string(&scoped_peek0)?; - scoped_peek1.patch(serde_json::from_str(&state0)?); - let state1 = serde_json::to_string(&scoped_peek0)?; - assert!(state0 == state1); - Ok(()) - } - - #[patchable_model] - #[derive(Clone, Default, Debug)] - struct SimpleStruct { - val: i32, - } - - #[cfg(feature = "common")] - #[test] - fn test_try_patch_blanket_impl() { - let mut s = SimpleStruct { val: 10 }; - // The derived patch struct is compatible with serde. - // We use from_str to create the patch value. - let patch: ::Patch = - serde_json::from_str(r#"{"val": 20}"#).unwrap(); - - // Should always succeed for `Patch` types due to the blanket impl. - let result = s.try_patch(patch); - assert!(result.is_ok()); - assert_eq!(s.val, 20); - } - - #[allow(dead_code)] - #[patchable_model] - #[derive(Clone, Debug, PartialEq)] - struct Inner { - value: i32, - } - - #[allow(dead_code)] - #[patchable_model] - #[derive(Clone, Debug, PartialEq)] - struct Outer { - #[patchable] - inner: InnerType, - extra: u32, - } - - // TODO: Not testing `impl_from` feature. Need fix. - #[cfg(feature = "impl_from")] - #[test] - fn test_from_struct_to_patch() { - let original = Outer { - inner: Inner { value: 42 }, - extra: 7, - }; - - let patch: as Patchable>::Patch = original.clone().into(); - let mut target = Outer { - inner: Inner { value: 0 }, - extra: 0, - }; - - target.patch(patch); - assert_eq!(target, original); - } - - #[patchable_model] - #[derive(Clone, Debug, PartialEq, Eq)] - struct TupleStruct(i32, u32); - - #[cfg(feature = "common")] - #[test] - fn test_tuple_struct_patch() { - let mut s = TupleStruct(1, 2); - let patch: ::Patch = serde_json::from_str(r#"[10, 20]"#).unwrap(); - s.patch(patch); - assert_eq!(s, TupleStruct(10, 20)); - } - - #[patchable_model] - #[derive(Clone, Debug, PartialEq, Eq)] - struct UnitStruct; - - #[cfg(feature = "common")] - #[test] - fn test_unit_struct_patch() { - let mut s = UnitStruct; - let patch: ::Patch = serde_json::from_str("null").unwrap(); - s.patch(patch); - assert_eq!(s, UnitStruct); - } - - #[patchable_model] - #[derive(Clone, Debug)] - struct SkipSerializingStruct { - #[patchable(skip)] - skipped: i32, - value: i32, - } - - #[cfg(feature = "common")] - #[test] - fn test_skip_serializing_field_is_excluded() { - let mut s = SkipSerializingStruct { - skipped: 5, - value: 10, - }; - let patch: ::Patch = - serde_json::from_str(r#"{"value": 42}"#).unwrap(); - s.patch(patch); - assert_eq!(s.skipped, 5); - assert_eq!(s.value, 42); - } - - #[derive(Debug)] - struct FallibleStruct { - value: i32, - } - - #[derive(Debug, Clone)] - struct FalliblePatch(i32); - - #[derive(Debug)] - struct PatchError(String); - - impl std::fmt::Display for PatchError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "PatchError: {}", self.0) - } - } - - impl std::error::Error for PatchError {} - - impl Patchable for FallibleStruct { - type Patch = FalliblePatch; - } - - impl From for FalliblePatch { - fn from(s: FallibleStruct) -> Self { - FalliblePatch(s.value) - } - } - - impl TryPatch for FallibleStruct { - type Error = PatchError; - - fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error> { - if patch.0 < 0 { - return Err(PatchError("Value cannot be negative".to_string())); - } - self.value = patch.0; - Ok(()) - } - } - - #[test] - fn test_try_patch_custom_error() { - let mut s = FallibleStruct { value: 0 }; - - // Valid patch - assert!(s.try_patch(FalliblePatch(10)).is_ok()); - assert_eq!(s.value, 10); - - // Invalid patch - let result = s.try_patch(FalliblePatch(-5)); - assert!(result.is_err()); - assert_eq!(s.value, 10); // Should not have changed - - match result { - Err(e) => assert_eq!(e.to_string(), "PatchError: Value cannot be negative"), - _ => panic!("Expected error"), - } - } -} diff --git a/patchable/tests/basic.rs b/patchable/tests/basic.rs new file mode 100644 index 0000000..0a1b168 --- /dev/null +++ b/patchable/tests/basic.rs @@ -0,0 +1,61 @@ +use patchable::{Patchable, TryPatch}; + +#[derive(Debug)] +struct FallibleStruct { + value: i32, +} + +#[derive(Debug, Clone)] +struct FallibleStructPatch(i32); + +#[derive(Debug)] +struct PatchError(String); + +impl std::fmt::Display for PatchError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PatchError: {}", self.0) + } +} + +impl std::error::Error for PatchError {} + +impl Patchable for FallibleStruct { + type Patch = FallibleStructPatch; +} + +impl From for FallibleStructPatch { + fn from(s: FallibleStruct) -> Self { + FallibleStructPatch(s.value) + } +} + +impl TryPatch for FallibleStruct { + type Error = PatchError; + + fn try_patch(&mut self, patch: Self::Patch) -> Result<(), Self::Error> { + if patch.0 < 0 { + return Err(PatchError("Value cannot be negative".to_string())); + } + self.value = patch.0; + Ok(()) + } +} + +#[test] +fn test_try_patch_custom_error() { + let mut s = FallibleStruct { value: 0 }; + + // Valid patch + assert!(s.try_patch(FallibleStructPatch(10)).is_ok()); + assert_eq!(s.value, 10); + + // Invalid patch + let result = s.try_patch(FallibleStructPatch(-5)); + assert!(result.is_err()); + assert_eq!(s.value, 10); // Should not have changed + + match result { + Err(e) => assert_eq!(e.to_string(), "PatchError: Value cannot be negative"), + _ => panic!("Expected error"), + } +} diff --git a/patchable/tests/impl_from.rs b/patchable/tests/impl_from.rs new file mode 100644 index 0000000..9df87de --- /dev/null +++ b/patchable/tests/impl_from.rs @@ -0,0 +1,32 @@ +use patchable::{Patch, Patchable, patchable_model}; + +#[patchable_model] +#[derive(Clone, Debug, PartialEq)] +struct Inner { + value: i32, +} + +#[patchable_model] +#[derive(Clone, Debug, PartialEq)] +struct Outer { + #[patchable] + inner: InnerType, + extra: u32, +} + +#[test] +fn test_from_struct_to_patch() { + let original = Outer { + inner: Inner { value: 42 }, + extra: 7, + }; + + let patch: as Patchable>::Patch = original.clone().into(); + let mut target = Outer { + inner: Inner { value: 0 }, + extra: 0, + }; + + target.patch(patch); + assert_eq!(target, original); +} diff --git a/patchable/tests/serde.rs b/patchable/tests/serde.rs new file mode 100644 index 0000000..4b67d6f --- /dev/null +++ b/patchable/tests/serde.rs @@ -0,0 +1,115 @@ +use std::fmt::Debug; + +use patchable::{Patch, Patchable, TryPatch, patchable_model}; +use serde::{Deserialize, Serialize}; + +#[patchable_model] +#[derive(Clone, Default, Debug, PartialEq)] +struct FakeMeasurement { + v: T, + #[patchable(skip)] + how: ClosureType, +} + +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +struct MeasurementResult(pub T); + +#[patchable_model] +#[derive(Clone, Debug)] +struct ScopedMeasurement { + current_control_level: ScopeType, + #[patchable] + inner: MeasurementType, + current_base: MeasurementResult, +} + +#[test] +fn test_scoped_peek() -> anyhow::Result<()> { + fn identity(x: &i32) -> i32 { + *x + } + + let fake_measurement: FakeMeasurement i32> = FakeMeasurement { + v: 42, + how: identity, + }; + let scoped_peek0 = ScopedMeasurement { + current_control_level: 33u32, + inner: fake_measurement.clone(), + current_base: MeasurementResult(20i32), + }; + let mut scoped_peek1 = ScopedMeasurement { + current_control_level: 0u32, + inner: fake_measurement.clone(), + current_base: MeasurementResult(0i32), + }; + let state0 = serde_json::to_string(&scoped_peek0)?; + scoped_peek1.patch(serde_json::from_str(&state0)?); + let state1 = serde_json::to_string(&scoped_peek0)?; + assert!(state0 == state1); + Ok(()) +} + +#[patchable_model] +#[derive(Clone, Default, Debug)] +struct SimpleStruct { + val: i32, +} + +#[test] +fn test_try_patch_blanket_impl() { + let mut s = SimpleStruct { val: 10 }; + // The derived patch struct is compatible with serde. + // We use from_str to create the patch value. + let patch: ::Patch = serde_json::from_str(r#"{"val": 20}"#).unwrap(); + + // Should always succeed for `Patch` types due to the blanket impl. + let result = s.try_patch(patch); + assert!(result.is_ok()); + assert_eq!(s.val, 20); +} + +#[patchable_model] +#[derive(Clone, Debug, PartialEq, Eq)] +struct TupleStruct(i32, u32); + +#[test] +fn test_tuple_struct_patch() { + let mut s = TupleStruct(1, 2); + let patch: ::Patch = serde_json::from_str(r#"[10, 20]"#).unwrap(); + s.patch(patch); + assert_eq!(s, TupleStruct(10, 20)); +} + +#[patchable_model] +#[derive(Clone, Debug, PartialEq, Eq)] +struct UnitStruct; + +#[test] +fn test_unit_struct_patch() { + let mut s = UnitStruct; + let patch: ::Patch = serde_json::from_str("null").unwrap(); + s.patch(patch); + assert_eq!(s, UnitStruct); +} + +#[patchable_model] +#[derive(Clone, Debug)] +struct SkipSerializingStruct { + #[patchable(skip)] + skipped: i32, + value: i32, +} + +#[test] +fn test_skip_serializing_field_is_excluded() { + let mut s = SkipSerializingStruct { + skipped: 5, + value: 10, + }; + let patch: ::Patch = + serde_json::from_str(r#"{"value": 42}"#).unwrap(); + s.patch(patch); + assert_eq!(s.skipped, 5); + assert_eq!(s.value, 42); +}