HashSlotRangesTest.java

package redis.clients.jedis;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;

/**
 * Unit coverage for {@link HashSlotRanges}: the slots-or-ranges wire format
 * ({@code 123,456,789-1000}) of cluster maintenance pushes ��� parsing, membership, counting,
 * iteration order and rejection of malformed input.
 */
public class HashSlotRangesTest {

  @Test
  public void parseSingleSlot() {
    HashSlotRanges r = HashSlotRanges.parse("42");
    assertTrue(r.contains(42));
    assertFalse(r.contains(41));
    assertFalse(r.contains(43));
    assertEquals(1, r.slotCount());
    assertEquals(Arrays.asList(42), slots(r));
  }

  @Test
  public void parseInclusiveRange() {
    HashSlotRanges r = HashSlotRanges.parse("10-13");
    assertFalse(r.contains(9));
    assertTrue(r.contains(10));
    assertTrue(r.contains(13));
    assertFalse(r.contains(14));
    assertEquals(4, r.slotCount());
    assertEquals(Arrays.asList(10, 11, 12, 13), slots(r));
  }

  @Test
  public void parseDegenerateRange() { // from == to is a one-slot range
    HashSlotRanges r = HashSlotRanges.parse("7-7");
    assertTrue(r.contains(7));
    assertEquals(1, r.slotCount());
    assertEquals(Arrays.asList(7), slots(r));
  }

  @Test
  public void parseMixedSlotsAndRanges() {
    HashSlotRanges r = HashSlotRanges.parse("123,456,789-1000");
    assertTrue(r.contains(123));
    assertTrue(r.contains(456));
    assertTrue(r.contains(789));
    assertTrue(r.contains(900));
    assertTrue(r.contains(1000));
    assertFalse(r.contains(0));
    assertFalse(r.contains(124));
    assertFalse(r.contains(788));
    assertFalse(r.contains(1001));
    assertEquals(2 + (1000 - 789 + 1), r.slotCount());
  }

  @Test
  public void forEachSlotVisitsInWireOrder() { // not sorted: wire order is preserved
    HashSlotRanges r = HashSlotRanges.parse("5,1-3,9");
    assertEquals(Arrays.asList(5, 1, 2, 3, 9), slots(r));
  }

  @Test
  public void overlappingRangesCountSlotsPerRange() { // no dedup: count is the sum of the ranges
    HashSlotRanges r = HashSlotRanges.parse("1-3,2-4");
    assertEquals(6, r.slotCount());
    assertEquals(Arrays.asList(1, 2, 3, 2, 3, 4), slots(r));
    assertTrue(r.contains(2));
  }

  @Test
  public void parseAcceptsFullSlotSpace() {
    HashSlotRanges r = HashSlotRanges.parse("0-16383");
    assertTrue(r.contains(0));
    assertTrue(r.contains(16383));
    assertFalse(r.contains(-1));
    assertFalse(r.contains(16384));
    assertEquals(Protocol.CLUSTER_HASHSLOTS, r.slotCount());
  }

  @Test
  public void parseAcceptsBoundarySlots() {
    assertTrue(HashSlotRanges.parse("0").contains(0));
    assertTrue(HashSlotRanges.parse("16383").contains(16383));
  }

  @Test
  public void toStringIsTheWireSource() {
    assertEquals("123,456,789-1000", HashSlotRanges.parse("123,456,789-1000").toString());
    assertEquals("7", HashSlotRanges.parse("7").toString());
  }

  @Test
  public void parseRejectsNullOrEmpty() {
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse(null));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse(""));
  }

  @Test
  public void parseRejectsNonNumericTokens() {
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("abc"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,x"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1-x"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("x-5"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1.5"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse(" 1"));
  }

  @Test
  public void parseRejectsEmptyTokens() {
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,,2"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse(",1"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1-"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("-5"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("-"));
    // trailing empties: String.split drops them unless asked to keep them
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,,"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse(","));
  }

  @Test
  public void parseRejectsInvertedRange() {
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("10-9"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,10-9"));
  }

  @Test
  public void parseRejectsSlotsOutsideHashSlotSpace() {
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("16384"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("0-16384"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("16383-99999"));
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1,2,3,16384"));
  }

  @Test
  public void parseRejectsAnyBadTokenEvenAfterValidOnes() { // whole string is rejected atomically
    assertThrows(IllegalArgumentException.class, () -> HashSlotRanges.parse("1-3,5,bad"));
  }

  private static List<Integer> slots(HashSlotRanges r) {
    List<Integer> out = new ArrayList<>();
    r.forEachSlot(out::add);
    return out;
  }
}