Skip to content

Commit 4d2e7e6

Browse files
mrecachinasCopilot
andcommitted
test: add per-module unit tests
- hex.rs: nibble parsing for digits, upper, lower, invalid, boundary chars - classic.rs: popcnt64, string/bytes classic with various sizes and max_dist - native.rs: popcnt64 vs count_ones, agreement with classic across sizes - neon_simd.rs: NEON string 16/64 chars, pack path, invalid chars, vs classic - api.rs: set_algorithm, bytes_within_dist, array first/best/all, error cases 40 unit tests + 2 doc tests, up from 8 + 2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 1240c8d commit 4d2e7e6

5 files changed

Lines changed: 304 additions & 0 deletions

File tree

src/api.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,70 @@ pub fn set_algorithm(algo_name: &str) -> Result<(), &'static str> {
228228
_ => Err("unknown algorithm"),
229229
}
230230
}
231+
232+
#[cfg(test)]
233+
mod tests {
234+
use super::*;
235+
236+
#[test]
237+
fn set_algorithm_classic_and_native() {
238+
set_algorithm("classic").unwrap();
239+
assert_eq!(hex_hamming_distance("deadbeef", "00000000").unwrap(), 24);
240+
set_algorithm("native").unwrap();
241+
assert_eq!(hex_hamming_distance("deadbeef", "00000000").unwrap(), 24);
242+
}
243+
244+
#[test]
245+
fn set_algorithm_unknown() {
246+
assert!(set_algorithm("bogus").is_err());
247+
}
248+
249+
#[test]
250+
fn bytes_within_dist_basic() {
251+
assert_eq!(bytes_within_dist(b"\xff", b"\xfe", 2).unwrap(), true);
252+
assert_eq!(bytes_within_dist(b"\xff", b"\x00", 2).unwrap(), false);
253+
}
254+
255+
#[test]
256+
fn bytes_within_dist_errors() {
257+
assert!(bytes_within_dist(b"", b"\xff", 1).is_err());
258+
assert!(bytes_within_dist(b"\xff", b"\xff\x00", 1).is_err());
259+
}
260+
261+
#[test]
262+
fn array_first_within_dist_test() {
263+
let big = b"\xaa\xbb\xcc\xff";
264+
let small = b"\xff";
265+
// \xaa vs \xff = dist 4, within max_dist 4
266+
assert_eq!(bytes_array_first_within_dist(big, small, 4).unwrap(), Some(0));
267+
// Only exact match at index 3
268+
assert_eq!(bytes_array_first_within_dist(big, small, 0).unwrap(), Some(3));
269+
// dist(\x00, \xff) = 8, exceeds max_dist 1
270+
assert_eq!(bytes_array_first_within_dist(b"\x00", b"\xff", 1).unwrap(), None);
271+
}
272+
273+
#[test]
274+
fn array_best_within_dist() {
275+
// \xfe is distance 1 from \xff, \xaa is distance 4
276+
let big = b"\xaa\xfe\xff";
277+
let small = b"\xff";
278+
let result = bytes_array_best_within_dist(big, small, 8).unwrap();
279+
assert_eq!(result, Some((0, 2))); // exact match at index 2
280+
}
281+
282+
#[test]
283+
fn array_all_within_dist() {
284+
let big = b"\xaa\xfe\xff";
285+
let small = b"\xff";
286+
let result = bytes_array_all_within_dist(big, small, 8).unwrap();
287+
assert_eq!(result.len(), 3);
288+
// Last entry should be exact match
289+
assert_eq!(result[2], (0, 2));
290+
}
291+
292+
#[test]
293+
fn array_errors() {
294+
assert!(bytes_array_first_within_dist(b"\xff", b"", 1).is_err()); // empty small
295+
assert!(bytes_array_first_within_dist(b"\xaa\xbb\xcc", b"\xff\xff", 1).is_err()); // not a multiple
296+
}
297+
}

src/classic.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,68 @@ pub(crate) fn hamming_distance_bytes_classic(a: &[u8], b: &[u8], max_dist: i64)
146146
1
147147
}
148148
}
149+
150+
#[cfg(test)]
151+
mod tests {
152+
use super::*;
153+
154+
#[test]
155+
fn popcnt64_known_values() {
156+
assert_eq!(popcnt64_classic(0), 0);
157+
assert_eq!(popcnt64_classic(1), 1);
158+
assert_eq!(popcnt64_classic(0xFF), 8);
159+
assert_eq!(popcnt64_classic(0xFFFFFFFFFFFFFFFF), 64);
160+
assert_eq!(popcnt64_classic(0xAAAAAAAAAAAAAAAA), 32);
161+
assert_eq!(popcnt64_classic(0xDEADBEEF), 24);
162+
}
163+
164+
#[test]
165+
fn string_classic_basic() {
166+
assert_eq!(hamming_distance_string_classic(b"ff", b"00").unwrap(), 8);
167+
assert_eq!(hamming_distance_string_classic(b"deadbeef", b"00000000").unwrap(), 24);
168+
assert_eq!(hamming_distance_string_classic(b"0000", b"0000").unwrap(), 0);
169+
}
170+
171+
#[test]
172+
fn string_classic_invalid() {
173+
assert!(hamming_distance_string_classic(b"zz", b"00").is_err());
174+
assert!(hamming_distance_string_classic(b"gg", b"00").is_err());
175+
}
176+
177+
#[test]
178+
fn string_classic_odd_length() {
179+
assert_eq!(hamming_distance_string_classic(b"f", b"0").unwrap(), 4);
180+
assert_eq!(hamming_distance_string_classic(b"fff", b"000").unwrap(), 12);
181+
assert_eq!(hamming_distance_string_classic(b"fffff", b"00000").unwrap(), 20);
182+
}
183+
184+
#[test]
185+
fn bytes_classic_full_distance() {
186+
assert_eq!(hamming_distance_bytes_classic(b"\xff", b"\x00", -1), 8);
187+
assert_eq!(hamming_distance_bytes_classic(b"\x00\x00", b"\x00\x00", -1), 0);
188+
// 64 bytes to exercise 32-byte unrolled loop
189+
let a = vec![0xFFu8; 64];
190+
let b = vec![0x00u8; 64];
191+
assert_eq!(hamming_distance_bytes_classic(&a, &b, -1), 512);
192+
}
193+
194+
#[test]
195+
fn bytes_classic_with_max_dist() {
196+
// Within threshold → returns 1
197+
assert_eq!(hamming_distance_bytes_classic(b"\xff", b"\xfe", 2), 1);
198+
// Exceeds threshold → returns 0
199+
assert_eq!(hamming_distance_bytes_classic(b"\xff", b"\x00", 2), 0);
200+
}
201+
202+
#[test]
203+
fn bytes_classic_tail_bytes() {
204+
// 9 bytes: exercises 8-byte chunk + 1 tail byte
205+
let a = vec![0xFF; 9];
206+
let b = vec![0x00; 9];
207+
assert_eq!(hamming_distance_bytes_classic(&a, &b, -1), 72);
208+
// 7 bytes: no 8-byte chunks, all tail
209+
let a = vec![0xFF; 7];
210+
let b = vec![0x00; 7];
211+
assert_eq!(hamming_distance_bytes_classic(&a, &b, -1), 56);
212+
}
213+
}

src/hex.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,53 @@ pub(crate) fn hex_char_to_val(c: u8) -> Option<u8> {
1616
let val = hex_char_to_nibble(c);
1717
if val == 0xFF { None } else { Some(val) }
1818
}
19+
20+
#[cfg(test)]
21+
mod tests {
22+
use super::*;
23+
24+
#[test]
25+
fn nibble_digits() {
26+
for (i, c) in b"0123456789".iter().enumerate() {
27+
assert_eq!(hex_char_to_nibble(*c), i as u8);
28+
}
29+
}
30+
31+
#[test]
32+
fn nibble_upper() {
33+
for (i, c) in b"ABCDEF".iter().enumerate() {
34+
assert_eq!(hex_char_to_nibble(*c), 10 + i as u8);
35+
}
36+
}
37+
38+
#[test]
39+
fn nibble_lower() {
40+
for (i, c) in b"abcdef".iter().enumerate() {
41+
assert_eq!(hex_char_to_nibble(*c), 10 + i as u8);
42+
}
43+
}
44+
45+
#[test]
46+
fn nibble_invalid() {
47+
assert_eq!(hex_char_to_nibble(b'g'), 0xFF);
48+
assert_eq!(hex_char_to_nibble(b'z'), 0xFF);
49+
assert_eq!(hex_char_to_nibble(b'@'), 0xFF);
50+
assert_eq!(hex_char_to_nibble(b' '), 0xFF);
51+
assert_eq!(hex_char_to_nibble(b'/'), 0xFF);
52+
assert_eq!(hex_char_to_nibble(b':'), 0xFF); // just past '9'
53+
assert_eq!(hex_char_to_nibble(b'G'), 0xFF); // just past 'F'
54+
}
55+
56+
#[test]
57+
fn hex_val_valid() {
58+
assert_eq!(hex_char_to_val(b'0'), Some(0));
59+
assert_eq!(hex_char_to_val(b'f'), Some(15));
60+
assert_eq!(hex_char_to_val(b'A'), Some(10));
61+
}
62+
63+
#[test]
64+
fn hex_val_invalid() {
65+
assert_eq!(hex_char_to_val(b'z'), None);
66+
assert_eq!(hex_char_to_val(b' '), None);
67+
}
68+
}

src/native.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,47 @@ pub(crate) fn hamming_distance_bytes_native(a: &[u8], b: &[u8], max_dist: i64) -
8080
1
8181
}
8282
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
88+
#[test]
89+
fn popcnt64_matches_count_ones() {
90+
// Verify native matches std count_ones for various values
91+
for x in [0u64, 1, 0xFF, 0xDEADBEEF, 0xFFFFFFFFFFFFFFFF, 0x123456789ABCDEF0] {
92+
assert_eq!(popcnt64_native(x), x.count_ones() as u64);
93+
}
94+
}
95+
96+
#[test]
97+
fn bytes_native_full_distance() {
98+
assert_eq!(hamming_distance_bytes_native(b"\xff", b"\x00", -1), 8);
99+
assert_eq!(hamming_distance_bytes_native(b"\x00\x00", b"\x00\x00", -1), 0);
100+
// 64 bytes to exercise 32-byte unrolled loop
101+
let a = vec![0xFFu8; 64];
102+
let b = vec![0x00u8; 64];
103+
assert_eq!(hamming_distance_bytes_native(&a, &b, -1), 512);
104+
}
105+
106+
#[test]
107+
fn bytes_native_with_max_dist() {
108+
assert_eq!(hamming_distance_bytes_native(b"\xff", b"\xfe", 2), 1);
109+
assert_eq!(hamming_distance_bytes_native(b"\xff", b"\x00", 2), 0);
110+
}
111+
112+
#[test]
113+
fn bytes_native_agrees_with_classic() {
114+
use crate::classic::hamming_distance_bytes_classic;
115+
// Test various sizes to cover all loop paths
116+
for size in [1, 7, 8, 9, 15, 16, 31, 32, 33, 63, 64, 127] {
117+
let a: Vec<u8> = (0..size).map(|i| i as u8).collect();
118+
let b: Vec<u8> = (0..size).map(|i| (i as u8).wrapping_add(1)).collect();
119+
assert_eq!(
120+
hamming_distance_bytes_native(&a, &b, -1),
121+
hamming_distance_bytes_classic(&a, &b, -1),
122+
"mismatch at size {size}"
123+
);
124+
}
125+
}
126+
}

src/neon_simd.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,81 @@ pub unsafe fn hamming_distance_string_neon_pack(a: &[u8], b: &[u8]) -> Result<u6
237237

238238
Ok(difference)
239239
}
240+
241+
#[cfg(test)]
242+
mod tests {
243+
use super::*;
244+
245+
#[test]
246+
fn neon_string_basic() {
247+
unsafe {
248+
assert_eq!(hamming_distance_string_neon(b"deadbeef", b"00000000").unwrap(), 24);
249+
assert_eq!(hamming_distance_string_neon(b"ffff", b"0000").unwrap(), 16);
250+
assert_eq!(hamming_distance_string_neon(b"0000", b"0000").unwrap(), 0);
251+
}
252+
}
253+
254+
#[test]
255+
fn neon_string_16_chars() {
256+
// Exactly 16 chars — one NEON iteration, no tail
257+
unsafe {
258+
let a = "f".repeat(16);
259+
let b = "0".repeat(16);
260+
assert_eq!(hamming_distance_string_neon(a.as_bytes(), b.as_bytes()).unwrap(), 64);
261+
}
262+
}
263+
264+
#[test]
265+
fn neon_string_64_chars() {
266+
// 64 chars — exercises the batched 4×16 loop
267+
unsafe {
268+
let a = "f".repeat(64);
269+
let b = "0".repeat(64);
270+
assert_eq!(hamming_distance_string_neon(a.as_bytes(), b.as_bytes()).unwrap(), 256);
271+
}
272+
}
273+
274+
#[test]
275+
fn neon_string_invalid() {
276+
unsafe {
277+
assert!(hamming_distance_string_neon(b"zzzzzzzzzzzzzzzz", b"0000000000000000").is_err());
278+
assert!(hamming_distance_string_neon(b"@@@@@@@@@@@@@@@@", b"0000000000000000").is_err());
279+
}
280+
}
281+
282+
#[test]
283+
fn neon_pack_basic() {
284+
unsafe {
285+
let a = "f".repeat(32);
286+
let b = "0".repeat(32);
287+
assert_eq!(hamming_distance_string_neon_pack(a.as_bytes(), b.as_bytes()).unwrap(), 128);
288+
}
289+
}
290+
291+
#[test]
292+
fn neon_pack_with_tail() {
293+
// 48 chars: 32-char pack loop + 16-char NEON tail
294+
unsafe {
295+
let a = "f".repeat(48);
296+
let b = "0".repeat(48);
297+
assert_eq!(hamming_distance_string_neon_pack(a.as_bytes(), b.as_bytes()).unwrap(), 192);
298+
}
299+
}
300+
301+
#[test]
302+
fn neon_agrees_with_classic() {
303+
use crate::classic::hamming_distance_string_classic;
304+
let a = "0123456789abcdef".repeat(8); // 128 chars
305+
let b = "fedcba9876543210".repeat(8);
306+
unsafe {
307+
assert_eq!(
308+
hamming_distance_string_neon(a.as_bytes(), b.as_bytes()).unwrap(),
309+
hamming_distance_string_classic(a.as_bytes(), b.as_bytes()).unwrap()
310+
);
311+
assert_eq!(
312+
hamming_distance_string_neon_pack(a.as_bytes(), b.as_bytes()).unwrap(),
313+
hamming_distance_string_classic(a.as_bytes(), b.as_bytes()).unwrap()
314+
);
315+
}
316+
}
317+
}

0 commit comments

Comments
 (0)