This is an automated email from the ASF dual-hosted git repository.

tisonkun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datasketches-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 8d9a36a  test: streamline deserialization regressions (#228)
8d9a36a is described below

commit 8d9a36ac54f592f242ad208bfc85818fa3ac04c6
Author: tison <[email protected]>
AuthorDate: Wed Aug 26 17:14:22 2026 +0800

    test: streamline deserialization regressions (#228)
---
 datasketches/tests/cpc_test/deserialize.rs    |  77 +-------------
 datasketches/tests/serde_tests/frequencies.rs | 147 ++++++--------------------
 datasketches/tests/serde_tests/req.rs         |   4 +-
 3 files changed, 38 insertions(+), 190 deletions(-)

diff --git a/datasketches/tests/cpc_test/deserialize.rs 
b/datasketches/tests/cpc_test/deserialize.rs
index 3a77577..3bd7886 100644
--- a/datasketches/tests/cpc_test/deserialize.rs
+++ b/datasketches/tests/cpc_test/deserialize.rs
@@ -19,7 +19,6 @@
 
 use datasketches::cpc::CpcSketch;
 
-/// Builds a valid serialized sketch that exercises a particular CPC flavor.
 fn valid_bytes(lg_k: u8, n: u64) -> Vec<u8> {
     let mut sketch = CpcSketch::new(lg_k);
     for i in 0..n {
@@ -28,17 +27,6 @@ fn valid_bytes(lg_k: u8, n: u64) -> Vec<u8> {
     sketch.serialize()
 }
 
-/// Each non-empty entry lands in a different CPC flavor.
-const CASES: &[(u8, u64)] = &[
-    (4, 0),
-    (4, 3),
-    (6, 20),
-    (8, 200),
-    (8, 2000),
-    (10, 800),
-    (10, 8000),
-];
-
 #[test]
 fn truncated_compressed_streams_return_errors() {
     let bytes = valid_bytes(10, 8_000);
@@ -51,65 +39,8 @@ fn truncated_compressed_streams_return_errors() {
 }
 
 #[test]
-fn targeted_corruptions_return_err() {
-    // A sliding-flavor sketch drives the pair/window decoders and the 
offset/permutation logic.
-    let base = valid_bytes(10, 8000);
-
-    // Layout: [preamble_ints, serial_version, family, lg_k, 
first_interesting_column, flags,
-    //          seed_hash(2), num_coupons(4), ...]. Corrupting the num_coupons 
field makes the
-    //          decoders read past the compressed buffer / compute an 
out-of-range window offset.
-    let mut num_coupons_hi = base.clone();
-    num_coupons_hi[11] = 0xff; // enormous num_coupons
-    assert!(CpcSketch::deserialize(&num_coupons_hi).is_err());
-
-    // Flipping the flags byte makes the declared flavor inconsistent with the 
stored data.
-    let mut bad_flags = base.clone();
-    bad_flags[5] ^= 0xff;
-    assert!(CpcSketch::deserialize(&bad_flags).is_err());
-
-    // These payload edits previously reached panicking decoder and pair-table 
paths.
-    let mut bad_payload = base.clone();
-    let last = bad_payload.len() - 1;
-    bad_payload[last] = bad_payload[last].wrapping_add(1);
-    bad_payload[last - 3] ^= 0xa5;
-    let _ = CpcSketch::deserialize(&bad_payload);
-
-    // A sparse sketch whose declared entry count exceeds its data words must 
be rejected up front.
-    let sparse = valid_bytes(8, 200);
-    let mut inflated = sparse.clone();
-    // num_coupons is a u32 at offset 8; inflate it far beyond the coupon 
space.
-    inflated[10] = 0xff;
-    inflated[11] = 0xff;
-    assert!(CpcSketch::deserialize(&inflated).is_err());
-}
-
-#[test]
-fn valid_sketches_round_trip_unchanged() {
-    for &(lg_k, n) in CASES {
-        let mut sketch = CpcSketch::new(lg_k);
-        for i in 0..n {
-            sketch.update(i);
-        }
-        let bytes = sketch.serialize();
-
-        let restored = CpcSketch::deserialize(&bytes).unwrap_or_else(|e| {
-            panic!("valid sketch (lg_k={lg_k}, n={n}) failed to deserialize: 
{e}")
-        });
-
-        assert_eq!(
-            sketch.estimate(),
-            restored.estimate(),
-            "estimate changed after round-trip (lg_k={lg_k}, n={n})"
-        );
-        assert_eq!(
-            sketch.num_coupons(),
-            restored.num_coupons(),
-            "num_coupons changed after round-trip (lg_k={lg_k}, n={n})"
-        );
-        assert_eq!(
-            bytes,
-            restored.serialize(),
-            "re-serialized bytes changed after round-trip (lg_k={lg_k}, n={n})"
-        );
-    }
+fn oversized_coupon_count_is_rejected() {
+    let mut bytes = valid_bytes(10, 8_000);
+    bytes[11] = u8::MAX;
+    assert!(CpcSketch::deserialize(&bytes).is_err());
 }
diff --git a/datasketches/tests/serde_tests/frequencies.rs 
b/datasketches/tests/serde_tests/frequencies.rs
index 75f69b6..1560bae 100644
--- a/datasketches/tests/serde_tests/frequencies.rs
+++ b/datasketches/tests/serde_tests/frequencies.rs
@@ -24,9 +24,7 @@ use datasketches::error::ErrorKind;
 use datasketches::frequencies::FrequentItemValue;
 use datasketches::frequencies::FrequentItemsSketch;
 use googletest::assert_that;
-use googletest::prelude::anything;
 use googletest::prelude::contains_substring;
-use googletest::prelude::err;
 use googletest::prelude::gt;
 
 use crate::serialization_test_data;
@@ -398,133 +396,52 @@ fn test_go_frequent_strings_utf8() {
     assert_eq!(sketch.estimate(&"эюя".to_string()), 7);
 }
 
-// Header field constants for the DataSketches frequent-items format.
-const FREQ_SERIAL_VERSION: u8 = 1;
-const FREQ_FAMILY_ID: u8 = 10;
-const FREQ_PREAMBLE_LONGS_EMPTY: u8 = 1;
-const FREQ_PREAMBLE_LONGS_NONEMPTY: u8 = 4;
-const FREQ_EMPTY_FLAG_MASK: u8 = 5;
-
-fn empty_header(lg_max: u8, lg_cur: u8) -> Vec<u8> {
-    let mut bytes = SketchBytes::with_capacity(8);
-    bytes.write_u8(FREQ_PREAMBLE_LONGS_EMPTY);
-    bytes.write_u8(FREQ_SERIAL_VERSION);
-    bytes.write_u8(FREQ_FAMILY_ID);
-    bytes.write_u8(lg_max);
-    bytes.write_u8(lg_cur);
-    bytes.write_u8(FREQ_EMPTY_FLAG_MASK);
-    bytes.write_u16_le(0);
-    bytes.into_bytes()
-}
-
-// Regression: a corrupt header must never panic (or trigger an oversized
-// allocation) inside `with_lg_map_sizes`; deserialize must reject it cleanly.
-// Before the fix, `lg_max_map_size = 222` drove `1usize << lg_max` past the
-// width of `usize`, panicking with "attempt to shift left with overflow" in
-// debug builds.
-#[test]
-fn test_deserialize_rejects_out_of_range_lg_max_map_size() {
-    let bytes = empty_header(222, 5);
-    let result = FrequentItemsSketch::<i64>::deserialize(&bytes);
-    assert_that!(result, err(anything()));
-    assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidData);
-}
-
-#[test]
-fn test_deserialize_rejects_out_of_range_lg_max_map_size_nonempty() {
-    let mut bytes = SketchBytes::with_capacity(16);
-    bytes.write_u8(FREQ_PREAMBLE_LONGS_NONEMPTY);
-    bytes.write_u8(FREQ_SERIAL_VERSION);
-    bytes.write_u8(FREQ_FAMILY_ID);
-    bytes.write_u8(200); // lg_max_map_size, out of range
-    bytes.write_u8(3); // lg_cur_map_size
-    bytes.write_u8(0); // flags (not empty)
-    bytes.write_u16_le(0);
-    bytes.write_u32_le(0); // active_items
-    bytes.write_u32_le(0);
-    bytes.write_u64_le(0); // stream_weight
-    bytes.write_u64_le(0); // offset
-    let result = FrequentItemsSketch::<i64>::deserialize(&bytes.into_bytes());
-    assert_that!(result, err(anything()));
-}
-
-// `lg_cur_map_size` below the documented minimum is also corruption; the C++
-// reference `check_size` rejects it, so the Rust port must too.
-#[test]
-fn test_deserialize_rejects_undersized_lg_cur_map_size() {
-    let bytes = empty_header(10, 1);
-    let result = FrequentItemsSketch::<i64>::deserialize(&bytes);
-    assert_that!(result, err(anything()));
-}
-
-// The largest supported configuration must remain cheap while the sketch is
-// empty and round-trip through the public constructor and serializer.
 #[test]
-fn test_maximum_map_size_empty_round_trip() {
-    let sketch = FrequentItemsSketch::<i64>::new(1usize << 30);
-    let bytes = sketch.serialize();
-    let restored = FrequentItemsSketch::<i64>::deserialize(&bytes).unwrap();
-    assert!(restored.is_empty());
-    assert_eq!(restored.lg_max_map_size(), 30);
-    assert_eq!(restored.lg_cur_map_size(), 3);
-}
-
-// Builds a minimal non-empty (four-preamble-long) header. The caller supplies
-// `lg_max`, `lg_cur`, and `active_items`; no item payload is appended, so this
-// is only useful for exercising the header-consistency guards that run before
-// the payload is read.
-fn nonempty_header(lg_max: u8, lg_cur: u8, active_items: u32) -> Vec<u8> {
-    let mut bytes = SketchBytes::with_capacity(32);
-    bytes.write_u8(FREQ_PREAMBLE_LONGS_NONEMPTY);
-    bytes.write_u8(FREQ_SERIAL_VERSION);
-    bytes.write_u8(FREQ_FAMILY_ID);
-    bytes.write_u8(lg_max);
-    bytes.write_u8(lg_cur);
-    bytes.write_u8(0); // flags (not empty)
-    bytes.write_u16_le(0);
-    bytes.write_u32_le(active_items);
-    bytes.write_u32_le(0); // unused
-    bytes.write_u64_le(0); // stream_weight
-    bytes.write_u64_le(0); // offset
-    bytes.into_bytes()
+fn test_deserialize_rejects_invalid_map_sizes() {
+    let empty = FrequentItemsSketch::<i64>::new(32).serialize();
+    let mut nonempty_sketch = FrequentItemsSketch::<i64>::new(32);
+    nonempty_sketch.update(1);
+    let nonempty = nonempty_sketch.serialize();
+
+    for (mut bytes, lg_max, lg_cur) in [(empty.clone(), 222, 3), (nonempty, 
200, 3), (empty, 10, 1)]
+    {
+        bytes[3] = lg_max;
+        bytes[4] = lg_cur;
+        assert_eq!(
+            FrequentItemsSketch::<i64>::deserialize(&bytes)
+                .unwrap_err()
+                .kind(),
+            ErrorKind::InvalidData
+        );
+    }
 }
 
-// Regression for tisonkun's report on #224: the accepted boundary
-// `(lg_max, lg_cur) = (30, 30)` on an *empty* header still drove
-// `ReversePurgeItemHashMap::new(1 << 30)` (~1e9 slots, multi-GB) before any
-// payload was read. An empty sketch holds nothing, so it must now build its 
map
-// at the minimum size and deserialize cheaply instead of attempting the
-// allocation. If the fix regressed, this test would OOM/hang rather than fail.
 #[test]
-fn test_deserialize_empty_header_does_not_over_allocate() {
-    let bytes = empty_header(30, 30);
+fn test_deserialize_empty_header_does_not_allocate_declared_map() {
+    let mut bytes = FrequentItemsSketch::<i64>::new(1usize << 30).serialize();
+    bytes[4] = 30;
     let restored = FrequentItemsSketch::<i64>::deserialize(&bytes).unwrap();
     assert!(restored.is_empty());
-    assert_eq!(restored.num_active_items(), 0);
     assert_eq!(restored.lg_max_map_size(), 30);
     assert_eq!(restored.lg_cur_map_size(), 3);
 }
 
-// A non-empty header whose `lg_cur_map_size` is too small to hold the claimed
-// `active_items` under the load factor is corrupt: a map of `1 << 3` slots has
-// capacity 6, so it can never hold 100 active items. Reject it instead of
-// trusting the header.
 #[test]
 fn test_deserialize_rejects_lg_cur_inconsistent_with_num_active() {
-    let bytes = nonempty_header(10, 3, 100);
-    let result = FrequentItemsSketch::<i64>::deserialize(&bytes);
-    assert_that!(result, err(anything()));
-    assert_eq!(result.unwrap_err().kind(), ErrorKind::InvalidData);
+    let mut sketch = FrequentItemsSketch::<i64>::new(32);
+    sketch.update(1);
+    let mut bytes = sketch.serialize();
+    bytes[8..12].copy_from_slice(&100u32.to_le_bytes());
+    assert!(FrequentItemsSketch::<i64>::deserialize(&bytes).is_err());
 }
 
-// A non-empty header claiming a huge `active_items` that the remaining bytes
-// cannot possibly contain must be rejected before any capacity is reserved 
from
-// that count, so a corrupt header cannot drive a multi-GB `Vec` allocation.
 #[test]
 fn test_deserialize_rejects_num_active_exceeding_payload() {
-    // 700_000_000 <= capacity implied by lg_cur = 30, so it clears the 
capacity
-    // guard, but there are no item bytes for it, so the length guard rejects 
it.
-    let bytes = nonempty_header(30, 30, 700_000_000);
-    let result = FrequentItemsSketch::<i64>::deserialize(&bytes);
-    assert_that!(result, err(anything()));
+    let mut sketch = FrequentItemsSketch::<i64>::new(32);
+    sketch.update(1);
+    let mut bytes = sketch.serialize();
+    bytes[3] = 30;
+    bytes[4] = 30;
+    bytes[8..12].copy_from_slice(&700_000_000u32.to_le_bytes());
+    assert!(FrequentItemsSketch::<i64>::deserialize(&bytes).is_err());
 }
diff --git a/datasketches/tests/serde_tests/req.rs 
b/datasketches/tests/serde_tests/req.rs
index d9d8acf..a5e48fa 100644
--- a/datasketches/tests/serde_tests/req.rs
+++ b/datasketches/tests/serde_tests/req.rs
@@ -53,9 +53,9 @@ where
 
 #[test]
 fn round_trip_f64_matrix() {
-    for &k in &[4u16, 6, 10, 12, 1024] {
+    for &k in &[4u16, 12, 1024] {
         for &ra in &[RankAccuracy::HighRank, RankAccuracy::LowRank] {
-            for &n in &[0u64, 1, 4, 5, 100, 1_250, 2_562, 10_000, 100_000] {
+            for &n in &[0u64, 1, 4, 5, 100, 10_000] {
                 round_trip_one::<f64>(k, ra, n, |i| i as f64);
             }
         }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to