From 0a328a13cbad4ef859015f4dd858976916f0f8ec Mon Sep 17 00:00:00 2001 From: John Loehrer Date: Mon, 26 Aug 2024 06:45:05 -0700 Subject: [PATCH] implement remaining list commands and improve documentation --- .../commands/RedisListAsyncCommands.scala | 202 +++++++++++++++++- .../LettuceRedisListAsyncCommands.scala | 80 ++++--- .../RedisCommandsIntegrationSpec.scala | 45 +++- .../LettuceRedisListAsyncCommandsSpec.scala | 98 ++++++++- 4 files changed, 390 insertions(+), 35 deletions(-) diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisListAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisListAsyncCommands.scala index 11c7f26..15e87e0 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisListAsyncCommands.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisListAsyncCommands.scala @@ -1,50 +1,248 @@ package com.github.scoquelin.arugula.commands import scala.concurrent.Future +import scala.concurrent.duration.FiniteDuration +import java.util.concurrent.TimeUnit + +/** + * Asynchronous commands for manipulating/querying Lists (ordered collections of elements) + * + * @tparam K The key type + * @tparam V The value type + */ trait RedisListAsyncCommands[K, V] { + + def blPop( + timeout: FiniteDuration, + keys: K* + ): Future[Option[(K, V)]] + + /** + * Atomically returns and removes the first/ last element (head/ tail depending on the where from argument) + * of the list stored at source, and pushes the element at the first/ last element + * (head/ tail depending on the whereto argument) of the list stored at destination. + * When source is empty, Redis will block the connection until another client pushes to it + * or until timeout is reached. + * @param source The source list + * @param destination The destination list + * @param sourceSide The side of the source list to pop from + * @param destinationSide The side of the destination list to push to + * @param timeout The timeout as a finite duration. Zero is infinite wait + * @return The element that was moved, or None if the source list is empty + */ def blMove( source: K, destination: K, sourceSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Right, destinationSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, - timeout: Double = 0.0, // zero is infinite wait + timeout: FiniteDuration = new FiniteDuration(0, TimeUnit.MILLISECONDS), // zero is infinite wait ): Future[Option[V]] + + /** + * Remove and get the first/ last elements in a list, or block until one is available. + * @param keys The source lists + * @param direction The side of the source list to pop from + * @param count The number of elements to pop + * @param timeout The timeout in seconds + * @return The element that was moved, or None if the source list is empty + */ def blMPop( keys: List[K], direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, count: Int = 1, - timeout: Double = 0.0, + timeout: FiniteDuration = FiniteDuration(0, TimeUnit.MILLISECONDS), ): Future[Option[(K, List[V])]] + + /** + * Remove and get the last element in a list, or block until one is available. + * @param timeout The timeout in seconds + * @param keys The source lists + * @return The element that was moved, or None if the source list is empty + */ + def brPop( + timeout: FiniteDuration, + keys: K* + ): Future[Option[(K, V)]] + + /** + * Pop an element from a list, push it to another list and return it; or block until one is available. + * @param timeout The timeout in seconds + * @param source The source list + * @param destination The destination list + * @return The element that was moved, or None if the source list is empty + */ + def brPopLPush(timeout: FiniteDuration, source: K, destination: K): Future[Option[V]] + + /** + * Insert an element before or after another element in a list. + * @param key The key + * @param before Whether to insert before or after the pivot + * @param pivot The pivot element + * @param value The value to insert + * @return The length of the list after the insert operation + */ + def lInsert( + key: K, + before: Boolean, + pivot: V, + value: V + ): Future[Long] + + /** + * Remove and get the first/ last elements in a list, or block until one is available. + * @param keys The source lists + * @param direction The side of the source list to pop from + * @param count The number of elements to pop + * @return The element that was moved, or None if the source list is empty + */ def lMPop( keys: List[K], direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, count: Int = 1, ): Future[Option[(K, List[V])]] + + /** + * Prepend one or multiple values to a list. + * @param key The key + * @param values The values to add + * @return The length of the list after the push operation + */ def lPush(key: K, values: V*): Future[Long] + + /** + * Prepend a value to a list, only if the list exists. + * @param key The key + * @param values The values to add + * @return The length of the list after the push operation + */ + def lPushX(key: K, values: V*): Future[Long] + + /** + * Append one or multiple values to a list. + * @param key The key + * @param values The values to add + * @return The length of the list after the push operation + */ def rPush(key: K, values: V*): Future[Long] + + /** + * Append a value to a list, only if the list exists. + * @param key The key + * @param values The values to add + * @return The length of the list after the push operation + */ + def rPushX(key: K, values: V*): Future[Long] + + /** + * Remove and get the first element in a list + * @param key The key + * @return The element that was removed, or None if the list is empty + */ def lPop(key: K): Future[Option[V]] + + /** + * Remove and get the last element in a list + * @param key The key + * @return The element that was removed, or None if the list is empty + */ def rPop(key: K): Future[Option[V]] + + /** + * Remove the last element in a list, append it to another list and return it + * @param source The source key + * @param destination The destination key + * @return The element that was moved, or None if the source list is empty + */ + def rPopLPush(source: K, destination: K): Future[Option[V]] + + /** + * Get a range of elements from a list + * @param key The key + * @param start The start index + * @param end The end index + * @return The list of elements in the specified range + */ def lRange(key: K, start: Long, end: Long): Future[List[V]] + + /** + * Insert an element before or after another element in a list + * @param source The source key + * @param destination The destination key + * @param sourceSide The side of the source list to pop from + * @param destinationSide The side of the destination list to push to + * @return The element that was moved, or None if the source list is empty + */ def lMove( source: K, destination: K, sourceSide: RedisListAsyncCommands.Side, destinationSide: RedisListAsyncCommands.Side ): Future[Option[V]] + + /** + * Get the index of an element in a list + * @param key The key + * @param value The value + * @return The index of the element, or None if the element is not in the list + */ def lPos(key: K, value: V): Future[Option[Long]] + + /** + * Get the length of a list + * @param key The key + * @return The length of the list + */ def lLen(key: K): Future[Long] + + /** + * Remove and get the first element in a list + * @param key The key + * @return The number of elements that were removed + */ def lRem(key: K, count: Long, value: V): Future[Long] + + /** + * Set the value of an element in a list by its index. + * @param key The key + * @param index The index + * @param value The value + */ + def lSet(key: K, index: Long, value: V): Future[Unit] + + /** + * Trim a list to the specified range + * @param key The key + * @param start The start index + * @param end The end index + */ def lTrim(key: K, start: Long, end: Long): Future[Unit] + + /** + * Set the value of a list element by index + * @param key The key + * @param index The index + * @return The value of the element at the specified index, or None if the index is out of range + */ def lIndex(key: K, index: Long): Future[Option[V]] } object RedisListAsyncCommands { + + /** + * The side of a list to pop from + */ sealed trait Side object Side { + /** + * The left side of the list + */ case object Left extends Side + /** + * The right side of the list + */ case object Right extends Side } diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisListAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisListAsyncCommands.scala index 7fa9dfc..53aedc2 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisListAsyncCommands.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisListAsyncCommands.scala @@ -1,19 +1,36 @@ package com.github.scoquelin.arugula.commands import scala.concurrent.Future +import scala.concurrent.duration.FiniteDuration import scala.jdk.CollectionConverters._ import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation import io.lettuce.core.{LMPopArgs, LMoveArgs} +import java.util.concurrent.TimeUnit + private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V]{ + override def blPop( + timeout: FiniteDuration, + keys: K* + ): Future[Option[(K, V)]] = { + delegateRedisClusterCommandAndLift(_.blpop(timeout.toMillis.toDouble/1000, keys: _*)).map{ + case null => None + case result if result.hasValue => + val key = result.getKey + val value = result.getValue + Some((key, value)) + case _ => None + } + } + override def blMove( source: K, destination: K, sourceSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Right, destinationSide: RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, - timeout: Double = 0.0, // zero is infinite wait + timeout: FiniteDuration = new FiniteDuration(0, TimeUnit.MILLISECONDS), // zero is infinite wait ): Future[Option[V]] = { val args = (sourceSide, destinationSide) match { case (RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.leftLeft() @@ -21,20 +38,20 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Left) => LMoveArgs.Builder.rightLeft() case (RedisListAsyncCommands.Side.Right, RedisListAsyncCommands.Side.Right) => LMoveArgs.Builder.rightRight() } - delegateRedisClusterCommandAndLift(_.blmove(source, destination, args, timeout)).map(Option.apply) + delegateRedisClusterCommandAndLift(_.blmove(source, destination, args, timeout.toMillis.toDouble/1000)).map(Option.apply) } override def blMPop( keys: List[K], direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, count: Int = 1, - timeout: Double = 0.0, + timeout: FiniteDuration = FiniteDuration(0, TimeUnit.MILLISECONDS), ): Future[Option[(K, List[V])]] = { val args: LMPopArgs = direction match { case RedisListAsyncCommands.Side.Left => LMPopArgs.Builder.left().count(count) case RedisListAsyncCommands.Side.Right => LMPopArgs.Builder.right().count(count) } - delegateRedisClusterCommandAndLift(_.blmpop(timeout, args, keys: _*)).map{ + delegateRedisClusterCommandAndLift(_.blmpop(timeout.toMillis.toDouble/1000, args, keys: _*)).map{ case null => None case result => val key = result.getKey @@ -43,6 +60,25 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn } } + override def brPop(timeout: FiniteDuration, keys: K*): Future[Option[(K, V)]] = { + delegateRedisClusterCommandAndLift(_.brpop(timeout.toMillis.toDouble/1000, keys: _*)).map{ + case null => None + case result if result.hasValue => + val key = result.getKey + val value = result.getValue + Some((key, value)) + case _ => None + } + } + + override def brPopLPush(timeout: FiniteDuration, source: K, destination: K): Future[Option[V]] = { + delegateRedisClusterCommandAndLift(_.brpoplpush(timeout.toMillis.toDouble/1000, source, destination)).map(Option.apply) + } + + override def lInsert(key: K, before: Boolean, pivot: V, value: V): Future[Long] = { + delegateRedisClusterCommandAndLift(_.linsert(key, before, pivot, value)).map(Long2long) + } + def lMPop( keys: List[K], direction:RedisListAsyncCommands.Side = RedisListAsyncCommands.Side.Left, @@ -64,6 +100,9 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn override def lRem(key: K, count: Long, value: V): Future[Long] = delegateRedisClusterCommandAndLift(_.lrem(key, count, value)).map(Long2long) + override def lSet(key: K, index: Long, value: V): Future[Unit] = + delegateRedisClusterCommandAndLift(_.lset(key, index, value)).map(_ => ()) + override def lTrim(key: K, start: Long, stop: Long): Future[Unit] = delegateRedisClusterCommandAndLift(_.ltrim(key, start, stop)).map(_ => ()) @@ -97,12 +136,22 @@ private[arugula] trait LettuceRedisListAsyncCommands[K, V] extends RedisListAsyn override def lPush(key: K, values: V*): Future[Long] = delegateRedisClusterCommandAndLift(_.lpush(key, values: _*)).map(Long2long) + override def lPushX(key: K, values: V*): Future[Long] = + delegateRedisClusterCommandAndLift(_.lpushx(key, values: _*)).map(Long2long) + override def rPop(key: K): Future[Option[V]] = delegateRedisClusterCommandAndLift(_.rpop(key)).map(Option.apply) + override def rPopLPush(source: K, destination: K): Future[Option[V]] = { + delegateRedisClusterCommandAndLift(_.rpoplpush(source, destination)).map(Option.apply) + } + override def rPush(key: K, values: V*): Future[Long] = delegateRedisClusterCommandAndLift(_.rpush(key, values: _*)).map(Long2long) + override def rPushX(key: K, values: V*): Future[Long] = + delegateRedisClusterCommandAndLift(_.rpushx(key, values: _*)).map(Long2long) + override def lIndex(key: K, index: Long): Future[Option[V]] = delegateRedisClusterCommandAndLift(_.lindex(key, index)).map(Option.apply) @@ -117,25 +166,4 @@ object LettuceRedisListAsyncCommands { case object Right extends Side } -} - -/** - * blpop - * brpop - * brpoplpush - * lindex - * linsert - * llen - * lpop - * lpos - * lpush - * lpushx - * lrange - * lrem - * lset - * ltrim - * rpop - * rpoplpush - * rpush - * rpushx - */ \ No newline at end of file +} \ No newline at end of file diff --git a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala index dd2a48c..fda9f02 100644 --- a/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala +++ b/modules/tests/it/src/test/scala/com/github/scoquelin/arugula/RedisCommandsIntegrationSpec.scala @@ -358,7 +358,16 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- popped shouldBe Some("one") endState <- client.lRange(key, 0, -1) _ <- endState.isEmpty shouldBe true - + _ <- client.lPush(key, "one", "two", "three") + _ <- client.lInsert(key, before = true, "two", "1.5") + range <- client.lRange(key, 0, -1) + _ <- range shouldBe List("three", "1.5", "two", "one") + lPushXResult <- client.lPushX(key, "zero") + _ <- lPushXResult shouldBe 5L + lSetResult <- client.lSet(key, 1, "1.75") + _ <- lSetResult shouldBe () + rPushXResult <- client.rPushX(key, "four") + _ <- rPushXResult shouldBe 6L } yield succeed } } @@ -374,7 +383,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- client.lPush(key1, "one", "two", "three") _ <- client.lPush(key2, "four", "five", "six") _ <- client.lMove(key1, key2, RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right) - _ <- client.blMove(key1, key3, RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, timeout = 0.1) + _ <- client.blMove(key1, key3, RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, timeout = FiniteDuration(100, TimeUnit.MILLISECONDS)) key1Range <- client.lRange(key1, 0, -1) _ <- key1Range shouldBe List("one") key2Range <- client.lRange(key2, 0, -1) @@ -386,6 +395,36 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with } } + "support pop operations" in { + withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => + val suffix = "{user1}" + val key1 = randomKey("list-key1") + suffix + val destKey = randomKey("list-key2") + suffix + for { + _ <- client.lPush(key1, "one", "two", "three") + popResult <- client.lPop(key1) + _ <- popResult shouldBe Some("three") + key1Range <- client.lRange(key1, 0, -1) + _ <- key1Range shouldBe List("two", "one") + blPopResult <- client.blPop(timeout = FiniteDuration(1, TimeUnit.MILLISECONDS), key1) + _ <- blPopResult shouldBe Some((key1, "two")) + key1Range <- client.lRange(key1, 0, -1) + _ <- key1Range shouldBe List("one") + rPopResult <- client.rPop(key1) + _ <- rPopResult shouldBe Some("one") + _ <- client.rPush(key1, "one") + brPopResult <- client.brPop(timeout = FiniteDuration(1, TimeUnit.MILLISECONDS), key1) + _ <- brPopResult shouldBe Some((key1, "one")) + _ <- client.rPush(key1, "one") + brPopLPushResult <- client.brPopLPush(timeout = FiniteDuration(1, TimeUnit.MILLISECONDS), key1, destKey) + _ <- brPopLPushResult shouldBe Some("one") + _ <- client.rPush(key1, "one") + rPopLPushResult <- client.rPopLPush(key1, destKey) + _ <- rPopLPushResult shouldBe Some("one") + } yield succeed + } + } + "support multi pop operations" in { withRedisSingleNodeAndCluster(RedisCodec.Utf8WithValueAsStringCodec) { client => val suffix = "{user1}" @@ -400,7 +439,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- key1Range shouldBe List("one") key2Range <- client.lRange(key2, 0, -1) _ <- key2Range shouldBe List("six", "five", "four") - blPopResult <- client.blMPop(List(key1, key2), count = 2, timeout = 0.1) + blPopResult <- client.blMPop(List(key1, key2), count = 2, timeout = FiniteDuration(1, TimeUnit.MILLISECONDS)) _ <- blPopResult shouldBe Some((key1, List("one"))) key1Range <- client.lRange(key1, 0, -1) _ <- key1Range shouldBe List() diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisListAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisListAsyncCommandsSpec.scala index a90a952..09f1efb 100644 --- a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisListAsyncCommandsSpec.scala +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisListAsyncCommandsSpec.scala @@ -1,5 +1,6 @@ package com.github.scoquelin.arugula +import scala.concurrent.duration.FiniteDuration import scala.jdk.CollectionConverters._ import com.github.scoquelin.arugula.commands.RedisListAsyncCommands @@ -9,6 +10,8 @@ import org.mockito.Mockito.{verify, when} import org.scalatest.matchers.must.Matchers import org.scalatest.{FutureOutcome, wordspec} +import java.util.concurrent.TimeUnit + class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { override type FixtureParam = LettuceRedisCommandsClientFixture.TestContext @@ -24,7 +27,7 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) when(lettuceAsyncCommands.blmove(any, any, any, anyDouble)).thenReturn(mockRedisFuture) - testClass.blMove("source", "destination", RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, 1.0).map { result => + testClass.blMove("source", "destination", RedisListAsyncCommands.Side.Left, RedisListAsyncCommands.Side.Right, FiniteDuration(1, TimeUnit.SECONDS)).map { result => result mustBe Some(expectedValue) verify(lettuceAsyncCommands).blmove(meq("source"), meq("destination"), any, meq(1.0)) succeed @@ -36,13 +39,28 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi val expectedValue = KeyValue.fromNullable("key", List("value").asJava) val mockRedisFuture: RedisFuture[KeyValue[String, java.util.List[String]]] = mockRedisFutureToReturn(expectedValue) when(lettuceAsyncCommands.blmpop(anyDouble, any, anyString)).thenReturn(mockRedisFuture) - testClass.blMPop(List("key"), timeout=1).map { result => + testClass.blMPop(List("key"), timeout=FiniteDuration(1, TimeUnit.SECONDS)).map { result => verify(lettuceAsyncCommands).blmpop(meq(1.0), any, meq("key")) result mustBe Some(("key", List("value"))) succeed } } + "delegate LINSERT command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.linsert("key", true, "value", "pivot")).thenReturn(mockRedisFuture) + + testClass.lInsert("key", before = true, pivot = "value", value = "pivot").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).linsert("key", true, "value", "pivot") + succeed + } + } + "delegate LMPOP command to Lettuce and lift result into a Future" in { testContext => import testContext._ val expectedValue = KeyValue.fromNullable("key", List("value").asJava) @@ -70,6 +88,21 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi } } + "delegate LPUSHX command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.lpushx("key", "value")).thenReturn(mockRedisFuture) + + testClass.lPushX("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).lpushx("key", "value") + succeed + } + } + "delegate LPOP command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -85,7 +118,22 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi } } - "delegate LRPOP command to Lettuce and lift result into a Future" in { testContext => + "delegate BRPOP command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = KeyValue.fromNullable("key", "value") + val mockRedisFuture: RedisFuture[KeyValue[String, String]] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.brpop(anyDouble, anyString)).thenReturn(mockRedisFuture) + + testClass.brPop(FiniteDuration(1, TimeUnit.SECONDS), "key").map { result => + result mustBe Some(("key", "value")) + verify(lettuceAsyncCommands).brpop(meq(1.0), meq("key")) + succeed + } + } + + "delegate RPOP command to Lettuce and lift result into a Future" in { testContext => import testContext._ val expectedValue = "value" @@ -100,6 +148,21 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi } } + "delegate RPOPLPUSH command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "value" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.rpoplpush("source", "destination")).thenReturn(mockRedisFuture) + + testClass.rPopLPush("source", "destination").map { result => + result mustBe Some(expectedValue) + verify(lettuceAsyncCommands).rpoplpush("source", "destination") + succeed + } + } + "delegate LINDEX command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -175,6 +238,19 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi } } + "delegate LSET command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn("OK") + + when(lettuceAsyncCommands.lset("key", 0, "value")).thenReturn(mockRedisFuture) + + testClass.lSet("key", 0, "value").map { _ => + verify(lettuceAsyncCommands).lset("key", 0, "value") + succeed + } + } + "delegate LTRIM command to Lettuce and lift result into a Future" in { testContext => import testContext._ @@ -202,6 +278,20 @@ class LettuceRedisListAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec wi succeed } } - } + "delegate RPUSHX command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = 1L + val mockRedisFuture: RedisFuture[java.lang.Long] = mockRedisFutureToReturn(expectedValue) + + when(lettuceAsyncCommands.rpushx("key", "value")).thenReturn(mockRedisFuture) + + testClass.rPushX("key", "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).rpushx("key", "value") + succeed + } + } + } }