diff --git a/native_tests/linux/tests/unit/type/zset.tcl b/native_tests/linux/tests/unit/type/zset.tcl index 8fb6fc576..79c3903f3 100644 --- a/native_tests/linux/tests/unit/type/zset.tcl +++ b/native_tests/linux/tests/unit/type/zset.tcl @@ -2391,24 +2391,23 @@ start_server {tags {"zset"}} { } } -# Don't support ZRandMember -# foreach {type contents} "listpack {1 a 2 b 3 c} skiplist {1 a 2 b 3 [randstring 70 90 alpha]}" { -# set original_max_value [lindex [r config get zset-max-ziplist-value] 1] -# r config set zset-max-ziplist-value 10 -# create_zset myzset $contents -# assert_encoding $type myzset -# -# test "ZRANDMEMBER - $type" { -# unset -nocomplain myzset -# array set myzset {} -# for {set i 0} {$i < 100} {incr i} { -# set key [r zrandmember myzset] -# set myzset($key) 1 -# } -# assert_equal [lsort [get_keys $contents]] [lsort [array names myzset]] -# } -# r config set zset-max-ziplist-value $original_max_value -# } + foreach {type contents} "listpack {1 a 2 b 3 c} skiplist {1 a 2 b 3 [randstring 70 90 alpha]}" { + set original_max_value [lindex [r config get zset-max-ziplist-value] 1] + r config set zset-max-ziplist-value 10 + create_zset myzset $contents + assert_encoding $type myzset + + test "ZRANDMEMBER - $type" { + unset -nocomplain myzset + array set myzset {} + for {set i 0} {$i < 100} {incr i} { + set key [r zrandmember myzset] + set myzset($key) 1 + } + assert_equal [lsort [get_keys $contents]] [lsort [array names myzset]] + } + r config set zset-max-ziplist-value $original_max_value + } # Don't support RESP3 # test "ZRANDMEMBER with RESP3" { @@ -2423,176 +2422,170 @@ start_server {tags {"zset"}} { # r hello 2 # } -# Don't support ZRandMember -# test "ZRANDMEMBER count of 0 is handled correctly" { -# r zrandmember myzset 0 -# } {} + test "ZRANDMEMBER count of 0 is handled correctly" { + r zrandmember myzset 0 + } {} -# Don't support ZRandMember -# test "ZRANDMEMBER with against non existing key" { -# r zrandmember nonexisting_key 100 -# } {} + test "ZRANDMEMBER with against non existing key" { + r zrandmember nonexisting_key 100 + } {} -# Don't support ZRandMember -# test "ZRANDMEMBER count overflow" { -# r zadd myzset 0 a -# assert_error {*value is out of range*} {r zrandmember myzset -9223372036854770000 withscores} -# assert_error {*value is out of range*} {r zrandmember myzset -9223372036854775808 withscores} -# assert_error {*value is out of range*} {r zrandmember myzset -9223372036854775808} -# } {} + test "ZRANDMEMBER count overflow" { + r zadd myzset 0 a + assert_error {*value is out of range*} {r zrandmember myzset -9223372036854770000 withscores} + assert_error {*value is out of range*} {r zrandmember myzset -9223372036854775808 withscores} + assert_error {*value is out of range*} {r zrandmember myzset -9223372036854775808} + } {} # Make sure we can distinguish between an empty array and a null response r readraw 1 -# Don't support ZRandMember -# test "ZRANDMEMBER count of 0 is handled correctly - emptyarray" { -# r zrandmember myzset 0 -# } {*0} + test "ZRANDMEMBER count of 0 is handled correctly - emptyarray" { + r zrandmember myzset 0 + } {*0} -# Don't support ZRandMember -# test "ZRANDMEMBER with against non existing key - emptyarray" { -# r zrandmember nonexisting_key 100 -# } {*0} + test "ZRANDMEMBER with against non existing key - emptyarray" { + r zrandmember nonexisting_key 100 + } {*0} r readraw 0 -# Don't support ZRandMember -# foreach {type contents} " -# skiplist {1 a 2 b 3 c 4 d 5 e 6 f 7 g 7 h 9 i 10 [randstring 70 90 alpha]} -# listpack {1 a 2 b 3 c 4 d 5 e 6 f 7 g 7 h 9 i 10 j} " { -# test "ZRANDMEMBER with - $type" { -# set original_max_value [lindex [r config get zset-max-ziplist-value] 1] -# r config set zset-max-ziplist-value 10 -# create_zset myzset $contents -# assert_encoding $type myzset -# -# # create a dict for easy lookup -# set mydict [dict create {*}[r zrange myzset 0 -1 withscores]] -# -# # We'll stress different parts of the code, see the implementation -# # of ZRANDMEMBER for more information, but basically there are -# # four different code paths. -# -# # PATH 1: Use negative count. -# -# # 1) Check that it returns repeated elements with and without values. -# # 2) Check that all the elements actually belong to the original zset. -# set res [r zrandmember myzset -20] -# assert_equal [llength $res] 20 -# check_member $mydict $res -# -# set res [r zrandmember myzset -1001] -# assert_equal [llength $res] 1001 -# check_member $mydict $res -# -# # again with WITHSCORES -# set res [r zrandmember myzset -20 withscores] -# assert_equal [llength $res] 40 -# check_member_and_score $mydict $res -# -# set res [r zrandmember myzset -1001 withscores] -# assert_equal [llength $res] 2002 -# check_member_and_score $mydict $res -# -# # Test random uniform distribution -# # df = 9, 40 means 0.00001 probability -# set res [r zrandmember myzset -1000] -# assert_lessthan [chi_square_value $res] 40 -# check_member $mydict $res -# -# # 3) Check that eventually all the elements are returned. -# # Use both WITHSCORES and without -# unset -nocomplain auxset -# set iterations 1000 -# while {$iterations != 0} { -# incr iterations -1 -# if {[expr {$iterations % 2}] == 0} { -# set res [r zrandmember myzset -3 withscores] -# foreach {key val} $res { -# dict append auxset $key $val -# } -# } else { -# set res [r zrandmember myzset -3] -# foreach key $res { -# dict append auxset $key -# } -# } -# if {[lsort [dict keys $mydict]] eq -# [lsort [dict keys $auxset]]} { -# break; -# } -# } -# assert {$iterations != 0} -# -# # PATH 2: positive count (unique behavior) with requested size -# # equal or greater than set size. -# foreach size {10 20} { -# set res [r zrandmember myzset $size] -# assert_equal [llength $res] 10 -# assert_equal [lsort $res] [lsort [dict keys $mydict]] -# check_member $mydict $res -# -# # again with WITHSCORES -# set res [r zrandmember myzset $size withscores] -# assert_equal [llength $res] 20 -# assert_equal [lsort $res] [lsort $mydict] -# check_member_and_score $mydict $res -# } -# -# # PATH 3: Ask almost as elements as there are in the set. -# # In this case the implementation will duplicate the original -# # set and will remove random elements up to the requested size. -# # -# # PATH 4: Ask a number of elements definitely smaller than -# # the set size. -# # -# # We can test both the code paths just changing the size but -# # using the same code. -# foreach size {1 2 8} { -# # 1) Check that all the elements actually belong to the -# # original set. -# set res [r zrandmember myzset $size] -# assert_equal [llength $res] $size -# check_member $mydict $res -# -# # again with WITHSCORES -# set res [r zrandmember myzset $size withscores] -# assert_equal [llength $res] [expr {$size * 2}] -# check_member_and_score $mydict $res -# -# # 2) Check that eventually all the elements are returned. -# # Use both WITHSCORES and without -# unset -nocomplain auxset -# unset -nocomplain allkey -# set iterations [expr {1000 / $size}] -# set all_ele_return false -# while {$iterations != 0} { -# incr iterations -1 -# if {[expr {$iterations % 2}] == 0} { -# set res [r zrandmember myzset $size withscores] -# foreach {key value} $res { -# dict append auxset $key $value -# lappend allkey $key -# } -# } else { -# set res [r zrandmember myzset $size] -# foreach key $res { -# dict append auxset $key -# lappend allkey $key -# } -# } -# if {[lsort [dict keys $mydict]] eq -# [lsort [dict keys $auxset]]} { -# set all_ele_return true -# } -# } -# assert_equal $all_ele_return true -# # df = 9, 40 means 0.00001 probability -# assert_lessthan [chi_square_value $allkey] 40 -# } -# } -# r config set zset-max-ziplist-value $original_max_value -# } + foreach {type contents} " + skiplist {1 a 2 b 3 c 4 d 5 e 6 f 7 g 7 h 9 i 10 [randstring 70 90 alpha]} + listpack {1 a 2 b 3 c 4 d 5 e 6 f 7 g 7 h 9 i 10 j} " { + test "ZRANDMEMBER with - $type" { + set original_max_value [lindex [r config get zset-max-ziplist-value] 1] + r config set zset-max-ziplist-value 10 + create_zset myzset $contents + assert_encoding $type myzset + + # create a dict for easy lookup + set mydict [dict create {*}[r zrange myzset 0 -1 withscores]] + + # We'll stress different parts of the code, see the implementation + # of ZRANDMEMBER for more information, but basically there are + # four different code paths. + + # PATH 1: Use negative count. + + # 1) Check that it returns repeated elements with and without values. + # 2) Check that all the elements actually belong to the original zset. + set res [r zrandmember myzset -20] + assert_equal [llength $res] 20 + check_member $mydict $res + + set res [r zrandmember myzset -1001] + assert_equal [llength $res] 1001 + check_member $mydict $res + + # again with WITHSCORES + set res [r zrandmember myzset -20 withscores] + assert_equal [llength $res] 40 + check_member_and_score $mydict $res + + set res [r zrandmember myzset -1001 withscores] + assert_equal [llength $res] 2002 + check_member_and_score $mydict $res + + # Test random uniform distribution + # df = 9, 40 means 0.00001 probability + set res [r zrandmember myzset -1000] + assert_lessthan [chi_square_value $res] 40 + check_member $mydict $res + + # 3) Check that eventually all the elements are returned. + # Use both WITHSCORES and without + unset -nocomplain auxset + set iterations 1000 + while {$iterations != 0} { + incr iterations -1 + if {[expr {$iterations % 2}] == 0} { + set res [r zrandmember myzset -3 withscores] + foreach {key val} $res { + dict append auxset $key $val + } + } else { + set res [r zrandmember myzset -3] + foreach key $res { + dict append auxset $key + } + } + if {[lsort [dict keys $mydict]] eq + [lsort [dict keys $auxset]]} { + break; + } + } + assert {$iterations != 0} + + # PATH 2: positive count (unique behavior) with requested size + # equal or greater than set size. + foreach size {10 20} { + set res [r zrandmember myzset $size] + assert_equal [llength $res] 10 + assert_equal [lsort $res] [lsort [dict keys $mydict]] + check_member $mydict $res + + # again with WITHSCORES + set res [r zrandmember myzset $size withscores] + assert_equal [llength $res] 20 + assert_equal [lsort $res] [lsort $mydict] + check_member_and_score $mydict $res + } + + # PATH 3: Ask almost as elements as there are in the set. + # In this case the implementation will duplicate the original + # set and will remove random elements up to the requested size. + # + # PATH 4: Ask a number of elements definitely smaller than + # the set size. + # + # We can test both the code paths just changing the size but + # using the same code. + foreach size {1 2 8} { + # 1) Check that all the elements actually belong to the + # original set. + set res [r zrandmember myzset $size] + assert_equal [llength $res] $size + check_member $mydict $res + + # again with WITHSCORES + set res [r zrandmember myzset $size withscores] + assert_equal [llength $res] [expr {$size * 2}] + check_member_and_score $mydict $res + + # 2) Check that eventually all the elements are returned. + # Use both WITHSCORES and without + unset -nocomplain auxset + unset -nocomplain allkey + set iterations [expr {1000 / $size}] + set all_ele_return false + while {$iterations != 0} { + incr iterations -1 + if {[expr {$iterations % 2}] == 0} { + set res [r zrandmember myzset $size withscores] + foreach {key value} $res { + dict append auxset $key $value + lappend allkey $key + } + } else { + set res [r zrandmember myzset $size] + foreach key $res { + dict append auxset $key + lappend allkey $key + } + } + if {[lsort [dict keys $mydict]] eq + [lsort [dict keys $auxset]]} { + set all_ele_return true + } + } + assert_equal $all_ele_return true + # df = 9, 40 means 0.00001 probability + assert_lessthan [chi_square_value $allkey] 40 + } + } + r config set zset-max-ziplist-value $original_max_value + } test {zset score double range} { set dblmax 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.00000000000000000 diff --git a/src/main/java/com/github/fppt/jedismock/Utils.java b/src/main/java/com/github/fppt/jedismock/Utils.java index fc9bac7d6..51e290803 100644 --- a/src/main/java/com/github/fppt/jedismock/Utils.java +++ b/src/main/java/com/github/fppt/jedismock/Utils.java @@ -3,6 +3,12 @@ import com.github.fppt.jedismock.exception.WrongValueTypeException; import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; /** * Created by Xiaolu on 2015/4/21. @@ -91,6 +97,54 @@ public static String createRegexFromGlob(String glob) { return out.toString(); } + /** + * Performs reservoir sampling on a given collection, returning a random subset of up to {@code count} elements. + *

+ * This implementation uses Algorithm L (a fast reservoir sampling method) to efficiently sample elements + * with uniform probability without needing to store or shuffle the entire dataset. + * + * @param collection the input collection to sample from + * @param count the number of elements to sample; must be non-negative and less than or equal to the size of the collection + * @param random a {@link Random} instance used for generating random numbers + * @param the type of elements in the input collection + * @return a list containing up to {@code count} randomly sampled elements from the collection + * @throws IllegalArgumentException if {@code count} is negative + */ + public static List reservoirSampling(Collection collection, int count, Random random) { + if (count < 0) { + throw new IllegalArgumentException("count must be non-negative"); + } + + if (count == 0) { + return Collections.emptyList(); + } + + if (count > collection.size()) { + count = collection.size(); + } + + List result = new ArrayList<>(count); + Iterator iter = collection.iterator(); + for (int i = 0; i < count; i++) { + result.add(iter.next()); + } + + double W = Math.exp(Math.log(random.nextDouble()) / count); + while (iter.hasNext()) { + int skip = (int)Math.floor(Math.log(random.nextDouble()) / Math.log(1 - W)); + for (int j = 0; j < skip; j++) { + if (!iter.hasNext()) break; + iter.next(); + } + if (iter.hasNext()) { + result.set(random.nextInt(count), iter.next()); + W = W * Math.exp(Math.log(random.nextDouble()) / count); + } + } + + return result; + } + public static long toNanoTimeout(String value) { return (long) (convertToDouble(value) * 1_000_000_000L); } diff --git a/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java index 029af43e1..fd8bc3624 100644 --- a/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java +++ b/src/main/java/com/github/fppt/jedismock/operations/sets/SRandMember.java @@ -1,19 +1,22 @@ package com.github.fppt.jedismock.operations.sets; +import com.github.fppt.jedismock.Utils; import com.github.fppt.jedismock.datastructures.RMSet; import com.github.fppt.jedismock.datastructures.Slice; -import com.github.fppt.jedismock.exception.WrongValueTypeException; import com.github.fppt.jedismock.operations.AbstractRedisOperation; import com.github.fppt.jedismock.operations.RedisCommand; import com.github.fppt.jedismock.server.Response; import com.github.fppt.jedismock.storage.RedisBase; import java.util.ArrayList; -import java.util.Collections; import java.util.List; +import java.util.Random; +import java.util.Set; import java.util.concurrent.ThreadLocalRandom; import java.util.stream.Collectors; +import static com.github.fppt.jedismock.Utils.reservoirSampling; + @RedisCommand("srandmember") public class SRandMember extends AbstractRedisOperation { public SRandMember(RedisBase base, List params) { @@ -28,48 +31,27 @@ protected int minArgs() { @Override protected Slice response() { RMSet set = base().getSet(params().get(0)); - int number; - if (params().size() > 1) { - if (set == null) { - return Response.EMPTY_ARRAY; - } - int result; - String value = params().get(1).toString(); - try { - result = Integer.parseInt(value); - } catch (NumberFormatException e) { - throw new WrongValueTypeException("ERR value is out of range"); - } - number = result; - } else { - if (set == null) { - return Response.NULL; - } - number = 1; + boolean isArrayResponse = params().size() > 1; + + int number = isArrayResponse ? Utils.convertToInteger(params().get(1).toString()) : 1; + if (set == null) { + return isArrayResponse ? Response.EMPTY_ARRAY : Response.NULL; } - // TODO: more effective algorithms should be used here, - // avoiding conversion of set to list, shuffling all the elements etc. - List list = new ArrayList<>(set.getStoredData()); - if (number == 1) { - int index = ThreadLocalRandom.current().nextInt(list.size()); - return params().size() > 1 ? - Response.array(Response.bulkString(list.get(index))) : - Response.bulkString(list.get(index)); - } else if (number > 1) { - Collections.shuffle(list); - return Response.array( - list.stream() - .map(Response::bulkString) - .limit(number) - .collect(Collectors.toList())); + List result = selectEntries(set.getStoredData(), number, ThreadLocalRandom.current()); + return isArrayResponse ? + Response.array(result.stream().map(Response::bulkString).collect(Collectors.toList())) : + Response.bulkString(result.get(0)); + } + + private List selectEntries(Set set, int count, Random random) { + if (count > 0) { + return reservoirSampling(set, count, random); } else { - List result = - ThreadLocalRandom.current().ints(-number, 0, list.size()) - .mapToObj(list::get) - .map(Response::bulkString) - .collect(Collectors.toList()); - return Response.array(result); + List list = new ArrayList<>(set); + return ThreadLocalRandom.current().ints(-count, 0, list.size()) + .mapToObj(list::get) + .collect(Collectors.toList()); } } } diff --git a/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRandMember.java b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRandMember.java new file mode 100644 index 000000000..0d726b6e9 --- /dev/null +++ b/src/main/java/com/github/fppt/jedismock/operations/sortedsets/ZRandMember.java @@ -0,0 +1,102 @@ +package com.github.fppt.jedismock.operations.sortedsets; + +import com.github.fppt.jedismock.Utils; +import com.github.fppt.jedismock.datastructures.RMZSet; +import com.github.fppt.jedismock.datastructures.Slice; +import com.github.fppt.jedismock.datastructures.ZSetEntry; +import com.github.fppt.jedismock.exception.WrongValueTypeException; +import com.github.fppt.jedismock.operations.AbstractRedisOperation; +import com.github.fppt.jedismock.operations.RedisCommand; +import com.github.fppt.jedismock.server.Response; +import com.github.fppt.jedismock.storage.RedisBase; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.github.fppt.jedismock.Utils.reservoirSampling; + + +@RedisCommand("zrandmember") +public class ZRandMember extends AbstractRedisOperation { + + public ZRandMember(RedisBase base, List params) { + super(base, params); + } + + @Override + protected int minArgs() { + return 1; + } + + @Override + protected int maxArgs() { + return 3; + } + + @Override + protected Slice response() { + RMZSet set = base().getZSet(params().get(0)); + boolean isArrayResponse = params().size() > 1; + + int count = parseCount(); + boolean withScores = parseWithScores(); + if (set == null) { + return isArrayResponse ? Response.EMPTY_ARRAY : Response.NULL; + } + + List selectedEntries = selectEntries(set, count); + return isArrayResponse + ? buildArrayResponse(selectedEntries, withScores) + : buildSingleResponse(selectedEntries.get(0)); + } + + private int parseCount() { + if (params().size() >= 2) { + return Utils.convertToInteger(params().get(1).toString()); + } + return 1; + } + + private boolean parseWithScores() { + if (params().size() == 3) { + if ("withscores".equalsIgnoreCase(params().get(2).toString())) { + return true; + } else { + throw new WrongValueTypeException("ERR syntax error"); + } + } + return false; + } + + private List selectEntries(RMZSet set, int count) { + if (count > 0) { + return reservoirSampling(set.entries(false), count, ThreadLocalRandom.current()); + } else { + List entries = new ArrayList<>(set.entries(false)); + return ThreadLocalRandom.current() + .ints(-count, 0, entries.size()) + .mapToObj(entries::get) + .collect(Collectors.toList()); + } + } + + private Slice buildArrayResponse(List entries, boolean withScores) { + return Response.array(entries.stream() + .flatMap(e -> withScores + ? Stream.of(e.getValue(), Slice.create(String.valueOf(Math.round(e.getScore())))) + : Stream.of(e.getValue())) + .map(Response::bulkString) + .collect(Collectors.toList())); + } + + private Slice buildSingleResponse(ZSetEntry entry) { + return Response.bulkString(entry.getValue()); + } +} diff --git a/src/test/java/com/github/fppt/jedismock/TestUtils.java b/src/test/java/com/github/fppt/jedismock/TestUtils.java index 5bbafc1ba..331a7d86a 100644 --- a/src/test/java/com/github/fppt/jedismock/TestUtils.java +++ b/src/test/java/com/github/fppt/jedismock/TestUtils.java @@ -4,6 +4,15 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; /** * Created by Xiaolu on 2015/4/20. @@ -26,4 +35,77 @@ public void close() throws IOException { }); } + @Test + void testReservoirSamplingBasicFunctionality() { + List input = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + int count = 5; + + List sample = Utils.reservoirSampling(input, count, new Random()); + Set sampleSet = new HashSet<>(sample); + + assertThat(sample).hasSize(count); + assertThat(sampleSet).hasSize(count); + assertThat(input).containsAll(sample); + } + + @Test + void testReservoirSamplingWithCountGreaterThanCollection() { + List input = Arrays.asList(1, 2, 3); + int count = 10; + + List sample = Utils.reservoirSampling(input, count, new Random()); + Set sampleSet = new HashSet<>(sample); + + assertThat(sample).hasSize(input.size()); + assertThat(sampleSet).hasSize(input.size()); + assertThat(input).containsAll(sample); + } + + @Test + void testReservoirSamplingWithExactCount() { + List input = Arrays.asList(1, 2, 3); + int count = 3; + + List sample = Utils.reservoirSampling(input, count, new Random()); + Set sampleSet = new HashSet<>(sample); + + assertThat(sample).hasSize(input.size()); + assertThat(sampleSet).hasSize(input.size()); + assertThat(input).containsAll(sample); + } + + @Test + void testReservoirSamplingWithZeroCount() { + List input = Arrays.asList(1, 2, 3); + int count = 0; + + List sample = Utils.reservoirSampling(input, count, new Random()); + + assertThat(sample).isEmpty(); + } + + @Test + void testReservoirSamplingWithNegativeCount() { + List input = Arrays.asList(1, 2, 3); + int count = -1; + + Throwable throwable = catchThrowable(() -> Utils.reservoirSampling(input, count, new Random())); + + assertThat(throwable).isInstanceOf(IllegalArgumentException.class); + } + + @Test + void testReservoirSamplingDeterminismWithFixedSeed() { + List input = new ArrayList<>(); + for (int i = 0; i < 100; i++) input.add(i); + int count = 10; + Random random1 = new Random(123); + Random random2 = new Random(123); + + List sample1 = Utils.reservoirSampling(input, count, random1); + List sample2 = Utils.reservoirSampling(input, count, random2); + + assertThat(sample1).isEqualTo(sample2); + } + } diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java index 95b2cba7f..97e7e8fcc 100644 --- a/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java +++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sets/SRandMemberTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.TestTemplate; import org.junit.jupiter.api.extension.ExtendWith; import redis.clients.jedis.Jedis; +import redis.clients.jedis.Protocol; import java.util.Arrays; import java.util.Collection; @@ -102,4 +103,13 @@ void randMemberReturnOneElementAsSingletonList(Jedis jedis) { assertThat(jedis.srandmember("key", 0)).isEmpty(); assertThat(jedis.srandmember("key", -1)).containsExactly("a"); } + + @TestTemplate + void randMemberWithInvalidArgumentAndNonExistingKeyThrowsError(Jedis jedis) { + try { + jedis.sendCommand(Protocol.Command.SRANDMEMBER, "myzset", "WRONGARG"); + } catch (Exception e) { + assertThat(e.getMessage()).contains("ERR value is not an integer or out of range"); + } + } } diff --git a/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRandMember.java b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRandMember.java new file mode 100644 index 000000000..3afcb769d --- /dev/null +++ b/src/test/java/com/github/fppt/jedismock/comparisontests/sortedsets/TestZRandMember.java @@ -0,0 +1,128 @@ +package com.github.fppt.jedismock.comparisontests.sortedsets; + +import com.github.fppt.jedismock.comparisontests.ComparisonBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.resps.Tuple; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(ComparisonBase.class) +public class TestZRandMember { + + @BeforeEach + void setUp(Jedis jedis) { + jedis.flushAll(); + } + + @TestTemplate + void zrandMemberReturnsSingleElement(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("a", 1.0); + put("b", 2.0); + put("c", 3.0); + }}); + String member = jedis.zrandmember("myzset"); + assertThat(new HashSet() {{ add("a"); add("b"); add("c");}}).contains(member); + } + + @TestTemplate + void zrandMemberReturnsNullIfZSetEmpty(Jedis jedis) { + assertThat(jedis.zrandmember("nonexistent")).isNull(); + } + + @TestTemplate + void zrandMemberWithCountReturnsEmptyListForNonexistentKey(Jedis jedis) { + assertThat(jedis.zrandmember("myzset", 2)).isEmpty(); + } + + @TestTemplate + void zrandMemberWithCountReturnsElements(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("a", 1.0); + put("b", 2.0); + put("c", 3.0); + }}); + List members = jedis.zrandmember("myzset", 2); + assertThat(members.size()).isLessThanOrEqualTo(2); + assertThat(new HashSet() {{ add("a"); add("b"); add("c");}}).containsAll(members); + } + + @TestTemplate + void zrandMemberWithNegativeCountReturnsRepeatedElements(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("x", 1.0); + put("y", 2.0); + put("z", 3.0); + }}); + List members = jedis.zrandmember("myzset", -10); + assertThat(members).hasSize(10); + assertThat(new HashSet() {{ add("x"); add("y"); add("z");}}).containsAll(members); + } + + @TestTemplate + void zrandMemberWithCountAndWithScores(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("apple", 5.0); + put("banana", 7.0); + put("carrot", 9.0); + }}); + List result = jedis.zrandmemberWithScores("myzset", 2); + assertThat(result.size()).isEqualTo(2); + + assertThat(result.get(0).getScore()).isIn(5.0, 7.0, 9.0); + assertThat(result.get(1).getScore()).isIn(5.0, 7.0, 9.0); + + assertThat(result.get(0).getElement()).isIn("apple", "banana", "carrot"); + assertThat(result.get(1).getElement()).isIn("apple", "banana", "carrot"); + } + + @TestTemplate + void zrandMemberReturnsAllWhenCountExceedsSet(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("1", 1.0); + put("2", 2.0); + put("3", 3.0); + }}); + List members = jedis.zrandmember("myzset", 10); + assertThat(members).containsExactlyInAnyOrder("1", "2", "3"); + } + + @TestTemplate + void zrandMemberZeroCountReturnsEmptyList(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("1", 1.0); + put("2", 2.0); + }}); + List result = jedis.zrandmember("myzset", 0); + assertThat(result).isEmpty(); + } + + @TestTemplate + void zrandMemberWithInvalidWithScoresThrowsError(Jedis jedis) { + jedis.zadd("myzset", new HashMap() {{ + put("1", 1.0); + }}); + try { + jedis.sendCommand(Protocol.Command.ZRANDMEMBER, "myzset", "1", "WRONGARG"); + } catch (Exception e) { + assertThat(e.getMessage()).contains("ERR syntax error"); + } + } + + @TestTemplate + void zrandMemberWithInvalidArgumentAndNonExistingKeyThrowsError(Jedis jedis) { + try { + jedis.sendCommand(Protocol.Command.ZRANDMEMBER, "myzset", "WRONGARG"); + } catch (Exception e) { + assertThat(e.getMessage()).contains("ERR value is not an integer or out of range"); + } + } +} diff --git a/supported_operations.md b/supported_operations.md index 3da6fe782..02da7158e 100644 --- a/supported_operations.md +++ b/supported_operations.md @@ -95,7 +95,7 @@ | :heavy_check_mark: [bzmpop](https://valkey.io/commands/bzmpop/) | :heavy_check_mark: [zdiffstore](https://valkey.io/commands/zdiffstore/) | :heavy_check_mark: [zmscore](https://valkey.io/commands/zmscore/) | :heavy_check_mark: [zrangestore](https://valkey.io/commands/zrangestore/) | :heavy_check_mark: [zrevrangebylex](https://valkey.io/commands/zrevrangebylex/) | | :heavy_check_mark: [bzpopmax](https://valkey.io/commands/bzpopmax/) | :heavy_check_mark: [zincrby](https://valkey.io/commands/zincrby/) | :heavy_check_mark: [zpopmax](https://valkey.io/commands/zpopmax/) | :heavy_check_mark: [zrank](https://valkey.io/commands/zrank/) | :heavy_check_mark: [zrevrangebyscore](https://valkey.io/commands/zrevrangebyscore/) | | :heavy_check_mark: [bzpopmin](https://valkey.io/commands/bzpopmin/) | :heavy_check_mark: [zinter](https://valkey.io/commands/zinter/) | :heavy_check_mark: [zpopmin](https://valkey.io/commands/zpopmin/) | :heavy_check_mark: [zrem](https://valkey.io/commands/zrem/) | :heavy_check_mark: [zrevrank](https://valkey.io/commands/zrevrank/) | -| :heavy_check_mark: [zadd](https://valkey.io/commands/zadd/) | :heavy_check_mark: [zintercard](https://valkey.io/commands/zintercard/) | :x: [zrandmember](https://valkey.io/commands/zrandmember/) | :heavy_check_mark: [zremrangebylex](https://valkey.io/commands/zremrangebylex/) | :heavy_check_mark: [zscan](https://valkey.io/commands/zscan/) | +| :heavy_check_mark: [zadd](https://valkey.io/commands/zadd/) | :heavy_check_mark: [zintercard](https://valkey.io/commands/zintercard/) | :heavy_check_mark: [zrandmember](https://valkey.io/commands/zrandmember/) | :heavy_check_mark: [zremrangebylex](https://valkey.io/commands/zremrangebylex/) | :heavy_check_mark: [zscan](https://valkey.io/commands/zscan/) | | :heavy_check_mark: [zcard](https://valkey.io/commands/zcard/) | :heavy_check_mark: [zinterstore](https://valkey.io/commands/zinterstore/) | :heavy_check_mark: [zrange](https://valkey.io/commands/zrange/) | :heavy_check_mark: [zremrangebyrank](https://valkey.io/commands/zremrangebyrank/) | :heavy_check_mark: [zscore](https://valkey.io/commands/zscore/) | | :heavy_check_mark: [zcount](https://valkey.io/commands/zcount/) | :heavy_check_mark: [zlexcount](https://valkey.io/commands/zlexcount/) | :heavy_check_mark: [zrangebylex](https://valkey.io/commands/zrangebylex/) | :heavy_check_mark: [zremrangebyscore](https://valkey.io/commands/zremrangebyscore/) | :heavy_check_mark: [zunion](https://valkey.io/commands/zunion/) | | :heavy_check_mark: [zdiff](https://valkey.io/commands/zdiff/) | :heavy_check_mark: [zmpop](https://valkey.io/commands/zmpop/) | :heavy_check_mark: [zrangebyscore](https://valkey.io/commands/zrangebyscore/) | :heavy_check_mark: [zrevrange](https://valkey.io/commands/zrevrange/) | :heavy_check_mark: [zunionstore](https://valkey.io/commands/zunionstore/) |