|
| 1 | +%% This Source Code Form is subject to the terms of the Mozilla Public |
| 2 | +%% License, v. 2.0. If a copy of the MPL was not distributed with this |
| 3 | +%% file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 4 | +%% |
| 5 | +%% Copyright (c) 2007-2025 Broadcom. All Rights Reserved. The term "Broadcom" refers to Broadcom Inc. and/or its subsidiaries. All rights reserved. |
| 6 | +%% |
| 7 | + |
| 8 | +-module(rabbit_murmur3_SUITE). |
| 9 | + |
| 10 | +-include_lib("eunit/include/eunit.hrl"). |
| 11 | + |
| 12 | +-compile(export_all). |
| 13 | + |
| 14 | +all() -> |
| 15 | + [ |
| 16 | + {group, parallel_tests} |
| 17 | + ]. |
| 18 | + |
| 19 | +groups() -> |
| 20 | + [ |
| 21 | + {parallel_tests, [parallel], [ |
| 22 | + hash_empty_string, |
| 23 | + hash_with_seed_zero, |
| 24 | + hash_with_custom_seed, |
| 25 | + hash_known_test_vectors, |
| 26 | + hash_binary_data, |
| 27 | + hash_iolist, |
| 28 | + hash_consistency, |
| 29 | + hash_distribution |
| 30 | + ]} |
| 31 | + ]. |
| 32 | + |
| 33 | +%% ------------------------------------------------------------------- |
| 34 | +%% Test Cases |
| 35 | +%% ------------------------------------------------------------------- |
| 36 | + |
| 37 | +%% Test empty string hashing |
| 38 | +hash_empty_string(_Config) -> |
| 39 | + %% Empty string with seed 0 should produce 0 |
| 40 | + %% Reference: with zero data and zero seed, everything becomes zero |
| 41 | + ?assertEqual(0, rabbit_murmur3:hash_32(<<>>)), |
| 42 | + ?assertEqual(0, rabbit_murmur3:hash_32(<<>>, 0)), |
| 43 | + %% Empty string with seed 1 - ignores nearly all the math |
| 44 | + ?assertEqual(16#514E28B7, rabbit_murmur3:hash_32(<<>>, 1)), |
| 45 | + %% Empty string with seed 0xffffffff - make sure seed uses unsigned 32-bit math |
| 46 | + ?assertEqual(16#81F16F39, rabbit_murmur3:hash_32(<<>>, 16#ffffffff)), |
| 47 | + passed. |
| 48 | + |
| 49 | +%% Test hashing with seed 0 |
| 50 | +hash_with_seed_zero(_Config) -> |
| 51 | + %% Known test vectors from reference implementation with seed 0 |
| 52 | + %% Reference: https://gist.github.com/vladimirgamalyan/defb2482feefbf5c3ea25b14c557753b |
| 53 | + ?assertEqual(16#B3DD93FA, rabbit_murmur3:hash_32(<<"abc">>)), |
| 54 | + ?assertEqual(16#EE925B90, rabbit_murmur3:hash_32(<<"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq">>)), |
| 55 | + passed. |
| 56 | + |
| 57 | +%% Test hashing with custom seeds |
| 58 | +hash_with_custom_seed(_Config) -> |
| 59 | + %% Test with seed 0x9747b28c (common test seed) |
| 60 | + Seed = 16#9747b28c, |
| 61 | + |
| 62 | + %% Reference test vectors with seed 0x9747b28c |
| 63 | + ?assertEqual(16#5A97808A, rabbit_murmur3:hash_32(<<"aaaa">>, Seed)), |
| 64 | + ?assertEqual(16#283E0130, rabbit_murmur3:hash_32(<<"aaa">>, Seed)), |
| 65 | + ?assertEqual(16#5D211726, rabbit_murmur3:hash_32(<<"aa">>, Seed)), |
| 66 | + ?assertEqual(16#7FA09EA6, rabbit_murmur3:hash_32(<<"a">>, Seed)), |
| 67 | + |
| 68 | + %% Endian order within chunks |
| 69 | + ?assertEqual(16#F0478627, rabbit_murmur3:hash_32(<<"abcd">>, Seed)), |
| 70 | + ?assertEqual(16#C84A62DD, rabbit_murmur3:hash_32(<<"abc">>, Seed)), |
| 71 | + ?assertEqual(16#74875592, rabbit_murmur3:hash_32(<<"ab">>, Seed)), |
| 72 | + |
| 73 | + ?assertEqual(16#24884CBA, rabbit_murmur3:hash_32(<<"Hello, world!">>, Seed)), |
| 74 | + ?assertEqual(16#2FA826CD, rabbit_murmur3:hash_32(<<"The quick brown fox jumps over the lazy dog">>, Seed)), |
| 75 | + |
| 76 | + %% Different seeds should produce different hashes |
| 77 | + Hash1 = rabbit_murmur3:hash_32(<<"test">>, Seed), |
| 78 | + Hash2 = rabbit_murmur3:hash_32(<<"test">>, 0), |
| 79 | + Hash3 = rabbit_murmur3:hash_32(<<"test">>, 42), |
| 80 | + ?assertNotEqual(Hash1, Hash2), |
| 81 | + ?assertNotEqual(Hash1, Hash3), |
| 82 | + ?assertNotEqual(Hash2, Hash3), |
| 83 | + passed. |
| 84 | + |
| 85 | +%% Test known test vectors from reference implementation |
| 86 | +hash_known_test_vectors(_Config) -> |
| 87 | + %% Test vectors verified against reference C implementation |
| 88 | + %% Reference: https://gist.github.com/vladimirgamalyan/defb2482feefbf5c3ea25b14c557753b |
| 89 | + |
| 90 | + %% Binary array tests with seed 0 |
| 91 | + %% Make sure 4-byte chunks use unsigned math |
| 92 | + ?assertEqual(16#76293B50, rabbit_murmur3:hash_32(<<16#ff, 16#ff, 16#ff, 16#ff>>)), |
| 93 | + |
| 94 | + %% Endian order test: bytes 0x21, 0x43, 0x65, 0x87 should be read as little-endian |
| 95 | + ?assertEqual(16#F55B516B, rabbit_murmur3:hash_32(<<16#21, 16#43, 16#65, 16#87>>)), |
| 96 | + |
| 97 | + %% Special seed value test |
| 98 | + ?assertEqual(16#2362F9DE, rabbit_murmur3:hash_32(<<16#21, 16#43, 16#65, 16#87>>, 16#5082EDEE)), |
| 99 | + |
| 100 | + %% Tail length tests (3, 2, 1 bytes) |
| 101 | + ?assertEqual(16#7E4A8634, rabbit_murmur3:hash_32(<<16#21, 16#43, 16#65>>)), |
| 102 | + ?assertEqual(16#A0F7B07A, rabbit_murmur3:hash_32(<<16#21, 16#43>>)), |
| 103 | + ?assertEqual(16#72661CF4, rabbit_murmur3:hash_32(<<16#21>>)), |
| 104 | + |
| 105 | + %% Zero bytes tests |
| 106 | + ?assertEqual(16#2362F9DE, rabbit_murmur3:hash_32(<<0, 0, 0, 0>>)), |
| 107 | + ?assertEqual(16#85F0B427, rabbit_murmur3:hash_32(<<0, 0, 0>>)), |
| 108 | + ?assertEqual(16#30F4C306, rabbit_murmur3:hash_32(<<0, 0>>)), |
| 109 | + ?assertEqual(16#514E28B7, rabbit_murmur3:hash_32(<<0>>)), |
| 110 | + passed. |
| 111 | + |
| 112 | +%% Test binary data hashing |
| 113 | +hash_binary_data(_Config) -> |
| 114 | + %% Test with raw binary data |
| 115 | + Bin = <<0, 1, 2, 3, 4, 5, 6, 7, 8, 9>>, |
| 116 | + Hash = rabbit_murmur3:hash_32(Bin), |
| 117 | + ?assert(is_integer(Hash)), |
| 118 | + ?assert(Hash >= 0), |
| 119 | + ?assert(Hash =< 16#ffffffff), |
| 120 | + |
| 121 | + %% Test with binary containing high bytes |
| 122 | + Bin2 = <<255, 254, 253, 252, 251>>, |
| 123 | + Hash2 = rabbit_murmur3:hash_32(Bin2), |
| 124 | + ?assert(is_integer(Hash2)), |
| 125 | + passed. |
| 126 | + |
| 127 | +%% Test iolist input |
| 128 | +hash_iolist(_Config) -> |
| 129 | + %% iolist should produce same result as equivalent binary |
| 130 | + Binary = <<"hello world">>, |
| 131 | + IoList = [<<"hello">>, <<" ">>, <<"world">>], |
| 132 | + ?assertEqual(rabbit_murmur3:hash_32(Binary), rabbit_murmur3:hash_32(IoList)), |
| 133 | + |
| 134 | + %% Nested iolist |
| 135 | + NestedIoList = [[<<"hel">>, <<"lo">>], [<<" ">>, [<<"wor">>, <<"ld">>]]], |
| 136 | + ?assertEqual(rabbit_murmur3:hash_32(Binary), rabbit_murmur3:hash_32(NestedIoList)), |
| 137 | + passed. |
| 138 | + |
| 139 | +%% Test hash consistency |
| 140 | +hash_consistency(_Config) -> |
| 141 | + %% Same input should always produce same output |
| 142 | + Input = <<"consistent input">>, |
| 143 | + Hash1 = rabbit_murmur3:hash_32(Input), |
| 144 | + Hash2 = rabbit_murmur3:hash_32(Input), |
| 145 | + Hash3 = rabbit_murmur3:hash_32(Input), |
| 146 | + ?assertEqual(Hash1, Hash2), |
| 147 | + ?assertEqual(Hash2, Hash3), |
| 148 | + |
| 149 | + %% Same input with same seed |
| 150 | + Hash4 = rabbit_murmur3:hash_32(Input, 12345), |
| 151 | + Hash5 = rabbit_murmur3:hash_32(Input, 12345), |
| 152 | + ?assertEqual(Hash4, Hash5), |
| 153 | + passed. |
| 154 | + |
| 155 | +%% Test hash distribution (basic avalanche property) |
| 156 | +hash_distribution(_Config) -> |
| 157 | + %% Generate hashes for sequential integers and verify reasonable distribution |
| 158 | + Hashes = [rabbit_murmur3:hash_32(integer_to_binary(N)) || N <- lists:seq(1, 1000)], |
| 159 | + UniqueHashes = length(lists:usort(Hashes)), |
| 160 | + |
| 161 | + %% All hashes should be unique for this small set |
| 162 | + ?assertEqual(1000, UniqueHashes), |
| 163 | + |
| 164 | + %% Check that hashes are well distributed across the 32-bit range |
| 165 | + %% by verifying we have hashes in different "buckets" |
| 166 | + Buckets = lists:foldl( |
| 167 | + fun(H, Acc) -> |
| 168 | + Bucket = H div (16#ffffffff div 16), |
| 169 | + maps:update_with(Bucket, fun(V) -> V + 1 end, 1, Acc) |
| 170 | + end, |
| 171 | + #{}, |
| 172 | + Hashes |
| 173 | + ), |
| 174 | + %% We should have hashes in at least 10 of the 16 buckets |
| 175 | + ?assert(maps:size(Buckets) >= 10), |
| 176 | + passed. |
0 commit comments