You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@geode.apache.org by GitBox <gi...@apache.org> on 2022/01/18 21:43:54 UTC

[GitHub] [geode] BalaKaza opened a new pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

BalaKaza opened a new pull request #7278:
URL: https://github.com/apache/geode/pull/7278


   SSCAN command is implemented and integration tests are added to test
   this command.
   
   Authored-by: Bala Kaza Venkata <bk...@vmware.com>
   
   <!-- Thank you for submitting a contribution to Apache Geode. -->
   
   <!-- In order to streamline the review of the contribution we ask you
   to ensure the following steps have been taken: 
   -->
   
   ### For all changes:
   - [ ] Is there a JIRA ticket associated with this PR? Is it referenced in the commit message?
   
   - [ ] Has your PR been rebased against the latest commit within the target branch (typically `develop`)?
   
   - [ ] Is your initial contribution a single, squashed commit?
   
   - [ ] Does `gradlew build` run cleanly?
   
   - [ ] Have you written or updated unit tests to verify your changes?
   
   - [ ] If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under [ASF 2.0](http://www.apache.org/legal/resolved.html#category-a)?
   
   <!-- Note:
   Please ensure that once the PR is submitted, check Concourse for build issues and
   submit an update to your PR as soon as possible. If you need help, please send an
   email to dev@geode.apache.org.
   -->
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on pull request #7278:
URL: https://github.com/apache/geode/pull/7278#issuecomment-1021391782


   Oops, I committed a merge from develop onto the feature branch, and then undid it.


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r796013240



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +72,394 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistentKey", ZERO_CURSOR);
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    ScanResult<byte[]> result =
+        sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenAdditionalArgumentNotEqualToMatchOrCount_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT",
+            "notAnInteger"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "notAnInteger", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(0)))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(-37)))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY, ZERO_CURSOR))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.hset(KEY, "b", MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sscan("nonexistentKey", "notAnInteger"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.set(KEY, "b");
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, MEMBER_ONE);
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 99;
 
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(count));
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
+  }
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+  @Test
+  public void givenMultipleCounts_usesLastCountSpecified() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
+    // Choose two COUNT arguments with a large difference, so that it's extremely unlikely that if
+    // the first COUNT is used, a number of members greater than or equal to the second COUNT will
+    // be returned.
+    int firstCount = 1;
+    int secondCount = 500;
+    ScanParams scanParams = new ScanParams().count(firstCount).count(secondCount);

Review comment:
       It's not possible to test the server's behaviour with multiple MATCH or COUNT parameters used if you try to specify them using `ScanParameters`, because internally, `ScanParameters` uses a map to store the values, meaning the multiple calls to `match()` or `count()` just overwrite previous calls. In order to test the behaviour we're interested in, you need to use the sendCustomSscanCommand helper method:
   ```
       result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR,
           "COUNT", String.valueOf(firstCount),
           "COUNT", String.valueOf(secondCount));
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +72,394 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistentKey", ZERO_CURSOR);
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    ScanResult<byte[]> result =
+        sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenAdditionalArgumentNotEqualToMatchOrCount_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT",
+            "notAnInteger"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "notAnInteger", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(0)))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(-37)))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY, ZERO_CURSOR))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.hset(KEY, "b", MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sscan("nonexistentKey", "notAnInteger"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.set(KEY, "b");
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, MEMBER_ONE);
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 99;
 
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(count));
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
+  }
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+  @Test
+  public void givenMultipleCounts_usesLastCountSpecified() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
+    // Choose two COUNT arguments with a large difference, so that it's extremely unlikely that if
+    // the first COUNT is used, a number of members greater than or equal to the second COUNT will
+    // be returned.
+    int firstCount = 1;
+    int secondCount = 500;
+    ScanParams scanParams = new ScanParams().count(firstCount).count(secondCount);
+
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
+
+    List<byte[]> returnedMembers = result.getResult();
+    assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(secondCount);
+    assertThat(returnedMembers).isSubsetOf(initialMemberData);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+  public void givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements() {
+    jedis.sadd(KEY, MEMBER_ONE, MEMBER_TWELVE, MEMBER_THREE);
+    ScanParams scanParams = new ScanParams().match("1*");
 
-    List<Object> result;
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
 
-    List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE.getBytes(),
+        MEMBER_TWELVE.getBytes());
+  }
 
-    do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+  @Test
+  public void givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument() {
+    jedis.sadd(KEY, MEMBER_ONE, MEMBER_TWELVE, MEMBER_THREE);
+    ScanParams scanParams = new ScanParams().match("3*").match("1*");
+
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat(result.getCursor()).isEqualTo(ZERO_CURSOR);
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE.getBytes(),
+        MEMBER_TWELVE.getBytes());
   }
 
   @Test
-  public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenLargeCountAndMatch_returnsOnlyMatchingMembers() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
     ScanParams scanParams = new ScanParams();
-    scanParams.match("1*");
+    // There are 111 matching members in the set 0..999
+    scanParams.match("9*");
+    // Choose a large COUNT to ensure that some matching members are returned
+    scanParams.count(950);
+
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
+
+    List<byte[]> returnedMembers = result.getResult();
+    // We know that we must have found at least 61 matching members, given the size of COUNT and the
+    // number of matching members in the set
+    assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(61);
+    assertThat(returnedMembers).isSubsetOf(initialMemberData);
+    assertThat(returnedMembers).allSatisfy(bytes -> assertThat(new String(bytes)).startsWith("9"));
+  }
 
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+  @Test
+  public void givenMultipleCountAndMatch_usesLastSpecified() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
+    // Choose a large COUNT to ensure that some matching members are returned
+    // There are 111 matching members in the set 0..999
+    ScanParams scanParams = new ScanParams().count(20).match("1*").count(950).match("9*");

Review comment:
       See the comment above about using `sendCustomSscanCommand()` instead of multiple calls to `ScanParams` methods here.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +72,394 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistentKey", ZERO_CURSOR);
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    ScanResult<byte[]> result =
+        sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenAdditionalArgumentNotEqualToMatchOrCount_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT",
+            "notAnInteger"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "notAnInteger", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(0)))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(-37)))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY, ZERO_CURSOR))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.hset(KEY, "b", MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sscan("nonexistentKey", "notAnInteger"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.set(KEY, "b");
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, MEMBER_ONE);
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 99;
 
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(count));
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
+  }
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+  @Test
+  public void givenMultipleCounts_usesLastCountSpecified() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
+    // Choose two COUNT arguments with a large difference, so that it's extremely unlikely that if
+    // the first COUNT is used, a number of members greater than or equal to the second COUNT will
+    // be returned.
+    int firstCount = 1;
+    int secondCount = 500;
+    ScanParams scanParams = new ScanParams().count(firstCount).count(secondCount);
+
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
+
+    List<byte[]> returnedMembers = result.getResult();
+    assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(secondCount);
+    assertThat(returnedMembers).isSubsetOf(initialMemberData);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+  public void givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements() {
+    jedis.sadd(KEY, MEMBER_ONE, MEMBER_TWELVE, MEMBER_THREE);
+    ScanParams scanParams = new ScanParams().match("1*");
 
-    List<Object> result;
+    ScanResult<byte[]> result = jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, scanParams);
 
-    List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE.getBytes(),
+        MEMBER_TWELVE.getBytes());
+  }
 
-    do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+  @Test
+  public void givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument() {
+    jedis.sadd(KEY, MEMBER_ONE, MEMBER_TWELVE, MEMBER_THREE);
+    ScanParams scanParams = new ScanParams().match("3*").match("1*");

Review comment:
       See the comment above about using `sendCustomSscanCommand()` instead of multiple calls to `ScanParams` methods here.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794898131



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {

Review comment:
       We've had problems in the past with getting the precedence of errors wrong on certain commands, so testing the returned error message with both a wrong type and a wrong count does give us value on top of just testing the wrong type case.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r787241591



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableObjectOpenCustomHashSetWithCursor.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.data.collections;
+
+
+import static it.unimi.dsi.fastutil.HashCommon.mix;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+
+import java.util.Collection;
+
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableObjectOpenCustomHashSetWithCursor<K>
+    extends ObjectOpenCustomHashSet<K>
+    implements Sizeable {
+  private static final long serialVersionUID = 9174920505089089517L;
+  private static final int OPEN_HASH_SET_OVERHEAD =
+      memoryOverhead(SizeableObjectOpenCustomHashSetWithCursor.class);
+
+  private int memberOverhead;
+
+  public SizeableObjectOpenCustomHashSetWithCursor(int expected, Strategy<? super K> strategy) {
+    super(expected, strategy);
+  }
+
+  public SizeableObjectOpenCustomHashSetWithCursor(Strategy<? super K> strategy) {
+    super(strategy);
+  }
+
+  public SizeableObjectOpenCustomHashSetWithCursor(Collection<? extends K> c,
+      Strategy<? super K> strategy) {
+    super(c, strategy);
+  }
+
+  @Override
+  public boolean add(K k) {
+    boolean added = super.add(k);
+    if (added) {
+      memberOverhead += sizeElement(k);
+    }
+    return added;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean remove(Object k) {
+    boolean removed = super.remove(k);
+    if (removed) {
+      memberOverhead -= sizeElement((K) k);
+    }
+    return removed;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    // The object referenced by the "strategy" field is not sized
+    // since it is usually a singleton instance.
+    return OPEN_HASH_SET_OVERHEAD + memoryOverhead(key) + memberOverhead;
+  }
+
+  /**
+   * Scan entries and pass them to the given consumer function, starting at the passed in
+   * cursor. This method will scan until at least count entries are returned, or the entire
+   * map has been scanned. Once the returned cursor is 0, the entire map is scanned.

Review comment:
       Comments in this class referring to maps should be changed to sets and references to keys or values should be changed to elements, as we're scanning a Set, not a Map. Also, rather than using `K` as a generic parameter, `E` should be used.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/server/AbstractHitsMissesIntegrationTest.java
##########
@@ -554,23 +559,12 @@ public void testSrandmember() {
     runCommandAndAssertHitsAndMisses(SET_KEY, k -> jedis.srandmember(k));
   }
 
-  @Test
-  public void testSscan() {
-    runCommandAndAssertHitsAndMisses(SET_KEY, k -> jedis.sscan(k, "0"));
-  }
-
   @Test
   public void testSinterstore() {
     runMultiKeyCommandAndAssertNoStatUpdates(SET_KEY,
         (k1, k2) -> jedis.sinterstore(HASHTAG + "dest", k1, k2));
   }
 
-  @Test
-  public void testSunionstore() {
-    runMultiKeyCommandAndAssertNoStatUpdates(SET_KEY,
-        (k1, k2) -> jedis.sunionstore(HASHTAG + "dest", k1, k2));
-  }
-

Review comment:
       This test should not be being removed in this PR.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -202,42 +201,29 @@ static int setOpStoreResult(RegionProvider regionProvider, RedisKey destinationK
     return diff.size();
   }
 
-  public Pair<BigInteger, List<Object>> sscan(GlobPattern matchPattern, int count,
-      BigInteger cursor) {
-    List<Object> returnList = new ArrayList<>();
-    int size = members.size();
-    BigInteger beforeCursor = new BigInteger("0");
-    int numElements = 0;
-    int i = -1;
-    for (byte[] value : members) {
-      i++;
-      if (beforeCursor.compareTo(cursor) < 0) {
-        beforeCursor = beforeCursor.add(new BigInteger("1"));
-        continue;
-      }
+  public Pair<Integer, List<byte[]>> sscan(GlobPattern matchPattern, int count,
+      int cursor) {
 
-      if (matchPattern != null) {
-        if (matchPattern.matches(value)) {
-          returnList.add(value);
-          numElements++;
-        }
-      } else {
-        returnList.add(value);
-        numElements++;
-      }
+    // No need to allocate more space than it's possible to use given the size of the hash. We need
+    // to add 1 to hlen() to ensure that if count > hash.size(), we return a cursor of 0

Review comment:
       This comment needs updating, since we're not operating on a hash set here.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,33 +14,53 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.commands.executor.cluster.CRC16;
+import org.apache.geode.redis.internal.services.RegionProvider;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
   private static final int REDIS_CLIENT_TIMEOUT =
       Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());

Review comment:
       Rather than defining this constant, the `RedisClusterStartupRule.REDIS_CLIENT_TIMEOUT` constant should be used in the `setUp()` method. Also, instead of hard-coding `"localhost"` as the bind address there, the `RedisClusterStartupRule.BIND_ADDRESS` constant should be used.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/collections/SizeableObjectOpenCustomHashSetWithCursor.java
##########
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License
+ * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied. See the License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.geode.redis.internal.data.collections;
+
+
+import static it.unimi.dsi.fastutil.HashCommon.mix;
+import static org.apache.geode.internal.JvmSizeUtils.memoryOverhead;
+
+import java.util.Collection;
+
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+
+import org.apache.geode.annotations.VisibleForTesting;
+import org.apache.geode.internal.size.Sizeable;
+
+public abstract class SizeableObjectOpenCustomHashSetWithCursor<K>
+    extends ObjectOpenCustomHashSet<K>
+    implements Sizeable {
+  private static final long serialVersionUID = 9174920505089089517L;
+  private static final int OPEN_HASH_SET_OVERHEAD =
+      memoryOverhead(SizeableObjectOpenCustomHashSetWithCursor.class);
+
+  private int memberOverhead;
+
+  public SizeableObjectOpenCustomHashSetWithCursor(int expected, Strategy<? super K> strategy) {
+    super(expected, strategy);
+  }
+
+  public SizeableObjectOpenCustomHashSetWithCursor(Strategy<? super K> strategy) {
+    super(strategy);
+  }
+
+  public SizeableObjectOpenCustomHashSetWithCursor(Collection<? extends K> c,
+      Strategy<? super K> strategy) {
+    super(c, strategy);
+  }
+
+  @Override
+  public boolean add(K k) {
+    boolean added = super.add(k);
+    if (added) {
+      memberOverhead += sizeElement(k);
+    }
+    return added;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public boolean remove(Object k) {
+    boolean removed = super.remove(k);
+    if (removed) {
+      memberOverhead -= sizeElement((K) k);
+    }
+    return removed;
+  }
+
+  @Override
+  public int getSizeInBytes() {
+    // The object referenced by the "strategy" field is not sized
+    // since it is usually a singleton instance.
+    return OPEN_HASH_SET_OVERHEAD + memoryOverhead(key) + memberOverhead;
+  }
+
+  /**
+   * Scan entries and pass them to the given consumer function, starting at the passed in
+   * cursor. This method will scan until at least count entries are returned, or the entire
+   * map has been scanned. Once the returned cursor is 0, the entire map is scanned.
+   *
+   * This method may emit more than *count* number of elements if there are hash collisions.
+   *
+   * @param cursor The cursor to start from. Should be 0 for the initial scan. Subsequent calls
+   *        should use the cursor returned by the previous scan call.
+   * @param count The number of elements to scan
+   * @param consumer A function to pass the scanned keys and values to
+   * @param privateData Some data to pass to the function, for example a map to collect values in.
+   *        This
+   *        allows the function to be stateless.
+   * @param <D> The type of the data passed to the function/
+   * @return The next cursor to scan from, or 0 if the scan has touched all elements.
+   */
+  public <D> int scan(int cursor, int count,
+      SizeableObjectOpenCustomHashSetWithCursor.EntryConsumer<K, D> consumer, D privateData) {
+    // Implementation notes
+    //
+    // This stateless scan cursor algorithm is based on the dictScan cursor
+    // implementation from dict.c in redis. Please see the comments in that class for the full
+    // details. That iteration algorithm was designed by Pieter Noordhuis.
+    //
+    // There is one wrinkle due to the fact that we are using a different type of hashtable here.
+    // The parent class, Object2ObjectOpenHashMap, uses an open addressing with a linear

Review comment:
       This comment also needs updating, as the parent class of this is not a `Object2ObjectOpenHashMap`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {

Review comment:
       This test name could be more accurate as "givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError" since the number of arguments isn't actually the relevant factor here, just that there is a syntax error of some kind.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }

Review comment:
       These two tests are redundant with the "givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError()` test, so they can be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
     List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", ZERO_CURSOR, "a*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
     assertThat((List<Object>) result.get(1)).isEmpty();
   }
 
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenMatchArgumentWithoutPatternOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+            ZERO_CURSOR, "MATCH"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(
+            jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+                ZERO_CURSOR, "COUNT"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", FIELD_ONE))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", FIELD_TWO,
+            "COUNT", "sjlfs", "COUNT", FIELD_ONE))

Review comment:
       `FIELD_ONE` and `FIELD_TWO` here should be replaced with "1" and "2" (or any other integer value) since they represent count values, not fields.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
     List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", ZERO_CURSOR, "a*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
     assertThat((List<Object>) result.get(1)).isEmpty();
   }
 
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenMatchArgumentWithoutPatternOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+            ZERO_CURSOR, "MATCH"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(
+            jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+                ZERO_CURSOR, "COUNT"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", FIELD_ONE))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", FIELD_TWO,
+            "COUNT", "sjlfs", "COUNT", FIELD_ONE))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", FIELD_TWO,
+            "COUNT", "0", "COUNT", FIELD_ONE))

Review comment:
       `FIELD_ONE` and `FIELD_TWO` here should be replaced with "1" and "2" (or any other integer value) since they represent count values, not fields.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {

Review comment:
       This test would be better named "givenSetWithThreeMembers_returnsAllMembers" as we certainly don't expect that this behaviour holds true for an arbitrary set size. The assertion should also be changed to `containsOnly()` to allow duplicate entries.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {

Review comment:
       This test is flawed, as for small set sizes, native Redis will return all elements in one SSCAN regardless of the value of COUNT, and duplicate elements are allowed. A significant improvement would be to populate a set with 1000 elements, then call SSCAN once and assert that the number of elements returned is greater than or equal to the appropriate COUNT value and that they are a subset of the total set contents.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {

Review comment:
       This test is flawed, as for small set sizes, native Redis will return all elements in one SSCAN regardless of the value of COUNT, and duplicate elements are allowed. A significant improvement would be to populate a set with 1000 elements, then call SSCAN once and assert that the number of elements returned is greater than or equal to the appropriate COUNT value and that they are a subset of the total set contents.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    List<Object> result =
+        (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR,
+            "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {

Review comment:
       This test would be better named "givenSetWithThreeMembersAndMatchAndCount_returnsAllMatchingMembers" with the assertions changed to `containsOnly()` to allow duplicate entries.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(Integer.MAX_VALUE);

Review comment:
       This value is equal to `Integer.MAX_VALUE` rather than being greater than it. This should be:
   ```
   String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_notLoseFields_givenConcurrentThreadsDoingSScansAndChangingValues() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnSizeOfResultSet(jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnSizeOfResultSet(jedis2, initialMemberData),
+        (i) -> {
+          int fieldSuffix = i % SIZE_OF_SET;
+          jedis.sadd(KEY, BASE_FIELD + fieldSuffix);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notLoseKeysForConsistentlyPresentFields_givenConcurrentThreadsAddingAndRemovingFields() {

Review comment:
       This test's name and variables should be updated to reflect that fact that it's testing a set and not a hash. Sets don't have keys, fields or values, they only have members. The name could be something like "should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers"

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {

Review comment:
       It would be good to add tests that show the behaviour when the cursor value is outside the range `Long.MIN_VALUE > x > Long.MAX_VALUE`, and tests for the behaviour when the cursor in just inside that range.
   
   The behaviour of geode-for-redis differs from native Redis here, as they accept cursor values up to `UNSIGNED_LONG_CAPACITY` but we only accept ones up to `Long.MAX_VALUE`, so in order to test this, the test cases for failing with values outside the range should be put in `SScanIntegrationTest` (not the Abstract parent class). Also, looking at that class, the test `givenDifferentCursorThanSpecifiedByPreviousSscan_returnsAllMembers` is wrong and should be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_notLoseFields_givenConcurrentThreadsDoingSScansAndChangingValues() {

Review comment:
       This test is not really doing anything, since the calls to `sadd` in the `ConcurrentLoopingThreads` are just adding already existing members. I suspect that the test this is copied from was for a Redis Hash, which has fields and values, and the values were being modified while asserting that this didn't result in the fields not showing up in an HSCAN, but this test case doesn't make sense for a Redis Set, so this should just be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {

Review comment:
       This test is flawed, as there is no such thing as an invalid syntax for glob-style patterns. This test is just testing that given a non-matching pattern, an empty array is returned, so the name should reflect that. In fact, if the test is modified to the below, then it fails, as the element `p` matches and is returned:
   ```
     @Test
     public void givenInvalidRegexSyntax_returnsEmptyArray() {
       jedis.sadd(KEY, "\\p", "p");
       ScanParams scanParams = new ScanParams();
       scanParams.count(10);
       scanParams.match("\\p");
   
       ScanResult<byte[]> result =
           jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
   
       assertThat(result.getResult()).isEmpty();
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,33 +14,53 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.commands.executor.cluster.CRC16;
+import org.apache.geode.redis.internal.services.RegionProvider;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
   private static final int REDIS_CLIENT_TIMEOUT =
       Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  public static final String KEY = "key";
+  public static final int SLOT_FOR_KEY = CRC16.calculate(KEY) % RegionProvider.REDIS_SLOTS;

Review comment:
       This could be simplified slightly to `KeyHashUtil.slotForKey(KEY.getBytes());`. If you wanted to, you could even add a method to `KeyHashUtil` that takes a String argument and then converts to to a byte array using `Coder.stringToBytes()` then calls the `byte[]` argument version, so we don't have to call `getBytes()` here.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
     List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", ZERO_CURSOR, "a*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
     assertThat((List<Object>) result.get(1)).isEmpty();
   }
 
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenMatchArgumentWithoutPatternOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+            ZERO_CURSOR, "MATCH"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, FIELD_ONE);
+
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenCountArgumentWithoutNumberOnNonExistentKey_returnsEmptyArray() {
+    List<Object> result =
+        uncheckedCast(
+            jedis.sendCommand("nonexistentKey", Protocol.Command.SSCAN, "nonexistentKey",
+                ZERO_CURSOR, "COUNT"));
+
+    assertThat((List<?>) result.get(1)).isEmpty();
+  }
+
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", FIELD_ONE))

Review comment:
       Rather than using `FIELD_ONE` here, it's okay to use "1" (or any other integer really) as this argument is intended to be the `COUNT` value and using `FIELD_ONE` could cause confusion.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {

Review comment:
       This test (and any other similar ones) is incorrect, as this is not the behaviour seen with native Redis. For example, if a set is loaded with 1000 entries using the redis cli, the following results are seen:
   ```
   127.0.0.1:6379> sscan key 10
   1) "586"
   2)  1) "928"
       2) "884"
       3) "28"
       4) "834"
       5) "411"
       6) "397"
       7) "346"
       8) "902"
       9) "218"
      10) "586"
      11) "440"
   127.0.0.1:6379> sscan key -10
   1) "398"
   2)  1) "688"
       2) "41"
       3) "9"
       4) "204"
       5) "674"
       6) "639"
       7) "550"
       8) "979"
       9) "2"
      10) "677"
      11) "643"
   ```
   showing that a cursor value of -10 is not equivalent to a cursor value of 10. Moreover, the only valid values for CURSOR in a SCAN command are 0 or the previously returned value, so behaviour with a negative cursor value is technically undefined (see [Redis documentation for SCAN](https://redis.io/commands/scan#calling-scan-with-a-corrupted-cursor)). This test would be better as just "negativeCursor_doesNotError" as that's all we guarantee.  

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+    jedis.sadd(KEY, FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {

Review comment:
       This test name could be more accurate as "givenIncorrectOptionalArgumentAndKeyDoesNotExist_returnsEmptyArray" since the number of arguments isn't actually the relevant factor here, just that there is a syntax error of some kind.

##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
##########
@@ -252,6 +252,8 @@
       new Parameter().exact(2).flags(READONLY, SORT_FOR_SCRIPT)),
   SMOVE(new SMoveExecutor(), SUPPORTED, new Parameter().exact(4).lastKey(2).flags(WRITE, FAST)),
   SREM(new SRemExecutor(), SUPPORTED, new Parameter().min(3).flags(WRITE, FAST)),
+  SSCAN(new SScanExecutor(), UNSUPPORTED, new Parameter().min(3).flags(READONLY, RANDOM),

Review comment:
       This command should be using the `SUPPORTED` value of RedisCommandSupportLevel, not `UNSUPPORTED`. Also, I think that the `firstKey(0)` flag should not be present and can be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {

Review comment:
       This test is also flawed for the same reasons as earlier tests. A better name would be "givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements" with the assertions changed to `containsOnly()` to allow duplicate entries.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,33 +14,53 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.commands.executor.cluster.CRC16;
+import org.apache.geode.redis.internal.services.RegionProvider;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
   private static final int REDIS_CLIENT_TIMEOUT =
       Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  public static final String KEY = "key";
+  public static final int SLOT_FOR_KEY = CRC16.calculate(KEY) % RegionProvider.REDIS_SLOTS;
+  public static final String ZERO_CURSOR = "0";
+  public static final BigInteger UNSIGNED_LONG_CAPACITY = new BigInteger("18446744073709551615");
+  public static final BigInteger SIGNED_LONG_CAPACITY = new BigInteger("-18446744073709551615");

Review comment:
       This would be better named "NEGATIVE_LONG_CAPACITY" since it's not accurate to say that the capacity of a signed long is -18446744073709551615.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {

Review comment:
       This test is also flawed for the same reasons as earlier tests. A better name would be "givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument" with the assertions changed to `containsOnly()` to allow duplicate entries.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    List<Object> result =
+        (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR,
+            "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("1*");
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCountsAndMatches_returnsAllEntriesWithoutDuplicates() {

Review comment:
       This test would be better named "givenSetWithThreeMembersAndMultipleMatchAndCountArguments_returnsAllMatchingMembers" with the assertions changed to `containsOnly()` to allow duplicate entries.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, FIELD_ONE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_notLoseFields_givenConcurrentThreadsDoingSScansAndChangingValues() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnSizeOfResultSet(jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnSizeOfResultSet(jedis2, initialMemberData),
+        (i) -> {
+          int fieldSuffix = i % SIZE_OF_SET;
+          jedis.sadd(KEY, BASE_FIELD + fieldSuffix);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notLoseKeysForConsistentlyPresentFields_givenConcurrentThreadsAddingAndRemovingFields() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+
+    initialMemberData
+        .forEach((field) -> assertThat(jedis.sismember(KEY, field)).isTrue());

Review comment:
       `field` should be `member`.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,33 +14,53 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.commands.executor.cluster.CRC16;
+import org.apache.geode.redis.internal.services.RegionProvider;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
   private static final int REDIS_CLIENT_TIMEOUT =
       Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  public static final String KEY = "key";
+  public static final int SLOT_FOR_KEY = CRC16.calculate(KEY) % RegionProvider.REDIS_SLOTS;
+  public static final String ZERO_CURSOR = "0";
+  public static final BigInteger UNSIGNED_LONG_CAPACITY = new BigInteger("18446744073709551615");
+  public static final BigInteger SIGNED_LONG_CAPACITY = new BigInteger("-18446744073709551615");
+
+  public static final String FIELD_ONE = "1";
+  public static final String FIELD_TWO = "12";
+  public static final String FIELD_THREE = "3";
+
+  public static final String BASE_FIELD = "baseField_";
+  private final int SIZE_OF_SET = 100;

Review comment:
       To ensure that native Redis isn't using its more compact data structure (which behaves differently for SSCAN) in tests using this constant, this size should be increased to 1000.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r788979823



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {

Review comment:
       We return a different output (if the set is large and we don't just scan the whole thing in one go) and the test is wrong.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794862362



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {

Review comment:
       Testing the error condition with the combination of "key is not a set" and "count is negative" seems unnecessary. Testing `givenKeyIsNotASet_returnsWrongTypeError` would be sufficient, no?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794857700



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }

Review comment:
       s




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794862362



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {

Review comment:
       Testing the error condition with the combination of "key is not a set" and "count is -37" seems unnecessary. Testing `givenKeyIsNotASet_returnsWrongTypeError` would be sufficient, no?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r796026682



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +72,394 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistentKey", ZERO_CURSOR);
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    ScanResult<byte[]> result =
+        sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
+
+  @Test
+  public void givenAdditionalArgumentNotEqualToMatchOrCount_returnsSyntaxError() {
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT",
+            "notAnInteger"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "notAnInteger", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(0)))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(-37)))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", MEMBER_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sscan(KEY, ZERO_CURSOR))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.hset(KEY, "b", MEMBER_ONE);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sscan("nonexistentKey", "notAnInteger"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
-        .hasMessageContaining(ERROR_CURSOR);
+    jedis.set(KEY, "b");
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, "notAnInteger"))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, MEMBER_ONE);
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(MEMBER_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 99;
 
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY_BYTES, ZERO_CURSOR_BYTES, new ScanParams().count(count));
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
+  }
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+  @Test
+  public void givenMultipleCounts_usesLastCountSpecified() {
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
+    // Choose two COUNT arguments with a large difference, so that it's extremely unlikely that if
+    // the first COUNT is used, a number of members greater than or equal to the second COUNT will
+    // be returned.
+    int firstCount = 1;
+    int secondCount = 500;
+    ScanParams scanParams = new ScanParams().count(firstCount).count(secondCount);

Review comment:
       Interesting. 




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794855975



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,37 +14,57 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.data.KeyHashUtil;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
-  private static final int REDIS_CLIENT_TIMEOUT =
-      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  protected ScanResult<byte[]> result;

Review comment:
       I didn't like the amount of boilerplate code it added and making lines unnecessary long. It was common across ~10 tests, and that made it worth it to me. Clearing it in the setup() method is kinda funky, I agree. Clearing it in the teardown method seems less bad.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] steve-sienk commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
steve-sienk commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r794855975



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,37 +14,57 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.data.KeyHashUtil;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
-  private static final int REDIS_CLIENT_TIMEOUT =
-      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  protected ScanResult<byte[]> result;

Review comment:
       I didn't like the amount of boilerplate code it added and making lines unnecessarily long. It was common across ~10 tests, and that made it worth it to me. Clearing it in the setup() method is kinda funky, I agree. Clearing it in the teardown method seems less bad.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] BalaKaza commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
BalaKaza commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r788973072



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {

Review comment:
       So at the moment we just return the same output as the positive cursor and that is okay?




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r792193884



##########
File path: geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisSet.java
##########
@@ -208,42 +207,26 @@ static int setOpStoreResult(RegionProvider regionProvider, RedisKey destinationK
     return result.size();
   }
 
-  public Pair<BigInteger, List<Object>> sscan(GlobPattern matchPattern, int count,
-      BigInteger cursor) {
-    List<Object> returnList = new ArrayList<>();
-    int size = members.size();
-    BigInteger beforeCursor = new BigInteger("0");
-    int numElements = 0;
-    int i = -1;
-    for (byte[] value : members) {
-      i++;
-      if (beforeCursor.compareTo(cursor) < 0) {
-        beforeCursor = beforeCursor.add(new BigInteger("1"));
-        continue;
-      }
+  public Pair<Integer, List<byte[]>> sscan(GlobPattern matchPattern, int count,
+      int cursor) {
+    long maximumCapacity = Math.min(count, scard() + 1);

Review comment:
       This can be an `int` which removed the need for a cast on line 213.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +215,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void negativeCursor_doesNotError() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsFewMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsAnyElementsOf(initialMemberData);

Review comment:
       This assertion should be `isSubsetOf()`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+
+    initialMemberData
+        .forEach((member) -> assertThat(jedis.sismember(KEY, member)).isTrue());
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  private void multipleSScanAndAssertOnContentOfResultSet(int iteration, Jedis jedis,
+      final List<String> initialMemberData) {
+
+    List<String> allEntries = new ArrayList<>();
+    ScanResult<String> result;
+    String cursor = ZERO_CURSOR;
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      cursor = result.getCursor();
+      List<String> resultEntries = result.getResult();
+      resultEntries
+          .forEach((entry) -> allEntries.add(entry));
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).as("failed on iteration " + iteration)
+        .containsAll(initialMemberData);
+  }
+
+  private void multipleSScanAndAssertOnSizeOfResultSet(Jedis jedis,

Review comment:
       This method is never used and should be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {

Review comment:
       > This test is flawed, as there is no such thing as an invalid syntax for glob-style patterns. This test is just testing that given a non-matching pattern, an empty array is returned, so the name should reflect that. In fact, if the test is modified to the below, then it fails, as the element `p` matches and is returned:
   > 
   > ```
   >   @Test
   >   public void givenInvalidRegexSyntax_returnsEmptyArray() {
   >     jedis.sadd(KEY, "\\p", "p");
   >     ScanParams scanParams = new ScanParams();
   >     scanParams.count(10);
   >     scanParams.match("\\p");
   > 
   >     ScanResult<byte[]> result =
   >         jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
   > 
   >     assertThat(result.getResult()).isEmpty();
   >   }
   > ```
   
   This comment still applies. The test should be renamed to something along the lines of "givenNonMatchingPattern_returnsEmptyResult()" and the matching pattern changed to something without backslashes in it, as they just complicate things unnecessarily.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +215,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void negativeCursor_doesNotError() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsFewMembers() {
+    final List<String> initialMemberData = makeSet();

Review comment:
       Since we're explicitly dealing with sets, `makeSet()` should probably return a Set rather than a List.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +215,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void negativeCursor_doesNotError() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsFewMembers() {

Review comment:
       This test might be better named something like "givenSetWithMultipleMembers_returnsSubsetOfMembers"

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +215,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void negativeCursor_doesNotError() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }

Review comment:
       Given that this test is just intending to show that a negative cursor value does not cause an error (behaviour with a negative cursor is undefined) we should only be asserting that no error is seen. This test should therefore be just:
   ```
     @Test
     public void negativeCursor_doesNotError() {
       initializeThreeMemberSet();
   
       assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-100"));
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -55,95 +73,136 @@ public void tearDown() {
 
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }

Review comment:
       These tests should be replaced with a single test, "givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError":
   ```
     @Test
     public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
       assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {

Review comment:
       This test should be changed to:
   ```
     @Test
     public void should_notErrorGivenNonzeroCursorOnFirstCall() {
       initializeThreeMemberSet();
   
       assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "5"));
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));

Review comment:
       This should be `jedis.sadd(KEY, initialMemberData.toArray(new String[0]));`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {

Review comment:
       For consistency, this should be "should_notReturnValue_givenValueWasRemovedBeforeSscanIsCalled"

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +215,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void negativeCursor_doesNotError() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsFewMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));

Review comment:
       This should be:
   ```
   jedis.sadd(KEY, initialMemberData.toArray(new String[0]));
   ```
   From the IntelliJ inspection description:
   >In older Java versions using pre-sized array was recommended, as the reflection call which is necessary to create an array of proper size was quite slow. However since late updates of OpenJDK 6 this call was intrinsified, making the performance of the empty array version the same and sometimes even better, compared to the pre-sized version. Also passing pre-sized array is dangerous for a concurrent or synchronized collection as a data race is possible between the size and toArray call which may result in extra nulls at the end of the array, if the collection was concurrently shrunk during the operation.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));

Review comment:
       This should be `jedis.sadd(KEY, initialMemberData.toArray(new String[0]));`

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);

Review comment:
       To be more explicit, this assert should be changed to: `assertThat(result.getResult()).doesNotContain(FIELD_THREE);`
   
   This also renders the `set` variable redundant, so it can be removed (although the call to `initializeThreeMemberSet()` will have to stay).

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+
+    initialMemberData
+        .forEach((member) -> assertThat(jedis.sismember(KEY, member)).isTrue());
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  private void multipleSScanAndAssertOnContentOfResultSet(int iteration, Jedis jedis,
+      final List<String> initialMemberData) {
+
+    List<String> allEntries = new ArrayList<>();
+    ScanResult<String> result;
+    String cursor = ZERO_CURSOR;
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      cursor = result.getCursor();
+      List<String> resultEntries = result.getResult();
+      resultEntries
+          .forEach((entry) -> allEntries.add(entry));
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).as("failed on iteration " + iteration)
+        .containsAll(initialMemberData);
+  }
+
+  private void multipleSScanAndAssertOnSizeOfResultSet(Jedis jedis,
+      final List<String> initialMemberData) {
+    List<String> allEntries = new ArrayList<>();
+    ScanResult<String> result;
+    String cursor = ZERO_CURSOR;
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      cursor = result.getCursor();
+      allEntries.addAll(result.getResult());
+    } while (!result.isCompleteIteration());
+
+    List<String> allDistinctEntries =
+        allEntries
+            .stream()
+            .distinct()
+            .collect(Collectors.toList());
+
+    assertThat(allDistinctEntries.size())
+        .isEqualTo(initialMemberData.size());
+  }
+
+  private List<String> initializeThreeMemberSet() {
+    List<String> set = new ArrayList<>();
+    set.add(FIELD_ONE);
+    set.add(FIELD_TWO);
+    set.add(FIELD_THREE);
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    return set;
+  }
+
+  private List<byte[]> initializeThreeMemberByteSet() {
+    List<byte[]> set = new ArrayList<>();
+    set.add(FIELD_ONE.getBytes());
+    set.add(FIELD_TWO.getBytes());
+    set.add(FIELD_THREE.getBytes());
+    jedis.sadd(KEY.getBytes(), FIELD_ONE.getBytes(), FIELD_TWO.getBytes(), FIELD_THREE.getBytes());
+    return set;
+  }
+
+  private List<String> makeSet() {
+    List<String> set = new ArrayList<>();
+    for (int i = 0; i < SIZE_OF_SET; i++) {
+      set.add((BASE_FIELD + i));
+    }
+    return set;
+  }
+
+  private List<byte[]> makeByteSet() {

Review comment:
       This method is never used and should be removed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+
+    initialMemberData
+        .forEach((member) -> assertThat(jedis.sismember(KEY, member)).isTrue());

Review comment:
       This would be better as:
   ```
   assertThat(jedis.smembers(KEY)).containsExactlyInAnyOrderElementsOf(initialMemberData);
   ```
   as we're currently only asserting that all the initial members are still present, but not that we haven't added any extra members somehow.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {

Review comment:
       > This test is flawed, as for small set sizes, native Redis will return all elements in one SSCAN regardless of the value of COUNT, and duplicate elements are allowed. A significant improvement would be to populate a set with 1000 elements, then call SSCAN once and assert that the number of elements returned is greater than or equal to the appropriate COUNT value and that they are a subset of the total set contents.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -53,97 +73,143 @@ public void tearDown() {
     jedis.close();
   }
 
+  @Test
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
   @Test
   public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
   public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {

Review comment:
       >This test name could be more accurate as "givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError" since the number of arguments isn't actually the relevant factor here, just that there is a syntax error of some kind.
   
   This comment still applies. This test name should be changed.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {

Review comment:
       > This test is flawed, as for small set sizes, native Redis will return all elements in one SSCAN regardless of the value of COUNT, and duplicate elements are allowed. A significant improvement would be to populate a set with 1000 elements, then call SSCAN once and assert that the number of elements returned is greater than or equal to the appropriate COUNT value and that they are a subset of the total set contents.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +402,204 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, NEGATIVE_LONG_CAPACITY.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("\\p");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.getResult()).isEmpty();
   }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSSCANISCalled() {
+    List<String> set = initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    set.remove(FIELD_THREE);
+
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    List<String> set = initializeThreeMemberSet();
+
+    ScanResult<String> result = jedis.sscan(KEY, "5");
+
+    assertThat(result.getResult()).isSubsetOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    List<byte[]> set = initializeThreeMemberByteSet();
+
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+    List<Object> result =
+        uncheckedCast(jedis.sendCommand(KEY.getBytes(), Protocol.Command.SSCAN,
+            KEY.getBytes(), ZERO_CURSOR.getBytes(),
+            "COUNT".getBytes(), greaterThanInt.getBytes()));
+
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+
+    List<byte[]> fields = uncheckedCast(result.get(1));
+    assertThat(fields).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String field = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, field);
+          jedis.srem(KEY, field);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final List<String> initialMemberData = makeSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[initialMemberData.size()]));
+    final int iterationCount = 500;
+
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+
+    initialMemberData
+        .forEach((member) -> assertThat(jedis.sismember(KEY, member)).isTrue());
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  private void multipleSScanAndAssertOnContentOfResultSet(int iteration, Jedis jedis,
+      final List<String> initialMemberData) {
+
+    List<String> allEntries = new ArrayList<>();
+    ScanResult<String> result;
+    String cursor = ZERO_CURSOR;
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      cursor = result.getCursor();
+      List<String> resultEntries = result.getResult();
+      resultEntries
+          .forEach((entry) -> allEntries.add(entry));

Review comment:
       This can be simplified to:
   ```
   allEntries.addAll(result.getResult());
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    List<Object> result =
+        (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR,
+            "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("1*");
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCountsAndMatches_returnsAllEntriesWithoutDuplicates() {

Review comment:
       > This test would be better named "givenSetWithThreeMembersAndMultipleMatchAndCountArguments_returnsAllMatchingMembers" with the assertions changed to `containsOnly()` to allow duplicate entries.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {

Review comment:
       > This test is also flawed for the same reasons as earlier tests. A better name would be "givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument" with the assertions changed to `containsOnly()` to allow duplicate entries.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {

Review comment:
       > This test is also flawed for the same reasons as earlier tests. A better name would be "givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements" with the assertions changed to `containsOnly()` to allow duplicate entries.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     List<Object> result;
 
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
       result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
+          (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, cursor, "COUNT",
+              FIELD_TWO,
+              "COUNT", FIELD_ONE);
       allEntries.addAll((List<byte[]>) result.get(1));
       cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+    } while (!Arrays.equals((byte[]) result.get(0), ZERO_CURSOR.getBytes()));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
     ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    List<Object> result =
+        (List<Object>) jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR,
+            "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat((byte[]) result.get(0)).isEqualTo(ZERO_CURSOR.getBytes());
+    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
   public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {

Review comment:
       > This test would be better named "givenSetWithThreeMembersAndMatchAndCount_returnsAllMatchingMembers" with the assertions changed to `containsOnly()` to allow duplicate entries.
   
   This comment still applies

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -323,26 +409,217 @@ public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
 
   @Test
   public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, UNSIGNED_LONG_CAPACITY.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {

Review comment:
       > It would be good to add tests that show the behaviour when the cursor value is outside the range `Long.MIN_VALUE > x > Long.MAX_VALUE`, and tests for the behaviour when the cursor in just inside that range.
   > 
   > The behaviour of geode-for-redis differs from native Redis here, as they accept cursor values up to `UNSIGNED_LONG_CAPACITY` but we only accept ones up to `Long.MAX_VALUE`, so in order to test this, the test cases for failing with values outside the range should be put in `SScanIntegrationTest` (not the Abstract parent class). Also, looking at that class, the test `givenDifferentCursorThanSpecifiedByPreviousSscan_returnsAllMembers` is wrong and should be removed.
   
   This comment still applies




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans merged pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans merged pull request #7278:
URL: https://github.com/apache/geode/pull/7278


   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



[GitHub] [geode] DonalEvans commented on a change in pull request #7278: GEODE-9835: Add SSCAN to Redis supported commands

Posted by GitBox <gi...@apache.org>.
DonalEvans commented on a change in pull request #7278:
URL: https://github.com/apache/geode/pull/7278#discussion_r793918352



##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/SScanIntegrationTest.java
##########
@@ -37,21 +36,29 @@ public int getPort() {
   }
 
   @Test
-  public void givenDifferentCursorThanSpecifiedByPreviousSscan_returnsAllMembers() {
-    List<byte[]> memberList = new ArrayList<>();
-    for (int i = 0; i < 10; i++) {
-      jedis.sadd("a", String.valueOf(i));
-      memberList.add(String.valueOf(i).getBytes());
-    }
-
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(5);
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
-    assertThat(result.isCompleteIteration()).isFalse();
-
-    result = jedis.sscan("a".getBytes(), "100".getBytes());
-
-    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(memberList);
+  public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {

Review comment:
       The test names in this class are inaccurate, as we're testing CURSOR values related to *signed* Long max values, not unsigned. A better naming format would be "LongMaxValue" or "LongMinValue" rather than "UnsignedLongCapacity".

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/SScanIntegrationTest.java
##########
@@ -37,21 +36,29 @@ public int getPort() {
   }
 
   @Test
-  public void givenDifferentCursorThanSpecifiedByPreviousSscan_returnsAllMembers() {
-    List<byte[]> memberList = new ArrayList<>();
-    for (int i = 0; i < 10; i++) {
-      jedis.sadd("a", String.valueOf(i));
-      memberList.add(String.valueOf(i).getBytes());
-    }
-
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(5);
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
-    assertThat(result.isCompleteIteration()).isFalse();
-
-    result = jedis.sscan("a".getBytes(), "100".getBytes());
-
-    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(memberList);
+  public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_MAX.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
+  }
+
+  @Test
+  public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_MIN.add(BigInteger.valueOf(-1)).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
+  }
+
+  @Test
+  public void givenCursorEqualToUnsignedLongCapacity_doesNotError() {
+    jedis.sadd(KEY, "1");
+    assertThatNoException()
+        .isThrownBy(() -> jedis.sscan(KEY, SIGNED_LONG_MAX.subtract(BigInteger.ONE).toString()));
+  }
+
+  @Test
+  public void givenNegativeCursorEqualToUnsignedLongCapacity_returnsCursorError() {
+    jedis.sadd(KEY, "1");
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, SIGNED_LONG_MAX.toString()));

Review comment:
       These tests should be asserting that CURSOR values equal to `Long.MAX_VALUE` (not that value -1) and `Long.MIN_VALUE` do not error, and should be named accordingly.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }

Review comment:
       This test should be deleted, because the behaviour in the test name does not match the behaviour in the test, and both behaviours are already covered by the "givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError" and "givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError" tests respectively.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {

Review comment:
       This test name is slightly misleading, as it's perfectly fine to not specify MATCH or COUNT when performing SSCAN. What this test is actually doing is checking the behaviour when an additional argument is given, but that argument is not MATCH or COUNT. A better name would therefore be something like "givenAdditionalArgumentNotEqualToMatchOrCount_returnsSyntaxError"

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.hset(KEY, "b", FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "not-int"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.set(KEY, "b");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
-
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 10;
     ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    scanParams.count(10);
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
-    List<Object> result;
-
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+      result = sendCustomSscanCommand(KEY, KEY, cursor, "COUNT", "1", "COUNT", "2");
+      cursor = result.getCursor();
+      allEntries.addAll(result.getResult());
+    } while (!cursor.equals(ZERO_CURSOR));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(initialMemberData);
   }
 
   @Test
-  public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+  public void givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR, "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getCursor()).isEqualTo(ZERO_CURSOR);
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeMembersAndMatchAndCount_returnsAllMatchingMembers() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("1*");
-    ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(allMembersFromScan).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleCountsAndMatches_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
-    List<Object> result;
+  public void givenSetWithThreeMembersAndMultipleMatchAndCountArguments_returnsAllMatchingMembers() {

Review comment:
       This test should be modified in a similar way to the one above, but with different COUNT values:
   ```
     @Test
     public void givenMultipleCountAndMatch_usesLastSpecified() {
       Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
   
       // Choose a large COUNT to ensure that some matching members are returned
       // There are 111 matching members in the set 0..999
       result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR,
           "COUNT", "20",
           "MATCH", "1*",
           "COUNT", "950",
           "MATCH", "9*");
   
       List<byte[]> returnedMembers = this.result.getResult();
       // We know that we must have found at least 61 matching members, given the size of COUNT and the
       // number of matching members in the set
       assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(61);
       assertThat(returnedMembers).isSubsetOf(initialMemberData);
       assertThat(returnedMembers).allSatisfy(bytes -> assertThat(new String(bytes)).startsWith("9"));
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,37 +14,57 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.data.KeyHashUtil;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
-  private static final int REDIS_CLIENT_TIMEOUT =
-      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  protected ScanResult<byte[]> result;
+  public static final String KEY = "key";
+  public static final int SLOT_FOR_KEY = KeyHashUtil.slotForKey(KEY.getBytes());
+  public static final String ZERO_CURSOR = "0";
+  public static final BigInteger SIGNED_LONG_MAX = new BigInteger(Long.toString(Long.MAX_VALUE));
+  public static final BigInteger SIGNED_LONG_MIN = new BigInteger(Long.toString(Long.MIN_VALUE));
+
+  public static final String FIELD_ONE = "1";
+  public static final String FIELD_TWO = "12";
+  public static final String FIELD_THREE = "3";
+
+  public static final String BASE_FIELD = "baseField_";

Review comment:
       Since Redis sets deal with members rather than fields and values, could these constants be renamed "MEMBER" instead of "FIELD" please?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -14,37 +14,57 @@
  */
 package org.apache.geode.redis.internal.commands.executor.set;
 
+import static org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_CURSOR;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_NOT_INTEGER;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_SYNTAX;
 import static org.apache.geode.redis.internal.RedisConstants.ERROR_WRONG_TYPE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatNoException;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
+import java.math.BigInteger;
 import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 import redis.clients.jedis.HostAndPort;
+import redis.clients.jedis.Jedis;
 import redis.clients.jedis.JedisCluster;
 import redis.clients.jedis.Protocol;
 import redis.clients.jedis.ScanParams;
 import redis.clients.jedis.ScanResult;
 
+import org.apache.geode.redis.ConcurrentLoopingThreads;
 import org.apache.geode.redis.RedisIntegrationTest;
+import org.apache.geode.redis.internal.data.KeyHashUtil;
 import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
 
 public abstract class AbstractSScanIntegrationTest implements RedisIntegrationTest {
   protected JedisCluster jedis;
-  private static final int REDIS_CLIENT_TIMEOUT =
-      Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+  protected ScanResult<byte[]> result;

Review comment:
       Rather than extracting this to a field and having to reset it in the `setUp()` method, it would be better to leave it as an instance variable in the tests that use it.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/SScanIntegrationTest.java
##########
@@ -37,21 +36,29 @@ public int getPort() {
   }
 
   @Test
-  public void givenDifferentCursorThanSpecifiedByPreviousSscan_returnsAllMembers() {
-    List<byte[]> memberList = new ArrayList<>();
-    for (int i = 0; i < 10; i++) {
-      jedis.sadd("a", String.valueOf(i));
-      memberList.add(String.valueOf(i).getBytes());
-    }
-
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(5);
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
-    assertThat(result.isCompleteIteration()).isFalse();
-
-    result = jedis.sscan("a".getBytes(), "100".getBytes());
-
-    assertThat(result.getResult()).containsExactlyInAnyOrderElementsOf(memberList);
+  public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_MAX.add(BigInteger.ONE).toString()))
+            .hasMessageContaining(ERROR_CURSOR);
+  }
+
+  @Test
+  public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
+    assertThatThrownBy(
+        () -> jedis.sscan(KEY, SIGNED_LONG_MIN.add(BigInteger.valueOf(-1)).toString()))

Review comment:
       For consistency with the test above, could this be `SIGNED_LONG_MIN.subtract(BigInteger.ONE)`?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {

Review comment:
       This test should probably be named "givenKeyIsNotASetAndCountIsNegative_returnsWrongTypeError". It would also be good to add another test before this one where the key is not a set but COUNT is not specified named "givenKeyIsNotASet_returnsWrongTypeError" just to cover all our bases.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.hset(KEY, "b", FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "not-int"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.set(KEY, "b");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
-
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 10;
     ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    scanParams.count(10);
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
-    List<Object> result;
-
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+      result = sendCustomSscanCommand(KEY, KEY, cursor, "COUNT", "1", "COUNT", "2");
+      cursor = result.getCursor();
+      allEntries.addAll(result.getResult());
+    } while (!cursor.equals(ZERO_CURSOR));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(initialMemberData);
   }
 
   @Test
-  public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+  public void givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR, "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getCursor()).isEqualTo(ZERO_CURSOR);
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeMembersAndMatchAndCount_returnsAllMatchingMembers() {

Review comment:
       Tests using both MATCH and COUNT are tricky. The way SSCAN works, first COUNT members are scanned, then the matching members (if any) from that list are returned, which means that it's entirely possible that 0 members are returned by a single SSCAN even if COUNT is not small and there are matching members in the set.
   
   As such, it's difficult to know what to assert in a test for both COUNT and MATCH that's actually meaningful, as the number of members returned could be anything between 0 and every matching member (depending on hash collisions resulting in more than COUNT members being scanned in a single scan). I think the best bet would be to have a test that shows that a very large COUNT results in almost all of the matching members in the set being returned, to show that we didn't use the default value of COUNT, and that we filtered the scanned elements by MATCH:
   ```
     @Test
     public void givenLargeCountAndMatch_returnsOnlyMatchingMembers() {
       Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
   
       ScanParams scanParams = new ScanParams();
       // There are 111 matching members in the set 0..999
       scanParams.match("9*");
       // Choose a large COUNT to ensure that some matching members are returned
       scanParams.count(950);
       result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
   
       List<byte[]> returnedMembers = this.result.getResult();
       // We know that we must have found at least 61 matching members, given the size of COUNT and the
       // number of matching members in the set
       assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(61);
       assertThat(returnedMembers).isSubsetOf(initialMemberData);
       assertThat(returnedMembers).allSatisfy(bytes -> assertThat(new String(bytes)).startsWith("9"));
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))

Review comment:
       Just to avoid potential confusion, could the argument after "COUNT" here be "notAnInteger" or some similar String?

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -156,152 +222,172 @@ public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError(
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
+    jedis.set(KEY, "b");
 
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "sjfls"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
     assertThat(result.getResult()).isEmpty();
   }
 
+  @Test
+  public void givenNegativeCursor_returnsEntriesUsingAbsoluteValueOfCursor() {
+    List<String> set = initializeThreeMemberSet();
+
+    String cursor = "-100";
+    ScanResult<String> result;
+    List<String> allEntries = new ArrayList<>();
+
+    do {
+      result = jedis.sscan(KEY, cursor);
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!result.isCompleteIteration());
+
+    assertThat(allEntries).hasSize(3);
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(set);
+  }
+
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsExactly(FIELD_ONE);
   }
 
   @Test
   public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    assertThat(result.getResult()).containsExactlyInAnyOrder(FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
     ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(allMembersFromScan).containsExactlyInAnyOrder(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
   }
 
   @Test
   @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {

Review comment:
       This test still needs to be improved by only calling SSCAN with multiple COUNT arguments once instead of for a full iteration of the set, then asserting that the last COUNT value was the one used. The test name should be also updated to reflect the behaviour being tested:
   ```
     @Test
     public void givenMultipleCounts_usesLastCountSpecified() {
       Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
   
       // Choose two COUNT arguments with a large difference, so that it's extremely unlikely that if
       // the first COUNT is used, a number of members greater than or equal to the second COUNT will
       // be returned.
       int firstCount = 1;
       int secondCount = 500;
   
       result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR,
           "COUNT", String.valueOf(firstCount),
           "COUNT", String.valueOf(secondCount));
   
       List<byte[]> returnedMembers = this.result.getResult();
       assertThat(returnedMembers.size()).isGreaterThanOrEqualTo(secondCount);
   
       assertThat(returnedMembers).isSubsetOf(initialMemberData);
     }
   ```

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.hset(KEY, "b", FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "not-int"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.set(KEY, "b");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
-
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 10;

Review comment:
       The default value of COUNT for SSCAN is 10, so it would be good to use a different, larger, value here, to make sure that the value of COUNT is correctly being picked up.

##########
File path: geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/set/AbstractSScanIntegrationTest.java
##########
@@ -54,295 +74,397 @@ public void tearDown() {
   }
 
   @Test
-  public void givenNoKeyArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN))
-        .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
+  public void givenLessThanTwoArguments_returnsWrongNumberOfArgumentsError() {
+    assertAtLeastNArgs(jedis, Protocol.Command.SSCAN, 2);
+  }
+
+  @Test
+  public void givenNonexistentKey_returnsEmptyArray() {
+    ScanResult<String> result = jedis.sscan("nonexistent", ZERO_CURSOR);
+
+    assertThat(result.isCompleteIteration()).isTrue();
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void givenNonexistentKeyAndIncorrectOptionalArguments_returnsEmptyArray() {
+    result = sendCustomSscanCommand("nonexistentKey", "nonexistentKey", ZERO_CURSOR, "ANY");
+    assertThat(result.getResult()).isEmpty();
   }
 
   @Test
-  public void givenNoCursorArgument_returnsWrongNumberOfArgumentsError() {
-    assertThatThrownBy(() -> jedis.sendCommand("key", Protocol.Command.SSCAN, "key"))
+  public void givenIncorrectOptionalArgumentAndKeyExists_returnsSyntaxError() {
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY))
         .hasMessageContaining("ERR wrong number of arguments for 'sscan' command");
   }
 
   @Test
-  public void givenArgumentsAreNotOddAndKeyExists_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*"))
+  public void givenIncorrectOptionalArgumentsAndKeyExists_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*"))
         .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenArgumentsAreNotOddAndKeyDoesNotExist_returnsEmptyArray() {
-    List<Object> result =
-        (List<Object>) jedis.sendCommand("key!", Protocol.Command.SSCAN, "key!", "0", "a*");
+  public void givenMatchArgumentWithoutPatternOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "MATCH"))
+            .hasMessageContaining(ERROR_SYNTAX);
+  }
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<Object>) result.get(1)).isEmpty();
+  @Test
+  public void givenCountArgumentWithoutNumberOnExistingKey_returnsSyntaxError() {
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenMatchOrCountKeywordNotSpecified_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "a*", "1"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "a*", "1"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "MATCH"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "MATCH"))
             .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsNotAnInteger_returnsNotIntegerError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "sjlfs", "COUNT", "1"))
-            .hasMessageContaining(ERROR_NOT_INTEGER);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "sjlfs", "COUNT", "1"))
+                .hasMessageContaining(ERROR_NOT_INTEGER);
   }
 
   @Test
   public void givenMultipleCounts_whenAnyCountParameterIsLessThanOne_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "2",
-        "COUNT", "0", "COUNT", "1"))
-            .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "12",
+            "COUNT", "0", "COUNT", "1"))
+                .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsZero_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "0"))
-        .hasMessageContaining(ERROR_SYNTAX);
+    jedis.sadd(KEY, "1");
+    assertThatThrownBy(
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "0"))
+            .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenCount_whenCountParameterIsNegative_returnsSyntaxError() {
-    jedis.sadd("a", "1");
-
+    jedis.sadd(KEY, "1");
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_SYNTAX);
   }
 
   @Test
   public void givenKeyIsNotASet_returnsWrongTypeError() {
-    jedis.hset("a", "b", "1");
-
+    jedis.hset(KEY, "b", FIELD_ONE);
     assertThatThrownBy(
-        () -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0", "COUNT", "-37"))
+        () -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, ZERO_CURSOR, "COUNT", "-37"))
             .hasMessageContaining(ERROR_WRONG_TYPE);
   }
 
   @Test
   public void givenKeyIsNotASet_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.hset("a", "b", "1");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.hset(KEY, "b", FIELD_ONE);
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenNonexistentKey_andCursorIsNotInteger_returnsInvalidCursorError() {
     assertThatThrownBy(
-        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "sjfls"))
+        () -> jedis.sendCommand("notReal", Protocol.Command.SSCAN, "notReal", "notReal", "not-int"))
             .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
   public void givenExistentSetKey_andCursorIsNotAnInteger_returnsInvalidCursorError() {
-    jedis.set("a", "b");
-
-    assertThatThrownBy(() -> jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "sjfls"))
+    jedis.set(KEY, "b");
+    assertThatThrownBy(() -> jedis.sendCommand(KEY, Protocol.Command.SSCAN, KEY, "not-int"))
         .hasMessageContaining(ERROR_CURSOR);
   }
 
   @Test
-  public void givenNonexistentKey_returnsEmptyArray() {
-    ScanResult<String> result = jedis.sscan("nonexistent", "0");
-
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).isEmpty();
+  public void givenNegativeCursor_doesNotError() {
+    initializeThousandMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "-1"));
   }
 
   @Test
   public void givenSetWithOneMember_returnsMember() {
-    jedis.sadd("a", "1");
-    ScanResult<String> result = jedis.sscan("a", "0");
+    jedis.sadd(KEY, "1");
+
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactly("1");
+    assertThat(result.getResult()).containsOnly(FIELD_ONE);
   }
 
   @Test
-  public void givenSetWithMultipleMembers_returnsAllMembers() {
-    jedis.sadd("a", "1", "2", "3");
-    ScanResult<String> result = jedis.sscan("a", "0");
+  public void givenSetWithMultipleMembers_returnsSubsetOfMembers() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
 
-    assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1", "2", "3");
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).isSubsetOf(initialMemberData);
   }
 
   @Test
   public void givenCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "2", "3");
-
+    Set<byte[]> initialTotalSet = initializeThousandMemberByteSet();
+    int count = 10;
     ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    String cursor = "0";
-    ScanResult<byte[]> result;
-    List<byte[]> allMembersFromScan = new ArrayList<>();
+    scanParams.count(10);
 
-    do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
-      allMembersFromScan.addAll(result.getResult());
-      cursor = result.getCursor();
-    } while (!result.isCompleteIteration());
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "2".getBytes(),
-        "3".getBytes());
+    assertThat(result.getResult().size()).isGreaterThanOrEqualTo(count);
+    assertThat(result.getResult()).isSubsetOf(initialTotalSet);
   }
 
   @Test
-  @SuppressWarnings("unchecked")
   public void givenMultipleCounts_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
-    List<Object> result;
-
+    Set<byte[]> initialMemberData = initializeThousandMemberByteSet();
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "2",
-              "COUNT", "1");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+      result = sendCustomSscanCommand(KEY, KEY, cursor, "COUNT", "1", "COUNT", "2");
+      cursor = result.getCursor();
+      allEntries.addAll(result.getResult());
+    } while (!cursor.equals(ZERO_CURSOR));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes(),
-        "3".getBytes());
+    assertThat(allEntries).containsExactlyInAnyOrderElementsOf(initialMemberData);
   }
 
   @Test
-  public void givenMatch_returnsAllMatchingMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeEntriesAndMatch_returnsOnlyMatchingElements() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     ScanParams scanParams = new ScanParams();
     scanParams.match("1*");
 
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+    result = jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
 
     assertThat(result.isCompleteIteration()).isTrue();
-    assertThat(result.getResult()).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleMatches_returnsMembersMatchingLastMatchParameter() {
-    jedis.sadd("a", "1", "12", "3");
+  public void givenSetWithThreeEntriesAndMultipleMatchArguments_returnsOnlyElementsMatchingLastMatchArgument() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
 
-    List<Object> result = (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", "0",
-        "MATCH", "3*", "MATCH", "1*");
+    result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR, "MATCH", "3*", "MATCH", "1*");
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat((List<byte[]>) result.get(1)).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(result.getCursor()).isEqualTo(ZERO_CURSOR);
+    assertThat(result.getResult()).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  public void givenMatchAndCount_returnsAllMembersWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
+  public void givenSetWithThreeMembersAndMatchAndCount_returnsAllMatchingMembers() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     ScanParams scanParams = new ScanParams();
     scanParams.count(1);
     scanParams.match("1*");
-    ScanResult<byte[]> result;
     List<byte[]> allMembersFromScan = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result = jedis.sscan("a".getBytes(), cursor.getBytes(), scanParams);
+      result = jedis.sscan(KEY.getBytes(), cursor.getBytes(), scanParams);
       allMembersFromScan.addAll(result.getResult());
       cursor = result.getCursor();
     } while (!result.isCompleteIteration());
 
-    assertThat(allMembersFromScan).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(allMembersFromScan).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  @SuppressWarnings("unchecked")
-  public void givenMultipleCountsAndMatches_returnsAllEntriesWithoutDuplicates() {
-    jedis.sadd("a", "1", "12", "3");
-
-    List<Object> result;
+  public void givenSetWithThreeMembersAndMultipleMatchAndCountArguments_returnsAllMatchingMembers() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
     List<byte[]> allEntries = new ArrayList<>();
-    String cursor = "0";
+    String cursor = ZERO_CURSOR;
 
     do {
-      result =
-          (List<Object>) jedis.sendCommand("a", Protocol.Command.SSCAN, "a", cursor, "COUNT", "37",
-              "MATCH", "3*", "COUNT", "2", "COUNT", "1", "MATCH", "1*");
-      allEntries.addAll((List<byte[]>) result.get(1));
-      cursor = new String((byte[]) result.get(0));
-    } while (!Arrays.equals((byte[]) result.get(0), "0".getBytes()));
+      result = sendCustomSscanCommand(KEY, KEY, cursor, "COUNT", "37", "MATCH", "3*", "COUNT",
+          FIELD_TWO, "COUNT", FIELD_ONE, "MATCH", "1*");
+      allEntries.addAll(result.getResult());
+      cursor = result.getCursor();
+    } while (!cursor.equals(ZERO_CURSOR));
 
-    assertThat((byte[]) result.get(0)).isEqualTo("0".getBytes());
-    assertThat(allEntries).containsExactlyInAnyOrder("1".getBytes(),
-        "12".getBytes());
+    assertThat(allEntries).containsOnly(FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes());
   }
 
   @Test
-  public void givenNegativeCursor_returnsMembersUsingAbsoluteValueOfCursor() {
-    jedis.sadd("b", "green", "orange", "yellow");
+  public void givenNonMatchingPattern_returnsEmptyResult() {
+    jedis.sadd(KEY, "cat dog emu");
+    ScanParams scanParams = new ScanParams();
+    scanParams.match("*fish*");
 
-    List<String> allEntries = new ArrayList<>();
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+
+    assertThat(result.getResult()).isEmpty();
+  }
+
+  @Test
+  public void should_notReturnValue_givenValueWasRemovedBeforeSscanIsCalled() {
+    initializeThreeMemberSet();
+
+    jedis.srem(KEY, FIELD_THREE);
+    GeodeAwaitility.await().untilAsserted(
+        () -> assertThat(jedis.sismember(KEY, FIELD_THREE)).isFalse());
+    ScanResult<String> result = jedis.sscan(KEY, ZERO_CURSOR);
+
+    assertThat(result.getResult()).doesNotContain(FIELD_THREE);
+  }
+
+  @Test
+  public void should_notErrorGivenNonzeroCursorOnFirstCall() {
+    initializeThreeMemberSet();
+    assertThatNoException().isThrownBy(() -> jedis.sscan(KEY, "5"));
+  }
+
+  @Test
+  public void should_notErrorGivenCountEqualToIntegerMaxValue() {
+    Set<byte[]> set = initializeThreeMemberByteSet();
+    ScanParams scanParams = new ScanParams().count(Integer.MAX_VALUE);
+
+    ScanResult<byte[]> result =
+        jedis.sscan(KEY.getBytes(), ZERO_CURSOR.getBytes(), scanParams);
+
+    assertThat(result.getResult())
+        .containsExactlyInAnyOrderElementsOf(set);
+  }
+
+  @Test
+  public void should_notErrorGivenCountGreaterThanIntegerMaxValue() {
+    initializeThreeMemberByteSet();
+    String greaterThanInt = String.valueOf(2L * Integer.MAX_VALUE);
+
+    result = sendCustomSscanCommand(KEY, KEY, ZERO_CURSOR, "COUNT", greaterThanInt);
+
+    assertThat(result.getCursor()).isEqualTo(ZERO_CURSOR);
+    assertThat(result.getResult()).containsExactlyInAnyOrder(
+        FIELD_ONE.getBytes(),
+        FIELD_TWO.getBytes(),
+        FIELD_THREE.getBytes());
+  }
+
+  /**** Concurrency ***/
+
+  @Test
+  public void should_returnAllConsistentlyPresentMembers_givenConcurrentThreadsAddingAndRemovingMembers() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
+    final int iterationCount = 500;
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData),
+        (i) -> {
+          String member = "new_" + BASE_FIELD + i;
+          jedis.sadd(KEY, member);
+          jedis.srem(KEY, member);
+        }).run();
+
+    jedis1.close();
+    jedis2.close();
+  }
+
+  @Test
+  public void should_notAlterUnderlyingData_givenMultipleConcurrentSscans() {
+    final Set<String> initialMemberData = initializeThousandMemberSet();
+    jedis.sadd(KEY, initialMemberData.toArray(new String[0]));
+    final int iterationCount = 500;
+    Jedis jedis1 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+    Jedis jedis2 = jedis.getConnectionFromSlot(SLOT_FOR_KEY);
+
+    new ConcurrentLoopingThreads(iterationCount,
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis1, initialMemberData),
+        (i) -> multipleSScanAndAssertOnContentOfResultSet(i, jedis2, initialMemberData))
+            .run();
+    assertThat(jedis.smembers(KEY)).containsExactlyInAnyOrderElementsOf(initialMemberData);
+
+    jedis1.close();
+    jedis2.close();
+  }
 
-    String cursor = "-100";
+  private void multipleSScanAndAssertOnContentOfResultSet(int iteration, Jedis jedis,
+      final Set<String> initialMemberData) {
+
+    List<String> allEntries = new ArrayList<>();
     ScanResult<String> result;
+    String cursor = ZERO_CURSOR;
+
     do {
-      result = jedis.sscan("b", cursor);
-      allEntries.addAll(result.getResult());
+      result = jedis.sscan(KEY, cursor);
       cursor = result.getCursor();
+      List<String> resultEntries = result.getResult();
+      allEntries.addAll(resultEntries);
     } while (!result.isCompleteIteration());
 
-    assertThat(allEntries).containsExactlyInAnyOrder("green", "orange", "yellow");
+    assertThat(allEntries).as("failed on iteration " + iteration)
+        .containsAll(initialMemberData);
   }
 
-  @Test
-  public void givenCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+  @SuppressWarnings("unchecked")
+  private ScanResult<byte[]> sendCustomSscanCommand(String key, String... args) {
+    List<Object> result = (List<Object>) (jedis.sendCommand(key, Protocol.Command.SSCAN, args));
+    return new ScanResult<>((byte[]) result.get(0), (List<byte[]>) result.get(1));
   }
 
-  @Test
-  public void givenNegativeCursorGreaterThanUnsignedLongCapacity_returnsCursorError() {
-    assertThatThrownBy(() -> jedis.sscan("a", "-18446744073709551616"))
-        .hasMessageContaining(ERROR_CURSOR);
+  private void initializeThreeMemberSet() {
+    jedis.sadd(KEY, FIELD_ONE, FIELD_TWO, FIELD_THREE);
   }
 
-  @Test
-  public void givenInvalidRegexSyntax_returnsEmptyArray() {
-    jedis.sadd("a", "1");
-    ScanParams scanParams = new ScanParams();
-    scanParams.count(1);
-    scanParams.match("\\p");
+  private Set<byte[]> initializeThreeMemberByteSet() {
+    Set<byte[]> set = new HashSet<>();
+    set.add(FIELD_ONE.getBytes());
+    set.add(FIELD_TWO.getBytes());
+    set.add(FIELD_THREE.getBytes());
+    jedis.sadd(KEY.getBytes(), FIELD_ONE.getBytes(), FIELD_TWO.getBytes(), FIELD_THREE.getBytes());
+    return set;
+  }
 
-    ScanResult<byte[]> result =
-        jedis.sscan("a".getBytes(), "0".getBytes(), scanParams);
+  private Set<String> initializeThousandMemberSet() {
+    Set<String> set = new HashSet<>();
+    int SIZE_OF_SET = 1000;

Review comment:
       This variable, and the one in the method below, should not be in all-caps, as that is reserved for static final constants. The [Geode Code Style Guide page](https://cwiki.apache.org/confluence/display/GEODE/Code+Style+Guide) on the wiki contains a link to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) which details the conventions we (try to) follow when writing code for Geode.




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: notifications-unsubscribe@geode.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org