Skip to content

Commit 352bd8d

Browse files
authored
Merge pull request #1532 from manmathbh/add-hash-tests
Add unit test file for hash package
1 parent 14b6c6c commit 352bd8d

File tree

2 files changed

+325
-4
lines changed

2 files changed

+325
-4
lines changed

pkg/utils/hash/hash_test.go

Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
/*
2+
* Copyright The Kmesh Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at:
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package hash
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestHash128(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
data []byte
27+
seed uint32
28+
expectedH1 uint64
29+
expectedH2 uint64
30+
}{
31+
{
32+
name: "empty input with seed 0",
33+
data: []byte{},
34+
seed: 0,
35+
expectedH1: 0,
36+
expectedH2: 0,
37+
},
38+
{
39+
name: "empty input with seed 42",
40+
data: []byte{},
41+
seed: 42,
42+
expectedH1: 17305828677633410339,
43+
expectedH2: 15060430851467758521,
44+
},
45+
{
46+
name: "single byte",
47+
data: []byte{0x42},
48+
seed: 0,
49+
expectedH1: 13609119272785792230,
50+
expectedH2: 16499902383673028068,
51+
},
52+
{
53+
name: "hello world with seed 0",
54+
data: []byte("hello world"),
55+
seed: 0,
56+
expectedH1: 5998619086395760910,
57+
expectedH2: 12364428806279881649,
58+
},
59+
{
60+
name: "hello world with seed 123",
61+
data: []byte("hello world"),
62+
seed: 123,
63+
expectedH1: 3184134337056710880,
64+
expectedH2: 6735459307601781589,
65+
},
66+
{
67+
name: "exactly 16 bytes",
68+
data: []byte("0123456789abcdef"),
69+
seed: 0,
70+
expectedH1: 5467490433528156583,
71+
expectedH2: 9782763267945859290,
72+
},
73+
{
74+
name: "more than 16 bytes",
75+
data: []byte("0123456789abcdef0123456789"),
76+
seed: 0,
77+
expectedH1: 9554952042823857868,
78+
expectedH2: 7769558039678505135,
79+
},
80+
{
81+
name: "32 bytes - exactly 2 blocks",
82+
data: []byte("0123456789abcdef0123456789abcdef"),
83+
seed: 0,
84+
expectedH1: 5708918040068455610,
85+
expectedH2: 1203913688419142818,
86+
},
87+
{
88+
name: "all tail cases - 15 bytes",
89+
data: []byte("012345678901234"),
90+
seed: 0,
91+
expectedH1: 11867552070264469923,
92+
expectedH2: 3266839343213454983,
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
h1, h2 := Hash128(tt.data, tt.seed)
99+
if h1 != tt.expectedH1 || h2 != tt.expectedH2 {
100+
t.Errorf("Hash128(%q, %d) = (%d, %d), want (%d, %d)",
101+
tt.data, tt.seed, h1, h2, tt.expectedH1, tt.expectedH2)
102+
}
103+
})
104+
}
105+
}
106+
107+
func TestHash128_Deterministic(t *testing.T) {
108+
testData := []byte("test data for deterministic check")
109+
seed := uint32(42)
110+
111+
h1First, h2First := Hash128(testData, seed)
112+
h1Second, h2Second := Hash128(testData, seed)
113+
114+
if h1First != h1Second || h2First != h2Second {
115+
t.Errorf("Hash128 is not deterministic: first call (%d, %d), second call (%d, %d)",
116+
h1First, h2First, h1Second, h2Second)
117+
}
118+
}
119+
120+
func TestHash128_DifferentInputs(t *testing.T) {
121+
data1 := []byte("test data 1")
122+
data2 := []byte("test data 2")
123+
seed := uint32(0)
124+
125+
h1Data1, h2Data1 := Hash128(data1, seed)
126+
h1Data2, h2Data2 := Hash128(data2, seed)
127+
128+
if h1Data1 == h1Data2 && h2Data1 == h2Data2 {
129+
t.Errorf("Different inputs produced same hash: (%d, %d)", h1Data1, h2Data1)
130+
}
131+
}
132+
133+
func TestHash128_AllTailLengths(t *testing.T) {
134+
// Test all tail lengths from 0 to 15 bytes
135+
seed := uint32(0)
136+
baseData := []byte("0123456789abcdef") // 16 bytes base
137+
138+
for tailLen := 0; tailLen <= 15; tailLen++ {
139+
data := append(baseData, []byte("extra tail bytes")[:tailLen]...)
140+
h1, h2 := Hash128(data, seed)
141+
142+
// This check is a sanity check to catch a hash function that is broken
143+
// (e.g., always returning zero). It is not a strict correctness check,
144+
// as a valid hash function could theoretically return (0, 0) for some input.
145+
if h1 == 0 && h2 == 0 && len(data) > 0 {
146+
t.Errorf("Hash128 with tail length %d produced zero hash for non-empty input", tailLen)
147+
}
148+
}
149+
}
150+
151+
func Test_rotl64(t *testing.T) {
152+
tests := []struct {
153+
name string
154+
x uint64
155+
r int8
156+
expected uint64
157+
}{
158+
{
159+
name: "rotate 0 positions",
160+
x: 0x0123456789abcdef,
161+
r: 0,
162+
expected: 0x0123456789abcdef,
163+
},
164+
{
165+
name: "rotate 1 position",
166+
x: 0x0123456789abcdef,
167+
r: 1,
168+
expected: 0x02468acf13579bde,
169+
},
170+
{
171+
name: "rotate 8 positions",
172+
x: 0x0123456789abcdef,
173+
r: 8,
174+
expected: 0x23456789abcdef01,
175+
},
176+
{
177+
name: "rotate 32 positions",
178+
x: 0x0123456789abcdef,
179+
r: 32,
180+
expected: 0x89abcdef01234567,
181+
},
182+
{
183+
name: "rotate 64 positions (full circle)",
184+
x: 0x0123456789abcdef,
185+
r: 64,
186+
expected: 0x0123456789abcdef,
187+
},
188+
{
189+
name: "rotate all 1s",
190+
x: 0xffffffffffffffff,
191+
r: 13,
192+
expected: 0xffffffffffffffff,
193+
},
194+
}
195+
196+
for _, tt := range tests {
197+
t.Run(tt.name, func(t *testing.T) {
198+
result := rotl64(tt.x, tt.r)
199+
if result != tt.expected {
200+
t.Errorf("rotl64(0x%x, %d) = 0x%x, want 0x%x",
201+
tt.x, tt.r, result, tt.expected)
202+
}
203+
})
204+
}
205+
}
206+
207+
func Test_fmix64(t *testing.T) {
208+
tests := []struct {
209+
name string
210+
input uint64
211+
expected uint64
212+
}{
213+
{
214+
name: "zero input",
215+
input: 0,
216+
expected: 0,
217+
},
218+
{
219+
name: "all ones",
220+
input: 0xffffffffffffffff,
221+
expected: 0x64b5720b4b825f21,
222+
},
223+
{
224+
name: "test value 1",
225+
input: 0x0123456789abcdef,
226+
expected: 0x87cbfbfe89022cea,
227+
},
228+
{
229+
name: "test value 2",
230+
input: 0xfedcba9876543210,
231+
expected: 0x03ebebcc1f4a6fd7,
232+
},
233+
}
234+
235+
for _, tt := range tests {
236+
t.Run(tt.name, func(t *testing.T) {
237+
result := fmix64(tt.input)
238+
if result != tt.expected {
239+
t.Errorf("fmix64(0x%x) = 0x%x, want 0x%x",
240+
tt.input, result, tt.expected)
241+
}
242+
})
243+
}
244+
}
245+
246+
func Test_fmix64_Avalanche(t *testing.T) {
247+
// Test that changing a single bit in input changes many bits in output
248+
input1 := uint64(0x0123456789abcdef)
249+
input2 := uint64(0x0123456789abcdee) // flip last bit
250+
251+
output1 := fmix64(input1)
252+
output2 := fmix64(input2)
253+
254+
if output1 == output2 {
255+
t.Errorf("fmix64 failed avalanche test: same output for different inputs")
256+
}
257+
258+
// Count changed bits
259+
xor := output1 ^ output2
260+
changedBits := 0
261+
for xor != 0 {
262+
changedBits++
263+
xor &= xor - 1
264+
}
265+
266+
// Expect significant bit change (at least 20 out of 64 bits)
267+
if changedBits < 20 {
268+
t.Errorf("fmix64 avalanche effect too weak: only %d bits changed", changedBits)
269+
}
270+
}
271+
272+
func BenchmarkHash128_Small(b *testing.B) {
273+
data := []byte("hello")
274+
seed := uint32(0)
275+
b.ResetTimer()
276+
for i := 0; i < b.N; i++ {
277+
Hash128(data, seed)
278+
}
279+
}
280+
281+
func BenchmarkHash128_Medium(b *testing.B) {
282+
data := make([]byte, 128)
283+
for i := range data {
284+
data[i] = byte(i)
285+
}
286+
seed := uint32(0)
287+
b.ResetTimer()
288+
for i := 0; i < b.N; i++ {
289+
Hash128(data, seed)
290+
}
291+
}
292+
293+
func BenchmarkHash128_Large(b *testing.B) {
294+
data := make([]byte, 1024)
295+
for i := range data {
296+
data[i] = byte(i)
297+
}
298+
seed := uint32(0)
299+
b.ResetTimer()
300+
for i := 0; i < b.N; i++ {
301+
Hash128(data, seed)
302+
}
303+
}
304+
305+
func TestHash128_UnalignedInput(t *testing.T) {
306+
// Create a buffer and then a sub-slice that is not 8-byte aligned.
307+
buf := make([]byte, 33)
308+
for i := range buf {
309+
buf[i] = byte(i)
310+
}
311+
unalignedData := buf[1:] // len=32
312+
313+
defer func() {
314+
if r := recover(); r != nil {
315+
t.Errorf("Hash128 panicked on unaligned data: %v", r)
316+
}
317+
}()
318+
319+
Hash128(unalignedData, 0)
320+
}

pkg/utils/hash/murmur3.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
package hash
1818

1919
import (
20-
"unsafe"
20+
"encoding/binary"
2121
)
2222

2323
// Hash128 calculates a 128 bits hash for the given data. It returns different
@@ -37,9 +37,10 @@ func Hash128(data []byte, seed uint32) (uint64, uint64) {
3737
h2 := uint64(seed)
3838

3939
for i := 0; i < nblocks; i++ {
40-
tmp := (*[2]uint64)(unsafe.Pointer(&data[i*16]))
41-
k1 := tmp[0]
42-
k2 := tmp[1]
40+
off := i * 16
41+
// Safe unaligned reads using encoding/binary
42+
k1 := binary.LittleEndian.Uint64(data[off : off+8])
43+
k2 := binary.LittleEndian.Uint64(data[off+8 : off+16])
4344

4445
k1 *= c1
4546
k1 = rotl64(k1, 31)

0 commit comments

Comments
 (0)