Skip to content

Commit 837bda2

Browse files
committed
Add Count-Min C++ serialization compatibility tests
1 parent 329913a commit 837bda2

3 files changed

Lines changed: 101 additions & 18 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
mod common;
19+
20+
use std::fs;
21+
22+
use common::serialization_test_data;
23+
use datasketches::countmin::CountMinSketch;
24+
use googletest::assert_that;
25+
use googletest::prelude::contains_substring;
26+
27+
// This test validates binary format compatibility (deserialize + byte round-trip) for
28+
// C++ Count-Min snapshots. It intentionally does not assert estimate equivalence against
29+
// original input keys because per-row hash seed derivation differs across implementations.
30+
fn assert_cpp_snapshot(
31+
filename: &str,
32+
seed: u64,
33+
expected_num_hashes: u8,
34+
expected_num_buckets: u32,
35+
expected_total_weight: u64,
36+
) {
37+
let path = serialization_test_data("cpp_generated_files", filename);
38+
let bytes = fs::read(&path).unwrap();
39+
40+
let sketch = CountMinSketch::<u64>::deserialize_with_seed(&bytes, seed).unwrap();
41+
42+
assert_eq!(sketch.num_hashes(), expected_num_hashes);
43+
assert_eq!(sketch.num_buckets(), expected_num_buckets);
44+
assert_eq!(sketch.seed(), seed);
45+
assert_eq!(sketch.total_weight(), expected_total_weight);
46+
assert_eq!(sketch.is_empty(), expected_total_weight == 0);
47+
48+
let roundtrip = sketch.serialize();
49+
assert_eq!(roundtrip, bytes, "round-trip bytes differ for {filename}");
50+
}
51+
52+
#[test]
53+
fn test_deserialize_cpp_empty_snapshot() {
54+
assert_cpp_snapshot("countmin_empty_cpp.sk", 9001, 1, 5, 0);
55+
}
56+
57+
#[test]
58+
fn test_deserialize_cpp_non_empty_snapshot() {
59+
assert_cpp_snapshot("countmin_non_empty_cpp.sk", 9001, 3, 1024, 2850);
60+
}
61+
62+
#[test]
63+
fn test_deserialize_cpp_snapshot_with_wrong_seed() {
64+
let path = serialization_test_data("cpp_generated_files", "countmin_non_empty_cpp.sk");
65+
let bytes = fs::read(&path).unwrap();
66+
67+
let err = CountMinSketch::<u64>::deserialize_with_seed(&bytes, 9000).unwrap_err();
68+
assert_that!(err.message(), contains_substring("incompatible seed hash"));
69+
}

datasketches/tests/countmin_test.rs

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
#![cfg(feature = "countmin")]
1919

2020
use datasketches::countmin::CountMinSketch;
21+
use googletest::assert_that;
22+
use googletest::prelude::ge;
23+
use googletest::prelude::le;
2124

2225
#[test]
2326
fn test_init_defaults() {
@@ -43,7 +46,7 @@ fn test_parameter_suggestions() {
4346

4447
let buckets = CountMinSketch::<i64>::suggest_num_buckets(0.1);
4548
let sketch = CountMinSketch::<i64>::new(3, buckets);
46-
assert!(sketch.relative_error() <= 0.1);
49+
assert_that!(sketch.relative_error(), le(0.1));
4750
}
4851

4952
#[test]
@@ -56,8 +59,8 @@ fn test_update_and_bounds() {
5659
let estimate = sketch.estimate("x");
5760
let upper = sketch.upper_bound("x");
5861
let lower = sketch.lower_bound("x");
59-
assert!(lower <= estimate);
60-
assert!(estimate <= upper);
62+
assert_that!(estimate, ge(lower));
63+
assert_that!(estimate, le(upper));
6164
}
6265

6366
#[test]
@@ -69,8 +72,8 @@ fn test_update_and_bounds_with_scaling() {
6972
let upper = sketch.upper_bound("x");
7073
let lower = sketch.lower_bound("x");
7174
assert_eq!(estimate, 10);
72-
assert!(lower <= estimate);
73-
assert!(estimate <= upper);
75+
assert_that!(estimate, ge(lower));
76+
assert_that!(estimate, le(upper));
7477

7578
let eps = sketch.relative_error();
7679

@@ -80,8 +83,8 @@ fn test_update_and_bounds_with_scaling() {
8083
let lower = sketch.lower_bound("x");
8184
assert_eq!(sketch.total_weight(), 5);
8285
assert_eq!(estimate, 5);
83-
assert!(lower <= estimate);
84-
assert!(estimate <= upper);
86+
assert_that!(estimate, ge(lower));
87+
assert_that!(estimate, le(upper));
8588
assert_eq!(
8689
upper,
8790
estimate + (eps * sketch.total_weight() as f64) as u64
@@ -93,8 +96,8 @@ fn test_update_and_bounds_with_scaling() {
9396
let lower = sketch.lower_bound("x");
9497
assert_eq!(sketch.total_weight(), 2);
9598
assert_eq!(estimate, 2);
96-
assert!(lower <= estimate);
97-
assert!(estimate <= upper);
99+
assert_that!(estimate, ge(lower));
100+
assert_that!(estimate, le(upper));
98101
assert_eq!(
99102
upper,
100103
estimate + (eps * sketch.total_weight() as f64) as u64
@@ -124,13 +127,13 @@ fn test_halve() {
124127
}
125128

126129
for i in 0..1000usize {
127-
assert!(sketch.estimate(i as u64) >= i as u64);
130+
assert_that!(sketch.estimate(i as u64), ge(i as u64));
128131
}
129132

130133
sketch.halve();
131134

132135
for i in 0..1000usize {
133-
assert!(sketch.estimate(i as u64) >= (i as u64) / 2);
136+
assert_that!(sketch.estimate(i as u64), ge((i as u64) / 2));
134137
}
135138
}
136139

@@ -147,15 +150,15 @@ fn test_decay() {
147150
}
148151

149152
for i in 0..1000usize {
150-
assert!(sketch.estimate(i as u64) >= i as u64);
153+
assert_that!(sketch.estimate(i as u64), ge(i as u64));
151154
}
152155

153156
const FACTOR: f64 = 0.5;
154157
sketch.decay(FACTOR);
155158

156159
for i in 0..1000usize {
157160
let expected = ((i as f64) * FACTOR).floor() as u64;
158-
assert!(sketch.estimate(i as u64) >= expected);
161+
assert_that!(sketch.estimate(i as u64), ge(expected));
159162
}
160163
}
161164

@@ -172,8 +175,8 @@ fn test_merge() {
172175
}
173176
left.merge(&right);
174177
assert_eq!(left.total_weight(), 18);
175-
assert!(left.estimate("a") >= 14);
176-
assert!(left.estimate("b") >= 4);
178+
assert_that!(left.estimate("a"), ge(14));
179+
assert_that!(left.estimate("b"), ge(4));
177180
}
178181

179182
#[test]
@@ -247,6 +250,6 @@ fn test_increment_multi_like_rust_count_min_sketch() {
247250
sketch.update(i % 100);
248251
}
249252
for key in 0..100u64 {
250-
assert!(sketch.estimate(key) >= 9_000);
253+
assert_that!(sketch.estimate(key), ge(9_000));
251254
}
252255
}

tools/generate_serialization_test_data.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def generate_cpp_files(workspace_dir, project_root):
134134
# 4. Clone repository
135135
repo_url = "https://github.com/apache/datasketches-cpp.git"
136136
branch = "master"
137+
commit = "0bab2596260510a40c1c953af1d6746c19d87dd1"
137138
run_command([
138139
"git", "clone",
139140
"--depth", "1",
@@ -142,6 +143,7 @@ def generate_cpp_files(workspace_dir, project_root):
142143
repo_url,
143144
str(temp_dir)
144145
])
146+
run_command(["git", "checkout", "--detach", commit], cwd=temp_dir)
145147

146148
# 5. Build and Run CMake
147149
build_dir = temp_dir / "build"
@@ -166,13 +168,22 @@ def generate_cpp_files(workspace_dir, project_root):
166168
output_dir.mkdir(parents=True, exist_ok=True)
167169

168170
files_copied = 0
169-
# Search recursively in build directory for *_cpp.sk
171+
172+
# Search recursively in build directory for standard C++ compatibility snapshots.
170173
for file_path in build_dir.rglob("*_cpp.sk"):
171-
# Avoid copying from CMakeFiles or other intermediate dirs if possible, but the pattern is specific enough
172174
shutil.copy2(file_path, output_dir)
173175
print(f"Copied: {file_path.name}")
174176
files_copied += 1
175177

178+
# Count-Min test binaries are produced as `count_min-*.bin`.
179+
# Normalize names to match the repository snapshot convention.
180+
for file_path in build_dir.rglob("count_min-*.bin"):
181+
base_name = file_path.stem[len("count_min-"):].replace("-", "_")
182+
output_name = f"countmin_{base_name}_cpp.sk"
183+
shutil.copy2(file_path, output_dir / output_name)
184+
print(f"Copied: {output_name} (from {file_path.name})")
185+
files_copied += 1
186+
176187
if files_copied == 0:
177188
print("Warning: No *_cpp.sk files were found to copy.")
178189
else:

0 commit comments

Comments
 (0)