diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala index 5e44c91..71dd1b6 100644 --- a/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/RedisCommandsClient.scala @@ -16,5 +16,6 @@ trait RedisCommandsClient[K, V] with RedisListAsyncCommands[K, V] with RedisSetAsyncCommands[K, V] with RedisSortedSetAsyncCommands[K, V] + with RedisScriptingAsyncCommands[K, V] with RedisServerAsyncCommands[K, V] with RedisPipelineAsyncCommands[K, V] \ No newline at end of file diff --git a/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisScriptingAsyncCommands.scala b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisScriptingAsyncCommands.scala new file mode 100644 index 0000000..e76cdd2 --- /dev/null +++ b/modules/api/src/main/scala/com/github/scoquelin/arugula/commands/RedisScriptingAsyncCommands.scala @@ -0,0 +1,193 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future +import scala.jdk.CollectionConverters.CollectionHasAsScala + +import com.github.scoquelin.arugula.commands.RedisScriptingAsyncCommands.ScriptOutputType + +trait RedisScriptingAsyncCommands[K, V] { + /** + * Evaluate a script + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @return The result of the script + */ + def eval(script: String, outputType: ScriptOutputType, keys:K*): Future[outputType.R] + + /** + * Evaluate a script + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @return The result of the script + */ + def eval(script: Array[Byte], outputType: ScriptOutputType, keys:K*): Future[outputType.R] + + /** + * Evaluate a script + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @param values The values to use in the script + * @return The result of the script + */ + def eval(script: String, outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] + + /** + * Evaluate a script + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @param values The values to use in the script + * @return The result of the script + */ + def eval(script: Array[Byte], outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] + + /** + * Evaluate a script by its SHA + * @param sha The SHA of the script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @return The result of the script + */ + def evalSha(sha: String, outputType: ScriptOutputType, keys:K*): Future[outputType.R] + + /** + * Evaluate a script by its SHA + * @param sha The SHA of the script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @return The result of the script + */ + def evalSha(sha: String, outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] + + /** + * Evaluate a script in read-only mode + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @param values The values to use in the script + * @return The result of the script + */ + def evalReadOnly(script: String, outputType: ScriptOutputType, keys:List[K], values: V*): Future[outputType.R] + + /** + * Evaluate a script in read-only mode + * @param script The script to evaluate + * @param outputType The expected output type + * @param keys The keys to use in the script + * @param values The values to use in the script + * @return The result of the script + */ + def evalReadOnly(script: Array[Byte], outputType: ScriptOutputType, keys:List[K], values: V*): Future[outputType.R] + + /** + * check if a script exists based on its SHA + * @param digest The SHA of the script to check + * @return True if the script exists, false otherwise + */ + def scriptExists(digest: String): Future[Boolean] + + /** + * check if a script exists based on its SHA + * @param digests The SHA of the scripts to check + * @return A list of booleans indicating if the scripts exist + */ + def scriptExists(digests: List[String]): Future[List[Boolean]] + + /** + * Flush all scripts from the script cache + * @return Unit + */ + def scriptFlush: Future[Unit] + + /** + * Flush all scripts from the script cache + * @param mode The mode to use for flushing + * @return Unit + */ + def scriptFlush(mode: RedisScriptingAsyncCommands.FlushMode): Future[Unit] + + /** + * Kill the currently executing script + * @return Unit + */ + def scriptKill: Future[Unit] + + /** + * Load a script into the script cache + * @param script The script to load + * @return The SHA of the script + */ + def scriptLoad(script: String): Future[String] + + /** + * Load a script into the script cache + * @param script The script to load + * @return The SHA of the script + */ + def scriptLoad(script: Array[Byte]): Future[String] +} + +object RedisScriptingAsyncCommands{ + + sealed trait ScriptOutputType { + type R + + def convert(in: Any): R + } + object ScriptOutputType { + case object Boolean extends ScriptOutputType{ + type R = Boolean + + def convert(in: Any): R = in match { + case 0L => false + case 1L => true + case b: Boolean => b + case s: String => s.toBoolean + case _ => throw new IllegalArgumentException(s"Cannot convert $in to Boolean") + } + } + case object Integer extends ScriptOutputType{ + type R = Long + + def convert(in: Any): R = in match { + case l: Long => l + case i: Int => i.toLong + case s: String => s.toLong + case _ => throw new IllegalArgumentException(s"Cannot convert $in to Int") + } + + } + case object Multi extends ScriptOutputType{ + type R = List[Any] + + def convert(in: Any): R = in match { + case l: List[_] => l + case l: java.util.List[_] => l.asScala.toList + case _ => throw new IllegalArgumentException(s"Cannot convert $in to List") + } + } + case object Status extends ScriptOutputType{ + type R = String + + def convert(in: Any): R = in match { + case s: String => s + case _ => throw new IllegalArgumentException(s"Cannot convert $in to String") + } + } + case object Value extends ScriptOutputType{ + type R = Any + + def convert(in: Any): R = in + } + } + + sealed trait FlushMode + + object FlushMode{ + case object Async extends FlushMode + case object Sync extends FlushMode + } +} diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala index 675e8a1..6c2100a 100644 --- a/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/LettuceRedisCommandsClient.scala @@ -33,6 +33,7 @@ private[arugula] class LettuceRedisCommandsClient[K, V]( with LettuceRedisHashAsyncCommands[K, V] with LettuceRedisServerAsyncCommands[K, V] with LettuceRedisListAsyncCommands[K, V] + with LettuceRedisScriptingAsyncCommands[K, V] with LettuceRedisStringAsyncCommands[K, V] with LettuceRedisSetAsyncCommands[K, V] with LettuceRedisSortedSetAsyncCommands[K, V] diff --git a/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisScriptingAsyncCommands.scala b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisScriptingAsyncCommands.scala new file mode 100644 index 0000000..3e5f20b --- /dev/null +++ b/modules/core/src/main/scala/com/github/scoquelin/arugula/commands/LettuceRedisScriptingAsyncCommands.scala @@ -0,0 +1,119 @@ +package com.github.scoquelin.arugula.commands + +import scala.concurrent.Future +import scala.jdk.CollectionConverters._ + +import com.github.scoquelin.arugula.RedisCommandsClient +import com.github.scoquelin.arugula.commands.RedisScriptingAsyncCommands.ScriptOutputType +import com.github.scoquelin.arugula.internal.LettuceRedisCommandDelegation + + +trait LettuceRedisScriptingAsyncCommands[K, V] extends RedisScriptingAsyncCommands[K, V] with LettuceRedisCommandDelegation[K, V] { this: RedisCommandsClient[K, V] => + + + override def eval(script: String, outputType: ScriptOutputType, keys:K*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.eval( + script, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys:_*)).map(outputType.convert) + } + + override def eval(script: Array[Byte], outputType: ScriptOutputType, keys: K*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.eval( + script, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys:_*)).map(outputType.convert) + } + + override def eval(script: String, outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.eval( + script, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys.toArray[Any].asInstanceOf[Array[K with AnyRef]], + values:_*)).map(outputType.convert) + } + +override def eval(script: Array[Byte], outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.eval( + script, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys.toArray[Any].asInstanceOf[Array[K with AnyRef]], + values: _*)).map(outputType.convert) + } + + override def evalSha(sha: String, outputType: ScriptOutputType, keys:K*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.evalsha( + sha, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys:_*)).map(outputType.convert) + } + + override def evalSha(sha: String, outputType: ScriptOutputType, keys: List[K], values: V*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.evalsha( + sha, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys.toArray[Any].asInstanceOf[Array[K with AnyRef]], + values:_*)).map(outputType.convert) + } + + override def evalReadOnly(script: String, outputType: ScriptOutputType, keys:List[K], values: V*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.evalReadOnly( + script.getBytes, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys.toArray[Any].asInstanceOf[Array[K with AnyRef]], + values:_* + )).map(outputType.convert) + } + + override def evalReadOnly(script: Array[Byte], outputType: ScriptOutputType, keys:List[K], values: V*): Future[outputType.R] = { + delegateRedisClusterCommandAndLift(_.evalReadOnly( + script, + LettuceRedisScriptingAsyncCommands.outputTypeToJava(outputType), + keys.toArray[Any].asInstanceOf[Array[K with AnyRef]], + values:_* + )).map(outputType.convert) + } + + override def scriptExists(digest: String): Future[Boolean] = + delegateRedisClusterCommandAndLift(_.scriptExists(digest)).map { + _.asScala.headOption.exists(Boolean2boolean) + } + + override def scriptExists(digests: List[String]): Future[List[Boolean]] = { + delegateRedisClusterCommandAndLift(_.scriptExists(digests:_*)).map(_.asScala.map(Boolean2boolean).toList) + } + + override def scriptFlush: Future[Unit] = { + delegateRedisClusterCommandAndLift(_.scriptFlush()).map(_ => ()) + } + + override def scriptFlush(mode: RedisScriptingAsyncCommands.FlushMode): Future[Unit] = { + delegateRedisClusterCommandAndLift(_.scriptFlush( + mode match { + case RedisScriptingAsyncCommands.FlushMode.Async => io.lettuce.core.FlushMode.ASYNC + case RedisScriptingAsyncCommands.FlushMode.Sync => io.lettuce.core.FlushMode.SYNC + } + )).map(_ => ()) + } + + override def scriptKill: Future[Unit] = + delegateRedisClusterCommandAndLift(_.scriptKill()).map(_ => ()) + + override def scriptLoad(script: String): Future[String] = { + delegateRedisClusterCommandAndLift(_.scriptLoad(script)) + } + + override def scriptLoad(script: Array[Byte]): Future[String] = + delegateRedisClusterCommandAndLift(_.scriptLoad(script)) + +} + +object LettuceRedisScriptingAsyncCommands{ + private[commands] def outputTypeToJava(outputType: ScriptOutputType): io.lettuce.core.ScriptOutputType = outputType match { + case ScriptOutputType.Boolean => io.lettuce.core.ScriptOutputType.BOOLEAN + case ScriptOutputType.Integer => io.lettuce.core.ScriptOutputType.INTEGER + case ScriptOutputType.Multi => io.lettuce.core.ScriptOutputType.MULTI + case ScriptOutputType.Status => io.lettuce.core.ScriptOutputType.STATUS + case ScriptOutputType.Value => io.lettuce.core.ScriptOutputType.VALUE + } +} \ 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 1edd2c5..ab5b41b 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 @@ -1,21 +1,24 @@ package com.github.scoquelin.arugula -import scala.collection.immutable.ListMap +import scala.collection.immutable.{ListMap, Map} import scala.concurrent.Future import com.github.scoquelin.arugula.codec.RedisCodec import com.github.scoquelin.arugula.commands.RedisSortedSetAsyncCommands.{Aggregate, AggregationArgs, RangeLimit, ScoreWithKeyValue, ScoreWithValue, SortOrder, ZAddOptions, ZRange} import org.scalatest.matchers.should.Matchers import scala.concurrent.duration._ +import scala.jdk.CollectionConverters.ListHasAsScala import com.github.scoquelin.arugula.commands.RedisBaseAsyncCommands.InitialCursor -import com.github.scoquelin.arugula.commands.{RedisBaseAsyncCommands, RedisKeyAsyncCommands, RedisListAsyncCommands, RedisServerAsyncCommands} +import com.github.scoquelin.arugula.commands.{RedisBaseAsyncCommands, RedisKeyAsyncCommands, RedisListAsyncCommands, RedisScriptingAsyncCommands, RedisServerAsyncCommands} import com.github.scoquelin.arugula.commands.RedisStringAsyncCommands.{BitFieldCommand, BitFieldDataType} +import io.lettuce.core.{RedisCommandExecutionException, RedisCommandInterruptedException} import java.time.Instant import java.util.concurrent.TimeUnit class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with Matchers { + import RedisCommandsIntegrationSpec.randomKey "RedisCommandsClient" when { @@ -100,8 +103,8 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- encoding shouldBe "embstr" // commenting out the following test because it returns this error: // ERR An LFU maxmemory policy is not selected, access frequency not tracked. -// frequency <- client.objectFreq(key) -// _ <- frequency shouldBe 1L + // frequency <- client.objectFreq(key) + // _ <- frequency shouldBe 1L idleTime <- client.objectIdleTime(key) _ <- idleTime shouldBe 0L refCount <- client.objectRefCount(key) @@ -237,7 +240,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- client.set(key2, "value") _ <- client.set(key3, "value") keys <- client.keys(prefix + "*") - _ <- keys should contain allOf (key1, key2, key3) + _ <- keys should contain allOf(key1, key2, key3) } yield succeed } } @@ -299,7 +302,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with _ <- client.set(key2, "value") _ <- client.set(key3, "value") keys <- scanAll(InitialCursor, List.empty) - _ <- keys should contain allOf (key1, key2, key3) + _ <- keys should contain allOf(key1, key2, key3) } yield succeed } } @@ -650,7 +653,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with lPushXResult <- client.lPushX(key, "zero") _ <- lPushXResult shouldBe 5L lSetResult <- client.lSet(key, 1, "1.75") - _ <- lSetResult shouldBe () + _ <- lSetResult shouldBe() rPushXResult <- client.rPushX(key, "four") _ <- rPushXResult shouldBe 6L } yield succeed @@ -1238,7 +1241,7 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with )) _ <- killResult shouldBe 0L - _ <- client.clientTracking(RedisServerAsyncCommands.TrackingArgs(enabled=true)) + _ <- client.clientTracking(RedisServerAsyncCommands.TrackingArgs(enabled = true)) cmd <- client.command _ <- cmd.isEmpty shouldBe false cmdCount <- client.commandCount @@ -1286,6 +1289,42 @@ class RedisCommandsIntegrationSpec extends BaseRedisCommandsIntegrationSpec with } + + "leveraging RedisScriptingAsyncCommands" should { + + "allow various scripting commands" in { + withRedisSingleNode(RedisCodec.Utf8WithValueAsStringCodec) { client => + val key = randomKey("script-key") + val hKey = randomKey("script-hash-key") + val value = "value" + for { + status <- client.eval("return redis.call('set', KEYS[1], ARGV[1])", RedisScriptingAsyncCommands.ScriptOutputType.Status, List(key), value) + _ <- status shouldBe "OK" + result <- client.get(key) + _ <- result shouldBe Some(value) + value <- client.eval("return redis.call('get', KEYS[1])", RedisScriptingAsyncCommands.ScriptOutputType.Value, key) + _ <- value shouldBe "value" + intValue <- client.eval("return 1", RedisScriptingAsyncCommands.ScriptOutputType.Integer) + _ <- intValue shouldBe 1 + _ <- client.hMSet(hKey, Map("field1" -> "value1", "field2" -> "value2", "field3" -> "value3")) + hGetAllValues <- client.eval("return redis.call('hgetall', KEYS[1])", RedisScriptingAsyncCommands.ScriptOutputType.Multi, hKey) + _ <- hGetAllValues shouldBe List("field1", "value1", "field2", "value2", "field3", "value3") + sha <- client.scriptLoad("return redis.call('get', KEYS[1])") + _ <- sha.isBlank shouldBe false + result <- client.evalSha(sha, RedisScriptingAsyncCommands.ScriptOutputType.Value, key) + _ <- result shouldBe "value" + scriptExists <- client.scriptExists(sha) + _ <- scriptExists shouldBe true + _ <- client.scriptFlush(RedisScriptingAsyncCommands.FlushMode.Sync) + scriptExists <- client.scriptExists(sha) + _ <- scriptExists shouldBe false + _ <- client.scriptKill.failed.map(_ shouldBe a[RedisCommandExecutionException]) + readOnlyResult <- client.evalReadOnly("return redis.call('get', KEYS[1])", RedisScriptingAsyncCommands.ScriptOutputType.Value, List(key)) + _ <- readOnlyResult shouldBe "value" + } yield succeed + } + } + } } } diff --git a/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisScriptingAsyncCommandsSpec.scala b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisScriptingAsyncCommandsSpec.scala new file mode 100644 index 0000000..606e00c --- /dev/null +++ b/modules/tests/test/src/test/scala/com/github/scoquelin/arugula/LettuceRedisScriptingAsyncCommandsSpec.scala @@ -0,0 +1,227 @@ +package com.github.scoquelin.arugula + +import scala.jdk.CollectionConverters.CollectionHasAsScala + +import com.github.scoquelin.arugula.commands.RedisScriptingAsyncCommands +import io.lettuce.core.{RedisFuture, ScriptOutputType} +import org.mockito.Mockito.{verify, when} +import org.scalatest.matchers.must.Matchers +import org.scalatest.{FutureOutcome, wordspec} + +class LettuceRedisScriptingAsyncCommandsSpec extends wordspec.FixtureAsyncWordSpec with Matchers { + + override type FixtureParam = LettuceRedisCommandsClientFixture.TestContext + + override def withFixture(test: OneArgAsyncTest): FutureOutcome = + withFixture(test.toNoArgAsyncTest(new LettuceRedisCommandsClientFixture.TestContext)) + + "LettuceRedisScriptingAsyncCommands" should { + "delegate EVAL command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.eval[String]("script", ScriptOutputType.VALUE, "key")).thenReturn(mockRedisFuture) + + testClass.eval("script", RedisScriptingAsyncCommands.ScriptOutputType.Value, "key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).eval("script", ScriptOutputType.VALUE, "key") + succeed + } + } + + "delegate EVAL command with values to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.eval[String]("script", ScriptOutputType.VALUE, List("key").toArray, "value")).thenReturn(mockRedisFuture) + + testClass.eval("script", RedisScriptingAsyncCommands.ScriptOutputType.Value, List("key"), "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).eval("script", ScriptOutputType.VALUE, List("key").toArray, "value") + succeed + } + } + + "delegate EVALSHA command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.evalsha[String]("sha", ScriptOutputType.VALUE, "key")).thenReturn(mockRedisFuture) + + testClass.evalSha("sha", RedisScriptingAsyncCommands.ScriptOutputType.Value, "key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).evalsha("sha", ScriptOutputType.VALUE, "key") + succeed + } + } + + "delegate EVALSHA command with values to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.evalsha[String]("sha", ScriptOutputType.VALUE, List("key").toArray, "value")).thenReturn(mockRedisFuture) + + testClass.evalSha("sha", RedisScriptingAsyncCommands.ScriptOutputType.Value, List("key"), "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).evalsha("sha", ScriptOutputType.VALUE, List("key").toArray, "value") + succeed + } + } + + "delegate EVAL command with Array[Byte] script to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.eval[String]("script".getBytes, ScriptOutputType.VALUE, "key")).thenReturn(mockRedisFuture) + + testClass.eval("script".getBytes, RedisScriptingAsyncCommands.ScriptOutputType.Value, "key").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).eval("script".getBytes, ScriptOutputType.VALUE, "key") + succeed + } + } + + "delegate EVAL command with Array[Byte] script and values to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.eval[String]("script".getBytes, ScriptOutputType.VALUE, List("key").toArray, "value")).thenReturn(mockRedisFuture) + + testClass.eval("script".getBytes, RedisScriptingAsyncCommands.ScriptOutputType.Value, List("key"), "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).eval("script".getBytes, ScriptOutputType.VALUE, List("key").toArray, "value") + succeed + } + } + + "delegate EVAL_RO command with Array[Byte] script and values to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.evalReadOnly[String]("script".getBytes, ScriptOutputType.VALUE, List("key").toArray, "value")).thenReturn(mockRedisFuture) + + testClass.evalReadOnly("script".getBytes, RedisScriptingAsyncCommands.ScriptOutputType.Value, List("key"), "value").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).evalReadOnly("script".getBytes, ScriptOutputType.VALUE, List("key").toArray, "value") + succeed + } + } + + "delegate EVAL_RO command with Array[Byte] script to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "Hello, world!" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.evalReadOnly[String]("script".getBytes, ScriptOutputType.VALUE, List("key").toArray)).thenReturn(mockRedisFuture) + + testClass.evalReadOnly("script".getBytes, RedisScriptingAsyncCommands.ScriptOutputType.Value, List("key")).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).evalReadOnly("script".getBytes, ScriptOutputType.VALUE, List("key").toArray) + succeed + } + } + + "delegate SCRIPT EXISTS command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = java.util.List.of(java.lang.Boolean.TRUE) + val mockRedisFuture: RedisFuture[java.util.List[java.lang.Boolean]] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.scriptExists("sha1")).thenReturn(mockRedisFuture) + + testClass.scriptExists("sha1").map { result => + result mustBe true + verify(lettuceAsyncCommands).scriptExists("sha1") + succeed + } + } + + "delegate SCRIPT EXISTS command to Lettuce with multiple shas and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = java.util.List.of(java.lang.Boolean.TRUE, java.lang.Boolean.FALSE) + val mockRedisFuture: RedisFuture[java.util.List[java.lang.Boolean]] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.scriptExists("sha1", "sha2")).thenReturn(mockRedisFuture) + + testClass.scriptExists(List("sha1", "sha2")).map { result => + result mustBe expectedValue.asScala.map(_.booleanValue()).toList + verify(lettuceAsyncCommands).scriptExists("sha1", "sha2") + succeed + } + } + + "delegate SCRIPT FLUSH command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn("OK") + when(lettuceAsyncCommands.scriptFlush()).thenReturn(mockRedisFuture) + + testClass.scriptFlush.map { result => + result mustBe () + verify(lettuceAsyncCommands).scriptFlush() + succeed + } + } + + "delegate SCRIPT FLUSH ASYNC command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn("OK") + when(lettuceAsyncCommands.scriptFlush(io.lettuce.core.FlushMode.ASYNC)).thenReturn(mockRedisFuture) + + testClass.scriptFlush(RedisScriptingAsyncCommands.FlushMode.Async).map { result => + result mustBe () + verify(lettuceAsyncCommands).scriptFlush(io.lettuce.core.FlushMode.ASYNC) + succeed + } + } + + "delegate SCRIPT KILL command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn("OK") + when(lettuceAsyncCommands.scriptKill()).thenReturn(mockRedisFuture) + + testClass.scriptKill.map { result => + result mustBe () + verify(lettuceAsyncCommands).scriptKill() + succeed + } + } + + "delegate SCRIPT LOAD command to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "sha1" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.scriptLoad("script")).thenReturn(mockRedisFuture) + + testClass.scriptLoad("script").map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).scriptLoad("script") + succeed + } + } + + "delegate SCRIPT LOAD command with bytes to Lettuce and lift result into a Future" in { testContext => + import testContext._ + + val expectedValue = "sha1" + val mockRedisFuture: RedisFuture[String] = mockRedisFutureToReturn(expectedValue) + when(lettuceAsyncCommands.scriptLoad("script".getBytes)).thenReturn(mockRedisFuture) + + testClass.scriptLoad("script".getBytes).map { result => + result mustBe expectedValue + verify(lettuceAsyncCommands).scriptLoad("script".getBytes) + succeed + } + } + } + +}