This commit is contained in:
Looly
2026-01-13 16:54:25 +08:00
parent 0a4625ad69
commit 55b8884705
13 changed files with 593 additions and 32 deletions

View File

@@ -208,7 +208,7 @@ public class HexUtil extends Hex {
* @param value float值
* @return 16进制字符串
*/
public static String toHex(float value) {
public static String toHex(final float value) {
return Integer.toHexString(Float.floatToIntBits(value));
}
@@ -218,7 +218,7 @@ public class HexUtil extends Hex {
* @param value 16进制字符串
* @return 16进制字符串float值
*/
public static float hexToFloat(String value) {
public static float hexToFloat(final String value) {
return Float.intBitsToFloat(Integer.parseUnsignedInt(removeHexPrefix(value), 16));
}
@@ -228,7 +228,7 @@ public class HexUtil extends Hex {
* @param value double值
* @return 16进制字符串
*/
public static String toHex(double value) {
public static String toHex(final double value) {
return Long.toHexString(Double.doubleToLongBits(value));
}
@@ -238,7 +238,7 @@ public class HexUtil extends Hex {
* @param value 16进制字符串
* @return 16进制字符串double值
*/
public static double hexToDouble(String value) {
public static double hexToDouble(final String value) {
return Double.longBitsToDouble(Long.parseUnsignedLong(removeHexPrefix(value), 16));
}
@@ -288,7 +288,7 @@ public class HexUtil extends Hex {
* </pre>
*
* @param hexStr Hex字符串
* @param prefix 自定义前缀如0x
* @param prefix 自定义前缀如0x此前缀每个16进制数前都添加
* @return 格式化后的字符串
*/
public static String format(final String hexStr, final String prefix) {

View File

@@ -26,8 +26,8 @@ import java.util.Arrays;
* 它们分别根据字串计算 64 和 128 位的散列值。这些算法不适用于加密,但适合用在散列表等处。
*
* <p>
* 代码来自https://github.com/rolandhe/string-tools<br>
* 原始算法https://github.com/google/cityhash
* 代码来自:<a href="https://github.com/rolandhe/string-tools">string-tools</a><br>
* 原始算法:<a href="https://github.com/google/cityhash">cityhash</a>
*
* @author hexiufeng
* @since 5.2.5

View File

@@ -23,9 +23,9 @@ import java.nio.ByteBuffer;
* 除了卓越的性能外,他们还以算法生成而著称。
*
* <p>
* 官方实现https://github.com/jandrewrogers/MetroHash
* 官方文档http://www.jandrewrogers.com/2015/05/27/metrohash/
* 来自https://github.com/postamar/java-metrohash/
* 官方实现:<a href="https://github.com/jandrewrogers/MetroHash">MetroHash</a>
* 官方文档:<a href="http://www.jandrewrogers.com/2015/05/27/metrohash/">metrohash</a>
* 来自:<a href="https://github.com/postamar/java-metrohash/">java-metrohash</a>
*
* @author Marius Posta
* @param <R> 返回值类型为this类型

View File

@@ -24,9 +24,9 @@ import java.nio.ByteOrder;
* 除了卓越的性能外,他们还以算法生成而著称。
*
* <p>
* 官方实现https://github.com/jandrewrogers/MetroHash
* 官方文档http://www.jandrewrogers.com/2015/05/27/metrohash/
* 来自https://github.com/postamar/java-metrohash/
* 官方实现:<a href="https://github.com/jandrewrogers/MetroHash">MetroHash</a>
* 官方文档:<a href="http://www.jandrewrogers.com/2015/05/27/metrohash/">metrohash</a>
* 来自:<a href="https://github.com/postamar/java-metrohash/">java-metrohash</a>
*
* @param <R> 返回值类型为this类型
* @author Marius Posta

View File

@@ -27,9 +27,10 @@ import java.nio.ByteOrder;
* 除了卓越的性能外,他们还以算法生成而著称。
*
* <p>
* 官方实现https://github.com/jandrewrogers/MetroHash
* 官方文档http://www.jandrewrogers.com/2015/05/27/metrohash/
* 来自https://github.com/postamar/java-metrohash/
* 官方实现:<a href="https://github.com/jandrewrogers/MetroHash">MetroHash</a>
* 官方文档:<a href="http://www.jandrewrogers.com/2015/05/27/metrohash/">metrohash</a>
* 来自:<a href="https://github.com/postamar/java-metrohash/">java-metrohash</a>
*
* @author Marius Posta
*/
public class MetroHash128 extends AbstractMetroHash<MetroHash128> implements Hash128<byte[]> {

View File

@@ -26,9 +26,10 @@ import java.nio.ByteOrder;
* 除了卓越的性能外,他们还以算法生成而著称。
*
* <p>
* 官方实现https://github.com/jandrewrogers/MetroHash
* 官方文档http://www.jandrewrogers.com/2015/05/27/metrohash/
* 来自https://github.com/postamar/java-metrohash/
* 官方实现:<a href="https://github.com/jandrewrogers/MetroHash">MetroHash</a>
* 官方文档:<a href="http://www.jandrewrogers.com/2015/05/27/metrohash/">metrohash</a>
* 来自:<a href="https://github.com/postamar/java-metrohash/">java-metrohash</a>
*
* @author Marius Posta
*/
public class MetroHash64 extends AbstractMetroHash<MetroHash64> implements Hash64<byte[]> {

View File

@@ -18,7 +18,7 @@ package cn.hutool.v7.core.codec;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;
public class RadixUtilTest {
@Test
@@ -29,4 +29,151 @@ public class RadixUtilTest {
RadixUtil.decode(radixs, bad);
});
}
@Test
public void testEncodeInt() {
// Test binary conversion (base 2)
assertEquals("1010", RadixUtil.encode("01", 10));
// Test base 3 conversion
assertEquals("101", RadixUtil.encode("012", 10));
// Test base 16 conversion
assertEquals("A", RadixUtil.encode("0123456789ABCDEF", 10));
// Test with 34-radix
assertEquals("Y", RadixUtil.encode(RadixUtil.RADIXS_34, 32));
// Test with 59-radix
assertEquals("b", RadixUtil.encode(RadixUtil.RADIXS_59, 11));
}
@Test
public void testEncodeLong() {
// Test binary conversion (base 2)
assertEquals("1010", RadixUtil.encode("01", 10L));
// Test base 3 conversion
assertEquals("101", RadixUtil.encode("012", 10L));
// Test larger number
assertEquals("RR", RadixUtil.encode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 999L));
// Test with shuffle 34-radix
assertEquals("R", RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_34, 22));
}
@Test
public void testDecodeToInt() {
// Test binary decoding (base 2)
assertEquals(10, RadixUtil.decodeToInt("01", "1010"));
// Test base 3 decoding
assertEquals(10, RadixUtil.decodeToInt("012", "101"));
// Test base 16 decoding
assertEquals(10, RadixUtil.decodeToInt("0123456789ABCDEF", "A"));
// Test with 34-radix
assertEquals(30, RadixUtil.decodeToInt(RadixUtil.RADIXS_34, "W"));
}
@Test
public void testDecode() {
// Test binary decoding (base 2)
assertEquals(10L, RadixUtil.decode("01", "1010"));
// Test base 3 decoding
assertEquals(10L, RadixUtil.decode("012", "101"));
// Test base 16 decoding
assertEquals(10L, RadixUtil.decode("0123456789ABCDEF", "A"));
// Test with 59-radix
assertEquals(11L, RadixUtil.decode(RadixUtil.RADIXS_59, "b"));
// Test with shuffle 59-radix
assertEquals(1L, RadixUtil.decode(RadixUtil.RADIXS_SHUFFLE_59, "vh"));
}
@Test
public void testEncodeDecodeRoundTrip() {
// Test round trip for various bases
assertEquals(42, RadixUtil.decodeToInt("01", RadixUtil.encode("01", 42)));
assertEquals(123, RadixUtil.decodeToInt("0123456789", RadixUtil.encode("0123456789", 123)));
assertEquals(255, RadixUtil.decodeToInt("0123456789ABCDEF", RadixUtil.encode("0123456789ABCDEF", 255)));
// Test with predefined radixes
assertEquals(1000, RadixUtil.decodeToInt(RadixUtil.RADIXS_34, RadixUtil.encode(RadixUtil.RADIXS_34, 1000)));
assertEquals(2000, RadixUtil.decodeToInt(RadixUtil.RADIXS_59, RadixUtil.encode(RadixUtil.RADIXS_59, 2000)));
assertEquals(500, RadixUtil.decodeToInt(RadixUtil.RADIXS_SHUFFLE_34, RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_34, 500)));
assertEquals(750, RadixUtil.decodeToInt(RadixUtil.RADIXS_SHUFFLE_59, RadixUtil.encode(RadixUtil.RADIXS_SHUFFLE_59, 750)));
}
@Test
public void testEdgeCases() {
// Test zero
assertEquals("0", RadixUtil.encode("01", 0));
assertEquals(0, RadixUtil.decodeToInt("01", "0"));
// Test single digit numbers
assertEquals("1", RadixUtil.encode("01", 1));
assertEquals(1, RadixUtil.decodeToInt("01", "1"));
// Test with larger numbers
assertEquals("11111111", RadixUtil.encode("01", 255)); // 255 in binary
assertEquals(255, RadixUtil.decodeToInt("01", "11111111"));
// Test negative numbers (using the special handling in encode)
assertEquals(4294967254L, RadixUtil.decode("01", RadixUtil.encode("01", -42)));
}
@SuppressWarnings("ConstantValue")
@Test
public void testPredefinedRadixes() {
// Test 34-radix constants
assertNotNull(RadixUtil.RADIXS_34);
assertEquals(34, RadixUtil.RADIXS_34.length());
assertFalse(RadixUtil.RADIXS_34.contains("I"));
assertFalse(RadixUtil.RADIXS_34.contains("O"));
// Test 59-radix constants
assertNotNull(RadixUtil.RADIXS_59);
assertEquals(59, RadixUtil.RADIXS_59.length());
assertFalse(RadixUtil.RADIXS_59.contains("I"));
assertFalse(RadixUtil.RADIXS_59.contains("O"));
assertFalse(RadixUtil.RADIXS_59.contains("l"));
// Test shuffle radixes
assertNotNull(RadixUtil.RADIXS_SHUFFLE_34);
assertEquals(34, RadixUtil.RADIXS_SHUFFLE_34.length());
assertNotNull(RadixUtil.RADIXS_SHUFFLE_59);
assertEquals(59, RadixUtil.RADIXS_SHUFFLE_59.length());
}
@Test
public void testInvalidInputs() {
// Test invalid radix (too short)
assertThrows(RuntimeException.class, () -> RadixUtil.encode("0", 10)); // only 1 char
// Test decode with null radix
assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode(null, "10"));
// Test decode with empty string
assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode("01", ""));
// Test decode with invalid character (already tested in original method)
final String radixs = "0123456789ABC"; // base 13
final String bad = "1X3"; // 'X' 不在 radix 中
assertThrows(IllegalArgumentException.class, () -> RadixUtil.decode(radixs, bad));
}
@Test
public void testLongValueEncodeDecode() {
final long testValue = 1234567890L;
final String encoded = RadixUtil.encode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", testValue);
final long decoded = RadixUtil.decode("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", encoded);
assertEquals(testValue, decoded);
}
}

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
package cn.hutool.v7.core.codec;
package cn.hutool.v7.core.codec.binary;
import cn.hutool.v7.core.codec.binary.Base32;
import cn.hutool.v7.core.util.ByteUtil;
import cn.hutool.v7.core.util.RandomUtil;
import org.junit.jupiter.api.Assertions;

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
package cn.hutool.v7.core.codec;
package cn.hutool.v7.core.codec.binary;
import cn.hutool.v7.core.codec.binary.Base58;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
package cn.hutool.v7.core.codec;
package cn.hutool.v7.core.codec.binary;
import cn.hutool.v7.core.codec.binary.Base62;
import cn.hutool.v7.core.util.RandomUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

View File

@@ -14,9 +14,8 @@
* limitations under the License.
*/
package cn.hutool.v7.core.codec;
package cn.hutool.v7.core.codec.binary;
import cn.hutool.v7.core.codec.binary.Base64;
import cn.hutool.v7.core.util.ByteUtil;
import cn.hutool.v7.core.util.CharsetUtil;
import cn.hutool.v7.core.util.RandomUtil;

View File

@@ -0,0 +1,202 @@
package cn.hutool.v7.core.codec.binary;
import org.junit.jupiter.api.Test;
import java.awt.Color;
import static org.junit.jupiter.api.Assertions.*;
public class HexUtilTest {
@Test
public void testEncodeColor() {
final Color red = new Color(255, 0, 0);
assertEquals("#ff0000", HexUtil.encodeColor(red));
final Color green = new Color(0, 255, 0);
assertEquals("#00ff00", HexUtil.encodeColor(green));
final Color blue = new Color(0, 0, 255);
assertEquals("#0000ff", HexUtil.encodeColor(blue));
final Color black = new Color(0, 0, 0);
assertEquals("#000000", HexUtil.encodeColor(black));
final Color white = new Color(255, 255, 255);
assertEquals("#ffffff", HexUtil.encodeColor(white));
// Test with single digit values (should be padded with 0)
final Color testColor = new Color(1, 16, 255);
assertEquals("#0110ff", HexUtil.encodeColor(testColor));
}
@Test
public void testEncodeColorWithPrefix() {
final Color red = new Color(255, 0, 0);
assertEquals("0xff0000", HexUtil.encodeColor(red, "0x"));
assertEquals("#ff0000", HexUtil.encodeColor(red, "#"));
assertEquals("ff0000", HexUtil.encodeColor(red, ""));
}
@Test
public void testDecodeColor() {
assertEquals(new Color(255, 0, 0), HexUtil.decodeColor("#ff0000"));
assertEquals(new Color(0, 255, 0), HexUtil.decodeColor("#00ff00"));
assertEquals(new Color(0, 0, 255), HexUtil.decodeColor("#0000ff"));
assertEquals(new Color(255, 0, 0), HexUtil.decodeColor("0xff0000"));
}
@Test
public void testIsHexNumber() {
assertTrue(HexUtil.isHexNumber("ff"));
assertTrue(HexUtil.isHexNumber("FF"));
assertTrue(HexUtil.isHexNumber("0xff"));
assertTrue(HexUtil.isHexNumber("0XFF"));
assertTrue(HexUtil.isHexNumber("#ff"));
assertTrue(HexUtil.isHexNumber("123abc"));
assertTrue(HexUtil.isHexNumber("0x123abc"));
assertTrue(HexUtil.isHexNumber("#123abc"));
assertFalse(HexUtil.isHexNumber(""));
assertFalse(HexUtil.isHexNumber(null));
assertFalse(HexUtil.isHexNumber("gg")); // g is not hex digit
assertFalse(HexUtil.isHexNumber("-ff"));
assertFalse(HexUtil.isHexNumber("ff-"));
assertFalse(HexUtil.isHexNumber("12 34")); // space not allowed
}
@Test
public void testToUnicodeHex() {
assertEquals("\\u4f60", HexUtil.toUnicodeHex('你'));
assertEquals("\\u0048", HexUtil.toUnicodeHex('H'));
assertEquals("\\u0065", HexUtil.toUnicodeHex('e'));
assertEquals("\\u006c", HexUtil.toUnicodeHex('l'));
assertEquals("\\u006f", HexUtil.toUnicodeHex('o'));
// Test with integer values
assertEquals("\\u0041", HexUtil.toUnicodeHex(65)); // 'A'
assertEquals("\\u0000", HexUtil.toUnicodeHex(0));
assertEquals("\\uffff", HexUtil.toUnicodeHex(65535)); // max char value
}
@Test
public void testToHexFromInt() {
assertEquals("ff", HexUtil.toHex(255));
assertEquals("0", HexUtil.toHex(0));
assertEquals("10", HexUtil.toHex(16));
assertEquals("64", HexUtil.toHex(100));
assertEquals("ffff", HexUtil.toHex(65535));
}
@Test
public void testHexToInt() {
assertEquals(255, HexUtil.hexToInt("ff"));
assertEquals(255, HexUtil.hexToInt("0xff"));
assertEquals(255, HexUtil.hexToInt("#ff"));
assertEquals(0, HexUtil.hexToInt("0"));
assertEquals(16, HexUtil.hexToInt("10"));
assertEquals(100, HexUtil.hexToInt("64"));
assertEquals(65535, HexUtil.hexToInt("ffff"));
assertEquals(65535, HexUtil.hexToInt("0xffff"));
}
@Test
public void testToHexFromLong() {
assertEquals("ff", HexUtil.toHex(255L));
assertEquals("0", HexUtil.toHex(0L));
assertEquals("10", HexUtil.toHex(16L));
assertEquals("ffffffffffffffff", HexUtil.toHex(-1L));
}
@Test
public void testHexToLong() {
assertEquals(255L, HexUtil.hexToLong("ff"));
assertEquals(255L, HexUtil.hexToLong("0xff"));
assertEquals(0L, HexUtil.hexToLong("0"));
assertEquals(16L, HexUtil.hexToLong("10"));
assertThrows(NumberFormatException.class, ()-> HexUtil.hexToLong("ffffffffffffffff"));
}
@Test
public void testToHexFromFloat() {
assertEquals("40490fdb", HexUtil.toHex((float) Math.PI));
assertEquals("0", HexUtil.toHex(0.0f));
assertEquals("3f800000", HexUtil.toHex(1.0f));
}
@Test
public void testHexToFloat() {
assertEquals(Math.PI, HexUtil.hexToFloat("40490fdb"), 0.0001f);
assertEquals(0.0f, HexUtil.hexToFloat("0"), 0.0001f);
assertEquals(1.0f, HexUtil.hexToFloat("3f800000"), 0.0001f);
}
@Test
public void testToHexFromDouble() {
assertEquals("400921fb54442d18", HexUtil.toHex(Math.PI));
assertEquals("0", HexUtil.toHex(0.0));
assertEquals("3ff0000000000000", HexUtil.toHex(1.0));
}
@Test
public void testHexToDouble() {
assertEquals(Math.PI, HexUtil.hexToDouble("400921fb54442d18"), 0.0001);
assertEquals(0.0, HexUtil.hexToDouble("0"), 0.0001);
assertEquals(1.0, HexUtil.hexToDouble("3ff0000000000000"), 0.0001);
}
@Test
public void testAppendHex() {
StringBuilder sb = new StringBuilder();
HexUtil.appendHex(sb, (byte) 255, true); // lowercase
assertEquals("ff", sb.toString());
sb = new StringBuilder();
HexUtil.appendHex(sb, (byte) 255, false); // uppercase
assertEquals("FF", sb.toString());
sb = new StringBuilder();
HexUtil.appendHex(sb, (byte) 0, true);
assertEquals("00", sb.toString());
sb = new StringBuilder();
HexUtil.appendHex(sb, (byte) 16, true);
assertEquals("10", sb.toString());
}
@Test
public void testToBigInteger() {
assertNull(HexUtil.toBigInteger(null));
assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("ff"));
assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("0xff"));
assertEquals(new java.math.BigInteger("ff", 16), HexUtil.toBigInteger("#ff"));
assertEquals(new java.math.BigInteger("0", 16), HexUtil.toBigInteger("0"));
assertEquals(new java.math.BigInteger("1234abcd", 16), HexUtil.toBigInteger("1234abcd"));
}
@Test
public void testFormat() {
assertEquals("", HexUtil.format(""));
assertEquals("a", HexUtil.format("a"));
assertEquals("ab", HexUtil.format("ab"));
assertEquals("ab cd", HexUtil.format("abcd"));
assertEquals("ab cd ef", HexUtil.format("abcdef"));
assertEquals("ab cd ef 12", HexUtil.format("abcdef12"));
}
@Test
public void testFormatWithPrefix() {
assertEquals("0xab", HexUtil.format("ab", "0x"));
assertEquals("0xab 0xcd", HexUtil.format("abcd", "0x"));
assertEquals("#ab", HexUtil.format("ab", "#"));
assertEquals("#ab #cd", HexUtil.format("abcd", "#"));
}
@Test
public void testFormatWithPrefixAndSeparator() {
assertEquals("0xab 0xcd", HexUtil.format("abcd", "0x", " "));
assertEquals("0xab:0xcd", HexUtil.format("abcd", "0x", ":"));
assertEquals("ab-cd", HexUtil.format("abcd", "", "-"));
assertEquals("ab cd ef 12", HexUtil.format("abcdef12", null, null));
}
}

View File

@@ -0,0 +1,214 @@
package cn.hutool.v7.core.codec.hash;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ConsistentHashTest {
@Test
public void testConstructorWithDefaultHashFunction() {
final List<String> nodes = Arrays.asList("node1", "node2", "node3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(3, nodes);
assertNotNull(consistentHash);
// Cannot directly access numberOfReplicas and circle as they are private
// We'll test the functionality instead
final String node = consistentHash.get("testKey");
assertNotNull(node);
assertTrue(nodes.contains(node));
}
@Test
public void testConstructorWithCustomHashFunction() {
final Hash32<Object> customHash = Object::hashCode;
final List<String> nodes = Arrays.asList("server1", "server2", "server3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(customHash, 2, nodes);
assertNotNull(consistentHash);
// Test functionality instead of accessing private fields
final String node = consistentHash.get("testKey");
assertNotNull(node);
assertTrue(nodes.contains(node));
}
@Test
public void testAddNode() {
final List<String> nodes = Collections.singletonList("initial");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(2, nodes);
// Test that we can add a new node
consistentHash.add("newNode");
// Verify that the new node can be retrieved for some keys
final String result = consistentHash.get("someKey");
assertNotNull(result);
}
@Test
public void testRemoveNode() {
final List<String> nodes = Arrays.asList("node1", "node2", "node3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(2, nodes);
// Initially, there should be nodes
final String initialResult = consistentHash.get("testKey");
assertNotNull(initialResult);
// Remove a node
consistentHash.remove("node2");
// After removal, there should still be nodes to handle requests
final String resultAfterRemoval = consistentHash.get("testKey");
assertNotNull(resultAfterRemoval);
// The result might be different, but should not be null if other nodes exist
}
@Test
public void testGetNode() {
final List<String> nodes = Arrays.asList("server1", "server2", "server3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(3, nodes);
// Test that we can get a node for a key
final String node = consistentHash.get("key1");
assertNotNull(node);
assertTrue(nodes.contains(node));
// Test with different keys
final String node2 = consistentHash.get("key2");
assertNotNull(node2);
assertTrue(nodes.contains(node2));
}
@Test
public void testGetNodeWithEmptyCircle() {
final ConsistentHash<String> consistentHash = new ConsistentHash<>(2, Collections.emptyList());
// Should return null when there are no nodes
final String node = consistentHash.get("anyKey");
assertNull(node);
}
@Test
public void testConsistency() {
final List<String> nodes = Arrays.asList("server1", "server2", "server3", "server4", "server5");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(10, nodes);
// Test that the same key always maps to the same node
final String node1 = consistentHash.get("consistentKey");
final String node2 = consistentHash.get("consistentKey");
final String node3 = consistentHash.get("consistentKey");
assertEquals(node1, node2);
assertEquals(node2, node3);
}
@Test
public void testLoadDistribution() {
final List<String> nodes = Arrays.asList("server1", "server2", "server3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(10, nodes);
// Map many keys to nodes to test distribution
final int[] hits = new int[3];
for (int i = 0; i < 100; i++) {
final String node = consistentHash.get("key" + i);
switch (node) {
case "server1":
hits[0]++;
break;
case "server2":
hits[1]++;
break;
case "server3":
hits[2]++;
break;
}
}
// Verify that all servers got some traffic (with higher replica count, distribution should be relatively balanced)
for (final int hitCount : hits) {
assertTrue(hitCount > 0, "Each server should receive some load");
}
}
@Test
public void testNodeAdditionDoesNotDisruptTooMuch() {
final List<String> initialNodes = Arrays.asList("server1", "server2");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(10, initialNodes);
// Map keys to nodes with initial setup
final String[] initialMappings = new String[50];
for (int i = 0; i < 50; i++) {
initialMappings[i] = consistentHash.get("key" + i);
}
// Add a new node
consistentHash.add("server3");
// Map the same keys again
int remappedCount = 0;
for (int i = 0; i < 50; i++) {
final String newMapping = consistentHash.get("key" + i);
if (!initialMappings[i].equals(newMapping)) {
remappedCount++;
}
}
// Verify that not ALL keys are remapped (which would happen with naive modulo hashing)
assertTrue(remappedCount < 50, "Not all keys should be remapped when adding a new node");
// In a well-distributed consistent hash, typically only a fraction of keys are remapped
assertTrue(remappedCount <= 20, "Remapping should be minimal");
}
@Test
public void testNodeRemovalDoesNotDisruptTooMuch() {
final List<String> initialNodes = Arrays.asList("server1", "server2", "server3");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(10, initialNodes);
// Map keys to nodes with initial setup
final String[] initialMappings = new String[50];
for (int i = 0; i < 50; i++) {
initialMappings[i] = consistentHash.get("key" + i);
}
// Remove a node
consistentHash.remove("server3");
// Map the same keys again
int remappedCount = 0;
for (int i = 0; i < 50; i++) {
final String newMapping = consistentHash.get("key" + i);
if (!newMapping.equals(initialMappings[i])) {
remappedCount++;
}
}
// Verify that not ALL keys are remapped
assertTrue(remappedCount < 50, "Not all keys should be remapped when removing a node");
}
@Test
public void testSingleNode() {
final List<String> nodes = Collections.singletonList("onlyServer");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(5, nodes);
// All keys should map to the same server
for (int i = 0; i < 20; i++) {
assertEquals("onlyServer", consistentHash.get("key" + i));
}
}
@Test
public void testManyVirtualNodes() {
final List<String> nodes = Arrays.asList("node1", "node2");
final ConsistentHash<String> consistentHash = new ConsistentHash<>(100, nodes); // Many replicas
// Should still work normally
final String node = consistentHash.get("testKey");
assertNotNull(node);
assertTrue(nodes.contains(node));
}
}