diff --git a/CHANGELOG.md b/CHANGELOG.md index 16312284..a34f7a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ Updated to Play `2.9.0` and dropped `Scala 2.12` since it was discontinued from the Play framework. Minimal supported Java version is now `11`. [#265](https://github.com/KarelCemus/play-redis/pull/265) +Migrated from test framework `specs2` to `scalatest` because specs2 is not friendly to +cross-compilation between Scala 2.13 and Scala 3. Test are uses `testcontainers` to run +standalone, cluster or sentinel instance. However, current redis connector is not friendly +and there are several significant limitations in the implementation, therefore the tests +for cluster are using fixed port mapping and tests for sentinel are disabled since it seems +that sentinel implementation is not fully reliable, therefore sentinel is not officially +supported at this moment. [#273](https://github.com/KarelCemus/play-redis/pull/273) + ### [:link: 2.7.0](https://github.com/KarelCemus/play-redis/tree/2.7.0) SET command supports milliseconds, previous versions used seconds [#247](https://github.com/KarelCemus/play-redis/issues/247) @@ -30,17 +38,17 @@ Note: This should not be a breaking change since it was not possible to properly the value in Java without encountering the exception. Introduced another source `aws-cluster`, which is a cluster with nodes defined by DNS record. For example, -Amazon AWS uses this kind of cluster definition. Therefore this type of a cluster resolves +Amazon AWS uses this kind of cluster definition. Therefore this type of a cluster resolves a domain main to extract all nodes. See [#221](https://github.com/KarelCemus/play-redis/pull/221) for more details. ### [:link: 2.5.0](https://github.com/KarelCemus/play-redis/tree/2.5.0) -Added `expiresIn(key: String): Option[Duration]` implementing PTTL +Added `expiresIn(key: String): Option[Duration]` implementing PTTL command to get expiration of the given key. [#204](https://github.com/KarelCemus/play-redis/pull/204) Introduced asynchronous implementation of advanced Java API for redis cache. The API wraps the Scala version thus provides slightly worse performance and deals with -the lack of `classTag` in Play's API design. **This API implementation is experimental +the lack of `classTag` in Play's API design. **This API implementation is experimental and may change in future.** Feedback will be welcome. [#206](https://github.com/KarelCemus/play-redis/issues/206) Added `getFields(fields: String*)` and `getFields(fields: Iterable[String])` into `RedisMap` API @@ -55,7 +63,7 @@ Cross-compiled with Scala 2.13 [#211](https://github.com/KarelCemus/play-redis/i Update to Play `2.7.0` [#202](https://github.com/KarelCemus/play-redis/pull/202) -Added `getAll[T: ClassTag](keys: Iterable[String]): Result[Seq[Option[T]]]` into `AbstractCacheApi` +Added `getAll[T: ClassTag](keys: Iterable[String]): Result[Seq[Option[T]]]` into `AbstractCacheApi` in order to also accept collections aside vararg. [#194](https://github.com/KarelCemus/play-redis/pull/194) Fixed `getOrElse` method in Synchronous API with non-empty cache prefix. [#196](https://github.com/KarelCemus/play-redis/pull/196) @@ -71,7 +79,7 @@ Returned keys are automatically unprefixed. [#184](https://github.com/KarelCemus Support of plain arrays in JavaRedis [#176](https://github.com/KarelCemus/play-redis/pull/176). -Connection timeout introduced in [#147](https://github.com/KarelCemus/play-redis/issues/147) +Connection timeout introduced in [#147](https://github.com/KarelCemus/play-redis/issues/147) is now configurable and can be disabled [#174](https://github.com/KarelCemus/play-redis/pull/174). Removed deprecations introduced in [2.0.0](https://github.com/KarelCemus/play-redis/tree/2.0.0) diff --git a/build.sbt b/build.sbt index d25a5030..78f6ea88 100644 --- a/build.sbt +++ b/build.sbt @@ -21,11 +21,12 @@ libraryDependencies ++= Seq( // redis connector "io.github.rediscala" %% "rediscala" % "1.14.0-akka", // test framework with mockito extension - "org.specs2" %% "specs2-mock" % "4.20.3" % Test, + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.scalamock" %% "scalamock" % "5.2.0" % Test, // test module for play framework "com.typesafe.play" %% "play-test" % playVersion.value % Test, // to run integration tests - "com.dimafeng" %% "testcontainers-scala-core" % "0.41.0" % Test + "com.dimafeng" %% "testcontainers-scala-core" % "0.41.2" % Test ) resolvers ++= Seq( @@ -40,3 +41,5 @@ enablePlugins(CustomReleasePlugin) // exclude from tests coverage coverageExcludedFiles := ".*exceptions.*" + +Test / test := (Test / testOnly).toTask(" * -- -l \"org.scalatest.Ignore\"").value diff --git a/src/main/scala/play/api/cache/redis/CacheApi.scala b/src/main/scala/play/api/cache/redis/CacheApi.scala index ba303080..267118b7 100644 --- a/src/main/scala/play/api/cache/redis/CacheApi.scala +++ b/src/main/scala/play/api/cache/redis/CacheApi.scala @@ -27,7 +27,7 @@ private[redis] trait AbstractCacheApi[Result[_]] { * @param key cache storage keys * @return stored record, Some if exists, otherwise None */ - def getAll[T: ClassTag](key: String*): Result[Seq[Option[T]]] = getAll(key) + final def getAll[T: ClassTag](key: String*): Result[Seq[Option[T]]] = getAll(key) /** * Retrieve the values of all specified keys from the cache. diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala b/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala index dfde44e9..fafbc716 100644 --- a/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala +++ b/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala @@ -46,6 +46,7 @@ object RedisHost extends ConfigLoader[RedisHost] { host = config.getString(path / "host"), port = config.getInt(path / "port"), database = config.getOption(path / "database", _.getInt), + username = config.getOption(path / "username", _.getString), password = config.getOption(path / "password", _.getString) ) diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala b/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala index 03c2f334..adab2ba5 100644 --- a/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala +++ b/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala @@ -71,7 +71,7 @@ trait RedisStandalone extends RedisInstance with RedisHost { object RedisStandalone { - def apply(name: String, host: RedisHost, settings: RedisSettings) = + def apply(name: String, host: RedisHost, settings: RedisSettings): RedisStandalone = create(name, host, settings) @inline diff --git a/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala b/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala index 4c2c0bc5..ffb0fd1d 100644 --- a/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala +++ b/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala @@ -1,13 +1,10 @@ package play.api.cache.redis.connector import javax.inject._ - import scala.concurrent.Future - import play.api.Logger import play.api.cache.redis.configuration._ import play.api.inject.ApplicationLifecycle - import akka.actor.ActorSystem import redis.{RedisClient => RedisStandaloneClient, RedisCluster => RedisClusterClient, _} @@ -17,7 +14,7 @@ import redis.{RedisClient => RedisStandaloneClient, RedisCluster => RedisCluster */ private[connector] class RedisCommandsProvider(instance: RedisInstance)(implicit system: ActorSystem, lifecycle: ApplicationLifecycle) extends Provider[RedisCommands] { - lazy val get = instance match { + lazy val get: RedisCommands = instance match { case cluster: RedisCluster => new RedisCommandsCluster(cluster).get case standalone: RedisStandalone => new RedisCommandsStandalone(standalone).get case sentinel: RedisSentinel => new RedisCommandsSentinel(sentinel).get @@ -115,7 +112,7 @@ private[connector] class RedisCommandsCluster(configuration: RedisCluster)(impli } // $COVERAGE-OFF$ - def start() = { + def start(): Unit = { def servers = nodes.map { case RedisHost(host, port, Some(database), _, _) => s" $host:$port?database=$database" case RedisHost(host, port, None, _, _) => s" $host:$port" @@ -126,7 +123,7 @@ private[connector] class RedisCommandsCluster(configuration: RedisCluster)(impli def stop(): Future[Unit] = Future successful { log.info("Stopping the redis cluster cache actor ...") - client.stop() + Option(client).foreach(_.stop()) log.info("Redis cluster cache stopped.") } // $COVERAGE-ON$ diff --git a/src/main/scala/play/api/cache/redis/connector/RedisConnectorProvider.scala b/src/main/scala/play/api/cache/redis/connector/RedisConnectorProvider.scala index 909a004a..32bc7d60 100644 --- a/src/main/scala/play/api/cache/redis/connector/RedisConnectorProvider.scala +++ b/src/main/scala/play/api/cache/redis/connector/RedisConnectorProvider.scala @@ -12,7 +12,7 @@ import akka.actor.ActorSystem */ private[redis] class RedisConnectorProvider(instance: RedisInstance, serializer: AkkaSerializer)(implicit system: ActorSystem, lifecycle: ApplicationLifecycle, runtime: RedisRuntime) extends Provider[RedisConnector] { - private lazy val commands = new RedisCommandsProvider(instance).get + private[connector] lazy val commands = new RedisCommandsProvider(instance).get lazy val get = new RedisConnectorImpl(serializer, commands) } diff --git a/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala b/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala index 986f68c7..ec0a74db 100644 --- a/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala +++ b/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala @@ -15,9 +15,14 @@ sealed trait InvocationPolicy { } object EagerInvocation extends InvocationPolicy { - def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext) = { f; Future successful thenReturn } + override def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext): Future[T] = { + f + Future successful thenReturn + } } object LazyInvocation extends InvocationPolicy { - def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext) = f.map(_ => thenReturn) + override def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext): Future[T] = { + f.map(_ => thenReturn) + } } diff --git a/src/main/scala/play/api/cache/redis/impl/RedisCache.scala b/src/main/scala/play/api/cache/redis/impl/RedisCache.scala index 85bf43b9..7f4a5e35 100644 --- a/src/main/scala/play/api/cache/redis/impl/RedisCache.scala +++ b/src/main/scala/play/api/cache/redis/impl/RedisCache.scala @@ -66,9 +66,8 @@ private[impl] class RedisCache[Result[_]](redis: RedisConnector, builder: Builde doMatching(pattern).recoverWithDefault(Seq.empty[String]) } - def getOrElse[T: ClassTag](key: String, expiration: Duration)(orElse: => T) = key.prefixed { key => + def getOrElse[T: ClassTag](key: String, expiration: Duration)(orElse: => T) = getOrFuture(key, expiration)(orElse.toFuture).recoverWithDefault(orElse) - } def getOrFuture[T: ClassTag](key: String, expiration: Duration)(orElse: => Future[T]): Future[T] = key.prefixed { key => redis.get[T](key).flatMap { diff --git a/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala b/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala index f4a9b68a..fee9850b 100644 --- a/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala +++ b/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala @@ -10,6 +10,17 @@ class RedisListJavaImpl[Elem](internal: RedisList[Elem, Future])(implicit runtim def This = this + private lazy val modifier: AsyncRedisList.AsyncRedisListModification[Elem] = newModifier() + private lazy val viewer: AsyncRedisList.AsyncRedisListView[Elem] = newViewer() + + protected def newViewer(): AsyncRedisList.AsyncRedisListView[Elem] = { + new AsyncRedisListViewJavaImpl(internal.view) + } + + protected def newModifier(): AsyncRedisList.AsyncRedisListModification[Elem] = { + new AsyncRedisListModificationJavaImpl(internal.modify) + } + def prepend(element: Elem): CompletionStage[AsyncRedisList[Elem]] = { async { implicit context => internal.prepend(element).map(_ => this) @@ -70,13 +81,9 @@ class RedisListJavaImpl[Elem](internal: RedisList[Elem, Future])(implicit runtim } } - def view(): AsyncRedisList.AsyncRedisListView[Elem] = { - new AsyncRedisListViewJavaImpl(internal.view) - } + def view(): AsyncRedisList.AsyncRedisListView[Elem] = viewer - def modify(): AsyncRedisList.AsyncRedisListModification[Elem] = { - new AsyncRedisListModificationJavaImpl(internal.modify) - } + def modify(): AsyncRedisList.AsyncRedisListModification[Elem] = modifier private class AsyncRedisListViewJavaImpl(view: internal.RedisListView) extends AsyncRedisList.AsyncRedisListView[Elem] { diff --git a/src/test/resources/reference.conf b/src/test/resources/reference.conf index 37f29bf7..03486f3b 100644 --- a/src/test/resources/reference.conf +++ b/src/test/resources/reference.conf @@ -18,5 +18,5 @@ akka { warn-about-java-serializer-usage = off } - loggers = ["play.api.cache.redis.logging.RedisLogger"] + loggers = ["play.api.cache.redis.test.RedisLogger"] } diff --git a/src/test/scala-2.13/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala b/src/test/scala-2.13/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala deleted file mode 100644 index 60d4d209..00000000 --- a/src/test/scala-2.13/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala +++ /dev/null @@ -1,64 +0,0 @@ -package play.api.cache.redis.connector - -import play.api.inject.guice.GuiceApplicationBuilder - -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification - -class ScalaSpecificSerializerSpec extends Specification with Mockito { - import SerializerImplicits._ - - private val system = GuiceApplicationBuilder().build().actorSystem - - private implicit val serializer: AkkaSerializer = new AkkaSerializerImpl(system) - - "AkkaEncoder" should "encode" >> { - - "custom classes" in { - SimpleObject("B", 3).encoded mustEqual - """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0 - |cyRTaW1wbGVPYmplY3TyYCEG2fNkUQIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJp - |bmc7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces - } - - "list" in { - List("A", "B", "C").encoded mustEqual - """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmdlbmVyaWMuRGVmYXVsdFNlcmlhbGl6YXRpb25Qcm94e - |QAAAAAAAAADAwABTAAHZmFjdG9yeXQAGkxzY2FsYS9jb2xsZWN0aW9uL0ZhY3Rvcnk7eHBzcgAqc2 - |NhbGEuY29sbGVjdGlvbi5JdGVyYWJsZUZhY3RvcnkkVG9GYWN0b3J5AAAAAAAAAAMCAAFMAAdmYWN - |0b3J5dAAiTHNjYWxhL2NvbGxlY3Rpb24vSXRlcmFibGVGYWN0b3J5O3hwc3IAJnNjYWxhLnJ1bnRp - |bWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqY - |XZhL2xhbmcvQ2xhc3M7eHB2cgAgc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUuTGlzdCQAAAAAAA - |AAAwIAAHhwdwT/////dAABQXQAAUJ0AAFDc3EAfgAGdnIAJnNjYWxhLmNvbGxlY3Rpb24uZ2VuZXJ - |pYy5TZXJpYWxpemVFbmQkAAAAAAAAAAMCAAB4cHg= - """.stripMargin.removeAllWhitespaces - } - } - - "AkkaDecoder" should "decode" >> { - - "custom classes" in { - """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0 - |cyRTaW1wbGVPYmplY3TyYCEG2fNkUQIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJp - |bmc7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces.decoded[SimpleObject] mustEqual SimpleObject("B", 3) - } - - "list" in { - """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmdlbmVyaWMuRGVmYXVsdFNlcmlhbGl6YXRpb25Qcm94e - |QAAAAAAAAADAwABTAAHZmFjdG9yeXQAGkxzY2FsYS9jb2xsZWN0aW9uL0ZhY3Rvcnk7eHBzcgAqc2 - |NhbGEuY29sbGVjdGlvbi5JdGVyYWJsZUZhY3RvcnkkVG9GYWN0b3J5AAAAAAAAAAMCAAFMAAdmYWN - |0b3J5dAAiTHNjYWxhL2NvbGxlY3Rpb24vSXRlcmFibGVGYWN0b3J5O3hwc3IAJnNjYWxhLnJ1bnRp - |bWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqY - |XZhL2xhbmcvQ2xhc3M7eHB2cgAgc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUuTGlzdCQAAAAAAA - |AAAwIAAHhwdwT/////dAABQXQAAUJ0AAFDc3EAfgAGdnIAJnNjYWxhLmNvbGxlY3Rpb24uZ2VuZXJ - |pYy5TZXJpYWxpemVFbmQkAAAAAAAAAAMCAAB4cHg= - """.stripMargin.removeAllWhitespaces.decoded[List[String]] mustEqual List("A", "B", "C") - } - } -} diff --git a/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala b/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala deleted file mode 100644 index da359612..00000000 --- a/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala +++ /dev/null @@ -1,20 +0,0 @@ -package play.api.cache.redis - -trait ClusterRedisContainer extends RedisContainer { - - protected def redisMaster = 4 - - protected def redisSlaves = 1 - - override protected lazy val redisConfig: RedisContainerConfig = - RedisContainerConfig( - "grokzen/redis-cluster:latest", - 0.until(redisMaster * (redisSlaves + 1)).map(7000 + _), - Map( - "IP" -> "0.0.0.0", - "INITIAL_PORT" -> "7000", - "MASTERS" -> s"$redisMaster", - "SLAVES_PER_MASTER" -> s"$redisSlaves", - ), - ) -} diff --git a/src/test/scala/play/api/cache/redis/ExpirationSpec.scala b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala index 8f160d79..51a24a6b 100644 --- a/src/test/scala/play/api/cache/redis/ExpirationSpec.scala +++ b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala @@ -1,6 +1,6 @@ package play.api.cache.redis -import org.specs2.mutable.Specification +import play.api.cache.redis.test._ import java.time.Instant import java.util.Date @@ -9,7 +9,7 @@ import scala.concurrent.duration._ /** *
This specification tests expiration conversion
*/ -class ExpirationSpec extends Specification { +class ExpirationSpec extends UnitSpec { "Expiration" should { @@ -20,12 +20,16 @@ class ExpirationSpec extends Specification { val expirationTo = expiration + 1.second "from java.util.Date" in { - new Date(expireAt.toEpochMilli).asExpiration must beBetween(expirationFrom, expirationTo) + val expiration = new Date(expireAt.toEpochMilli).asExpiration + expiration mustBe >=(expirationFrom) + expiration mustBe <=(expirationTo) } "from java.time.LocalDateTime" in { import java.time._ - LocalDateTime.ofInstant(expireAt, ZoneId.systemDefault()).asExpiration must beBetween(expirationFrom, expirationTo) + val expiration = LocalDateTime.ofInstant(expireAt, ZoneId.systemDefault()).asExpiration + expiration mustBe >=(expirationFrom) + expiration mustBe <=(expirationTo) } } } diff --git a/src/test/scala/play/api/cache/redis/Implicits.scala b/src/test/scala/play/api/cache/redis/Implicits.scala deleted file mode 100644 index abd74bf5..00000000 --- a/src/test/scala/play/api/cache/redis/Implicits.scala +++ /dev/null @@ -1,118 +0,0 @@ -package play.api.cache.redis - -import java.util.concurrent.Callable - -import scala.collection.mutable -import scala.concurrent._ -import scala.concurrent.duration._ -import scala.language.implicitConversions -import scala.util._ - -import play.api.cache.redis.configuration._ - -import akka.actor.ActorSystem - -import org.specs2.execute.{AsResult, Result} -import org.specs2.matcher.Expectations -import org.specs2.mock.mockito._ -import org.specs2.specification.{Around, Scope} - -object Implicits { - - val defaultCacheName = "play" - val localhost = "localhost" - val localhostIp = "127.0.0.1" - val dockerIp = localhostIp - val defaultPort = 6379 - - val defaults = RedisSettingsTest("akka.actor.default-dispatcher", "lazy", RedisTimeouts(1.second, None, 500.millis), "log-and-default", "standalone") - - val defaultInstance = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort), defaults) - - implicit def implicitlyImmutableSeq[T](value: mutable.ListBuffer[T]): Seq[T] = value.toSeq - - implicit def implicitlyAny2Some[T](value: T): Option[T] = Some(value) - - implicit def implicitlyAny2future[T](value: T): Future[T] = Future.successful(value) - - implicit def implicitlyEx2future(ex: Throwable): Future[Nothing] = Future.failed(ex) - - implicit def implicitlyAny2success[T](value: T): Try[T] = Success(value) - - implicit def implicitlyAny2failure(ex: Throwable): Try[Nothing] = Failure(ex) - - implicit class FutureAwait[T](val future: Future[T]) extends AnyVal { - def awaitForFuture = Await.result(future, 2.minutes) - } - - implicit def implicitlyAny2Callable[T](f: => T): Callable[T] = new Callable[T] { - def call() = f - } - - implicit class RichFutureObject(val future: Future.type) extends AnyVal { - /** returns a future resolved in given number of seconds */ - def after[T](seconds: Int, value: => T)(implicit system: ActorSystem, ec: ExecutionContext): Future[T] = { - val promise = Promise[T]() - // after a timeout, resolve the promise - akka.pattern.after(seconds.seconds, system.scheduler) { - promise.success(value) - promise.future - } - // return the promise - promise.future - } - - def after(seconds: Int)(implicit system: ActorSystem, ec: ExecutionContext): Future[Unit] = { - after(seconds, ()) - } - } -} - -trait ReducedMockito extends MocksCreation - with CalledMatchers - with MockitoStubs - with CapturedArgument - // with MockitoMatchers - // with ArgThat - with Expectations - with MockitoFunctions { - - override def argThat[T, U <: T](m: org.specs2.matcher.Matcher[U]): T = super.argThat(m) -} - -object MockitoImplicits extends ReducedMockito - -trait WithApplication { - import play.api.Application - import play.api.inject.guice.GuiceApplicationBuilder - - protected def builder = new GuiceApplicationBuilder() - - private lazy val theBuilder = builder - - protected lazy val injector = theBuilder.injector() - - protected lazy val application: Application = injector.instanceOf[Application] - - implicit protected lazy val system: ActorSystem = injector.instanceOf[ActorSystem] -} - -trait WithHocon { - import play.api.Configuration - - import com.typesafe.config.ConfigFactory - - protected def hocon: String - - protected lazy val config = { - val reference = ConfigFactory.load() - val local = ConfigFactory.parseString(hocon.stripMargin) - local.withFallback(reference) - } - - protected lazy val configuration = Configuration(config) -} - -abstract class WithConfiguration(val hocon: String) extends WithHocon with Around with Scope { - def around[T: AsResult](t: => T): Result = AsResult.effectively(t) -} diff --git a/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala b/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala index 172f089a..907e4eb0 100644 --- a/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala +++ b/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala @@ -1,37 +1,31 @@ package play.api.cache.redis -import scala.concurrent.Future - import play.api.Logger +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { +class RecoveryPolicySpec extends AsyncUnitSpec { - class BasicPolicy extends RecoveryPolicy { - def recoverFrom[T](rerun: => Future[T], default: => Future[T], failure: RedisException) = default - } + private val rerun = Future.successful(10) + private val default = Future.successful(0) - val rerun = Future.successful(10) - val default = Future.successful(0) - - object ex { - private val internal = new IllegalArgumentException("Internal cause") - val unexpectedAny = UnexpectedResponseException(None, "TEST-CMD") - val unexpectedKey = UnexpectedResponseException(Some("some key"), "TEST-CMD") - val failedAny = ExecutionFailedException(None, "TEST-CMD", "TEST-CMD", internal) - val failedKey = ExecutionFailedException(Some("key"), "TEST-CMD", "TEST-CMD key value", internal) - val timeout = TimeoutException(internal) - val serialization = SerializationException("some key", "TEST-CMD", internal) + private object ex { + private val internal = new IllegalArgumentException("Simulated Internal cause") + val unexpectedAny: UnexpectedResponseException = UnexpectedResponseException(None, "TEST-CMD") + val unexpectedKey: UnexpectedResponseException = UnexpectedResponseException(Some("some key"), "TEST-CMD") + val failedAny: ExecutionFailedException = ExecutionFailedException(None, "TEST-CMD", "TEST-CMD", internal) + val failedKey: ExecutionFailedException = ExecutionFailedException(Some("key"), "TEST-CMD", "TEST-CMD key value", internal) + val timeout: TimeoutException = TimeoutException(internal) + val serialization: SerializationException = SerializationException("some key", "TEST-CMD", internal) def any: UnexpectedResponseException = unexpectedAny } "Recovery Policy" should { "log detailed report" in { - val policy = new BasicPolicy with DetailedReports { - override val log = mock[Logger] + val policy = new RecoverWithDefault with DetailedReports { + override val log: Logger = Logger(getClass) } // note: there should be tested a logger and the message @@ -44,8 +38,8 @@ class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with R } "log condensed report" in { - val policy = new BasicPolicy with CondensedReports { - override val log = mock[Logger] + val policy = new RecoverWithDefault with CondensedReports { + override val log: Logger = Logger(getClass) } // note: there should be tested a logger and the message @@ -58,14 +52,12 @@ class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with R } "fail through" in { - val policy = new BasicPolicy with FailThrough - - policy.recoverFrom(rerun, default, ex.any) must throwA(ex.any).await + val policy = new FailThrough {} + policy.recoverFrom(rerun, default, ex.any).assertingFailure(ex.any) } "recover with default" in { - val policy = new BasicPolicy with RecoverWithDefault - + val policy = new RecoverWithDefault {} policy.recoverFrom(rerun, default, ex.any) mustEqual default } } diff --git a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala index f8ca9019..14db7b83 100644 --- a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala +++ b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala @@ -1,49 +1,48 @@ package play.api.cache.redis +import akka.actor.ActorSystem import play.api._ +import play.api.cache.redis.test._ import play.api.inject.ApplicationLifecycle -import org.specs2.mutable.Specification - -class RedisCacheComponentsSpec extends Specification with WithApplication with StandaloneRedisContainer { - import Implicits._ - - object components extends RedisCacheComponents with WithHocon { - def actorSystem = system - def applicationLifecycle = injector.instanceOf[ApplicationLifecycle] - def environment = injector.instanceOf[Environment] - lazy val syncRedis = cacheApi("play").sync - override lazy val configuration = Configuration(config) - override protected def hocon: String = s"play.cache.redis.port: ${container.mappedPort(defaultPort)}" - } - private type Cache = CacheApi +class RedisCacheComponentsSpec extends IntegrationSpec with RedisStandaloneContainer { - private lazy val cache = components.syncRedis + private val prefix = "components-sync" - val prefix = "components-sync" + test("miss on get") { cache => + cache.get[String](s"$prefix-test-1") mustEqual None + } - "RedisComponents" should { + test(" hit after set") { cache => + cache.set(s"$prefix-test-2", "value") + cache.get[String](s"$prefix-test-2") mustEqual Some("value") + } - "provide api" >> { - "miss on get" in { - cache.get[String](s"$prefix-test-1") must beNone - } + test("return positive exists on existing keys") { cache => + cache.set(s"$prefix-test-11", "value") + cache.exists(s"$prefix-test-11") mustEqual true + } - "hit after set" in { - cache.set(s"$prefix-test-2", "value") - cache.get[String](s"$prefix-test-2") must beSome[Any] - cache.get[String](s"$prefix-test-2") must beSome("value") + private def test(name: String)(cache: CacheApi => Assertion): Unit = { + s"should $name" in { + + val components: TestComponents = new TestComponents { + override lazy val configuration: Configuration = Helpers.configuration.fromHocon( + s"play.cache.redis.port: ${container.mappedPort(defaultPort)}" + ) + override lazy val actorSystem: ActorSystem = system + override lazy val applicationLifecycle: ApplicationLifecycle = injector.instanceOf[ApplicationLifecycle] + override lazy val environment: Environment = injector.instanceOf[Environment] + override lazy val syncRedis: CacheApi = cacheApi("play").sync } - "positive exists on existing keys" in { - cache.set(s"$prefix-test-11", "value") - cache.exists(s"$prefix-test-11") must beTrue + components.runInApplication { + cache(components.syncRedis) } } } - override def afterAll() = { - Shutdown.run.awaitForFuture - super.afterAll() + private trait TestComponents extends RedisCacheComponents with FakeApplication { + def syncRedis: CacheApi } } diff --git a/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala index 8735aa78..a2dca40f 100644 --- a/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala +++ b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala @@ -1,179 +1,175 @@ package play.api.cache.redis -import javax.inject.Provider +import akka.actor.ActorSystem +import play.api.cache.redis.configuration.{RedisStandalone, RedisTimeouts} +import play.api.cache.redis.test._ +import play.api.inject._ +import play.api.inject.guice.GuiceApplicationBuilder +import play.cache.NamedCacheImpl -import scala.concurrent.duration._ +import scala.concurrent.duration.DurationInt import scala.reflect.ClassTag -import play.api.inject._ +class RedisCacheModuleSpec extends IntegrationSpec with RedisStandaloneContainer { +import Helpers._ + + private final val defaultCacheName: String = "play" + + test("bind defaults") { + _.bindings(new RedisCacheModule).configure(s"play.cache.redis.port" -> container.mappedPort(defaultPort)) + } { injector => + injector.checkBinding[RedisConnector] + injector.checkBinding[CacheApi] + injector.checkBinding[CacheAsyncApi] + injector.checkBinding[play.cache.AsyncCacheApi] + injector.checkBinding[play.cache.SyncCacheApi] + injector.checkBinding[play.cache.redis.AsyncCacheApi] + injector.checkBinding[play.api.cache.AsyncCacheApi] + injector.checkBinding[play.api.cache.SyncCacheApi] + } -import akka.actor.ActorSystem -import org.specs2.execute.{AsResult, Result} -import org.specs2.mutable._ -import org.specs2.specification.Scope - -/** - *This specification tests expiration conversion
- */ -class RedisCacheModuleSpec extends Specification { - - import Implicits._ - import RedisCacheModuleSpec._ - - "RedisCacheModule" should { - - "bind defaults" in new WithApplication with Scope with Around { - override protected def builder = super.builder.bindings(new RedisCacheModule) - def around[T: AsResult](t: => T): Result = runAndStop(t) - - injector.instanceOf[RedisConnector] must beAnInstanceOf[RedisConnector] - injector.instanceOf[CacheApi] must beAnInstanceOf[CacheApi] - injector.instanceOf[CacheAsyncApi] must beAnInstanceOf[CacheAsyncApi] - injector.instanceOf[play.cache.AsyncCacheApi] must beAnInstanceOf[play.cache.AsyncCacheApi] - injector.instanceOf[play.cache.SyncCacheApi] must beAnInstanceOf[play.cache.SyncCacheApi] - injector.instanceOf[play.cache.redis.AsyncCacheApi] must beAnInstanceOf[play.cache.redis.AsyncCacheApi] - injector.instanceOf[play.api.cache.AsyncCacheApi] must beAnInstanceOf[play.api.cache.AsyncCacheApi] - injector.instanceOf[play.api.cache.SyncCacheApi] must beAnInstanceOf[play.api.cache.SyncCacheApi] + test("not bind defaults") { + _.bindings(new RedisCacheModule) + .configure("play.cache.redis.bind-default" -> false) + .configure(s"play.cache.redis.port" -> container.mappedPort(defaultPort)) + } { injector => + // bind named caches + injector.checkNamedBinding[CacheApi] + injector.checkNamedBinding[CacheAsyncApi] + + // but do not bind defaults + assertThrows[com.google.inject.ConfigurationException] { + injector.instanceOf[CacheApi] } - - "not bind defaults" in new WithHocon with WithApplication with Scope with Around { - override protected def builder = super.builder.bindings(new RedisCacheModule).configure(configuration) - def around[T: AsResult](t: => T): Result = runAndStop(t) - protected def hocon = "play.cache.redis.bind-default: false" - - // bind named caches - injector.instanceOf(binding[CacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[CacheApi] - injector.instanceOf(binding[CacheAsyncApi].namedCache(defaultCacheName)) must beAnInstanceOf[CacheAsyncApi] - - // but do not bind defaults - injector.instanceOf[CacheApi] must throwA[com.google.inject.ConfigurationException] - injector.instanceOf[CacheAsyncApi] must throwA[com.google.inject.ConfigurationException] + assertThrows[com.google.inject.ConfigurationException] { + injector.instanceOf[CacheAsyncApi] } + } - "bind named cache in simple mode" in new WithApplication with Scope with Around { - override protected def builder = super.builder.bindings(new RedisCacheModule) - def around[T: AsResult](t: => T): Result = runAndStop(t) - def checkBinding[T <: AnyRef: ClassTag] = { - injector.instanceOf(binding[T].namedCache(defaultCacheName)) must beAnInstanceOf[T] - } - - checkBinding[RedisConnector] - checkBinding[CacheApi] - checkBinding[CacheAsyncApi] - checkBinding[play.cache.AsyncCacheApi] - checkBinding[play.cache.SyncCacheApi] - checkBinding[play.api.cache.AsyncCacheApi] - checkBinding[play.api.cache.SyncCacheApi] - } + test("bind named cache in simple mode") { + _.bindings(new RedisCacheModule) + } { injector => + injector.checkNamedBinding[RedisConnector] + injector.checkNamedBinding[CacheApi] + injector.checkNamedBinding[CacheAsyncApi] + injector.checkNamedBinding[play.cache.AsyncCacheApi] + injector.checkNamedBinding[play.cache.SyncCacheApi] + injector.checkNamedBinding[play.api.cache.AsyncCacheApi] + injector.checkNamedBinding[play.api.cache.SyncCacheApi] + } - "bind named caches" in new WithHocon with WithApplication with Scope with Around { - override protected def builder = super.builder.bindings(new RedisCacheModule).configure(configuration) - def around[T: AsResult](t: => T): Result = runAndStop(t) - protected def hocon = - """ + test("bind named caches") { + _.bindings(new RedisCacheModule).configure( + configuration.fromHocon( + s""" |play.cache.redis { | instances { - | | play { - | host: localhost - | port: 6379 + | host: ${container.host} + | port: ${container.mappedPort(defaultPort)} | database: 1 | } - | | other { - | host: redis.localhost.cz - | port: 6378 + | host: ${container.host} + | port: ${container.mappedPort(defaultPort)} | database: 2 | password: something | } | } - | | default-cache: other |} """.stripMargin - val other = "other" - - // something is bound to the default cache name - injector.instanceOf(binding[RedisConnector].namedCache(defaultCacheName)) must beAnInstanceOf[RedisConnector] - injector.instanceOf(binding[CacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[CacheApi] - injector.instanceOf(binding[CacheAsyncApi].namedCache(defaultCacheName)) must beAnInstanceOf[CacheAsyncApi] - injector.instanceOf(binding[play.cache.AsyncCacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[play.cache.AsyncCacheApi] - injector.instanceOf(binding[play.cache.SyncCacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[play.cache.SyncCacheApi] - injector.instanceOf(binding[play.api.cache.AsyncCacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[play.api.cache.AsyncCacheApi] - injector.instanceOf(binding[play.api.cache.SyncCacheApi].namedCache(defaultCacheName)) must beAnInstanceOf[play.api.cache.SyncCacheApi] - - // something is bound to the other cache name - injector.instanceOf(binding[RedisConnector].namedCache(other)) must beAnInstanceOf[RedisConnector] - injector.instanceOf(binding[CacheApi].namedCache(other)) must beAnInstanceOf[CacheApi] - injector.instanceOf(binding[CacheAsyncApi].namedCache(other)) must beAnInstanceOf[CacheAsyncApi] - injector.instanceOf(binding[play.cache.AsyncCacheApi].namedCache(other)) must beAnInstanceOf[play.cache.AsyncCacheApi] - injector.instanceOf(binding[play.cache.SyncCacheApi].namedCache(other)) must beAnInstanceOf[play.cache.SyncCacheApi] - injector.instanceOf(binding[play.api.cache.AsyncCacheApi].namedCache(other)) must beAnInstanceOf[play.api.cache.AsyncCacheApi] - injector.instanceOf(binding[play.api.cache.SyncCacheApi].namedCache(other)) must beAnInstanceOf[play.api.cache.SyncCacheApi] - - // the other cache is a default - injector.instanceOf(binding[RedisConnector].namedCache(other)) mustEqual injector.instanceOf[RedisConnector] - injector.instanceOf(binding[CacheApi].namedCache(other)) mustEqual injector.instanceOf[CacheApi] - injector.instanceOf(binding[CacheAsyncApi].namedCache(other)) mustEqual injector.instanceOf[CacheAsyncApi] - injector.instanceOf(binding[play.cache.AsyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.cache.AsyncCacheApi] - injector.instanceOf(binding[play.cache.SyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.cache.SyncCacheApi] - injector.instanceOf(binding[play.api.cache.AsyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.api.cache.AsyncCacheApi] - injector.instanceOf(binding[play.api.cache.SyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.api.cache.SyncCacheApi] - } - - "resolve custom redis instance" in new WithHocon with WithApplication with Scope with Around { - override protected def builder = super.builder.bindings(new RedisCacheModule).configure(configuration).bindings( - binding[RedisInstance].namedCache(defaultCacheName).to(MyRedisInstance) ) - def around[T: AsResult](t: => T): Result = runAndStop(t) - protected def hocon = "play.cache.redis.source: custom" + ) + } { injector => + val other = "other" + + // something is bound to the default cache name + injector.checkNamedBinding[RedisConnector] + injector.checkNamedBinding[CacheApi] + injector.checkNamedBinding[CacheAsyncApi] + injector.checkNamedBinding[play.cache.AsyncCacheApi] + injector.checkNamedBinding[play.cache.SyncCacheApi] + injector.checkNamedBinding[play.api.cache.AsyncCacheApi] + injector.checkNamedBinding[play.api.cache.SyncCacheApi] + + // something is bound to the other cache name + injector.checkNamedBinding[RedisConnector](other) + injector.checkNamedBinding[CacheApi](other) + injector.checkNamedBinding[CacheAsyncApi](other) + injector.checkNamedBinding[play.cache.AsyncCacheApi](other) + injector.checkNamedBinding[play.cache.SyncCacheApi](other) + injector.checkNamedBinding[play.api.cache.AsyncCacheApi](other) + injector.checkNamedBinding[play.api.cache.SyncCacheApi](other) + + // the other cache is a default + injector.instanceOf(binding[RedisConnector].namedCache(other)) mustEqual injector.instanceOf[RedisConnector] + injector.instanceOf(binding[CacheApi].namedCache(other)) mustEqual injector.instanceOf[CacheApi] + injector.instanceOf(binding[CacheAsyncApi].namedCache(other)) mustEqual injector.instanceOf[CacheAsyncApi] + injector.instanceOf(binding[play.cache.AsyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.cache.AsyncCacheApi] + injector.instanceOf(binding[play.cache.SyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.cache.SyncCacheApi] + injector.instanceOf(binding[play.api.cache.AsyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.api.cache.AsyncCacheApi] + injector.instanceOf(binding[play.api.cache.SyncCacheApi].namedCache(other)) mustEqual injector.instanceOf[play.api.cache.SyncCacheApi] + } - // bind named caches - injector.instanceOf[RedisConnector] must beAnInstanceOf[RedisConnector] - injector.instanceOf[CacheApi] must beAnInstanceOf[CacheApi] - injector.instanceOf[CacheAsyncApi] must beAnInstanceOf[CacheAsyncApi] + test("resolve custom redis instance") { + _.bindings(new RedisCacheModule) + .configure("play.cache.redis.source" -> "custom") + .bindings(binding[RedisInstance].namedCache(defaultCacheName).to(MyRedisInstance)) + } { injector => + injector.checkBinding[RedisConnector] + injector.checkBinding[CacheApi] + injector.checkBinding[CacheAsyncApi] + } - // Note: there should be tested which recovery policy instance is actually used - } + private object MyRedisInstance extends RedisStandalone { + override lazy val name: String = defaultCacheName + override lazy val invocationContext: String = "akka.actor.default-dispatcher" + override lazy val invocationPolicy: String = "lazy" + override lazy val timeout: RedisTimeouts = RedisTimeouts(1.second) + override lazy val recovery: String = "log-and-default" + override lazy val source: String = "my-instance" + override lazy val prefix: Option[String] = None + override lazy val host: String = container.host + override lazy val port: Int = container.mappedPort(defaultPort) + override lazy val database: Option[Int] = None + override lazy val username: Option[String] = None + override lazy val password: Option[String] = None } -} -object RedisCacheModuleSpec { - import Implicits._ - import play.api.cache.redis.configuration._ - import play.cache.NamedCacheImpl + private def binding[T: ClassTag]: BindingKey[T] = + BindingKey(implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]]) - class AnyProvider[T](instance: => T) extends Provider[T] { - lazy val get = instance + private implicit class RichBindingKey[T](private val key: BindingKey[T]) { + def namedCache(name: String): BindingKey[T] = key.qualifiedWith(new NamedCacheImpl(name)) } - def binding[T: ClassTag]: BindingKey[T] = BindingKey(implicitly[ClassTag[T]].runtimeClass.asInstanceOf[Class[T]]) + private implicit class InjectorAssertions(private val injector: Injector) { - implicit class RichBindingKey[T](val key: BindingKey[T]) { - def namedCache(name: String) = key.qualifiedWith(new NamedCacheImpl(name)) - } + def checkBinding[T <: AnyRef : ClassTag]: Assertion = { + injector.instanceOf(binding[T]) mustBe a[T] + } - def runAndStop[T: AsResult](t: => T)(implicit system: ActorSystem) = { - try { - AsResult.effectively(t) - } finally { - Shutdown.run(system) + def checkNamedBinding[T <: AnyRef : ClassTag]: Assertion = { + checkNamedBinding(defaultCacheName) + } + + def checkNamedBinding[T <: AnyRef : ClassTag](name: String): Assertion = { + injector.instanceOf(binding[T].namedCache(name)) mustBe a[T] } } - object MyRedisInstance extends RedisStandalone { - - override def name = defaultCacheName - override def invocationContext = "akka.actor.default-dispatcher" - override def invocationPolicy = "lazy" - override def timeout = RedisTimeouts(1.second) - override def recovery = "log-and-default" - override def source = "my-instance" - override def prefix = None - override def host = localhost - override def port = defaultPort - override def database = None - override def username = None - override def password = None + + private def test(name: String)(createBuilder: GuiceApplicationBuilder => GuiceApplicationBuilder)(f: Injector => Assertion): Unit = { + s"should $name" in { + + val builder = createBuilder(new GuiceApplicationBuilder) + val injector = builder.injector() + val application = StoppableApplication(injector.instanceOf[ActorSystem]) + + application.runInApplication { + f(injector) + } + } } } diff --git a/src/test/scala/play/api/cache/redis/RedisContainer.scala b/src/test/scala/play/api/cache/redis/RedisContainer.scala deleted file mode 100644 index b0682d46..00000000 --- a/src/test/scala/play/api/cache/redis/RedisContainer.scala +++ /dev/null @@ -1,19 +0,0 @@ -package play.api.cache.redis - -import com.dimafeng.testcontainers.GenericContainer -import org.testcontainers.containers.wait.strategy.Wait - -trait RedisContainer extends ForAllTestContainer { - - protected def redisConfig: RedisContainerConfig - - private lazy val config = redisConfig - - override val newContainer = GenericContainer( - dockerImage = config.redisDockerImage, - exposedPorts = config.redisPorts, - env = config.redisEnvironment, - waitStrategy = Wait.forListeningPorts(config.redisPorts: _*), - ) -} - diff --git a/src/test/scala/play/api/cache/redis/Shutdown.scala b/src/test/scala/play/api/cache/redis/Shutdown.scala deleted file mode 100644 index 56fb6a20..00000000 --- a/src/test/scala/play/api/cache/redis/Shutdown.scala +++ /dev/null @@ -1,13 +0,0 @@ -package play.api.cache.redis - -import akka.actor.{ActorSystem, CoordinatedShutdown} - -trait Shutdown extends RedisCacheComponents { - - def shutdown() = Shutdown.run -} - -object Shutdown { - - def run(implicit system: ActorSystem) = CoordinatedShutdown(system).run(CoordinatedShutdown.UnknownReason) -} diff --git a/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala b/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala deleted file mode 100644 index 505e0276..00000000 --- a/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala +++ /dev/null @@ -1,11 +0,0 @@ -package play.api.cache.redis - -trait StandaloneRedisContainer extends RedisContainer { - - override protected lazy val redisConfig: RedisContainerConfig = - RedisContainerConfig( - redisDockerImage = "redis:latest", - redisPorts = Seq(6379), - redisEnvironment = Map.empty - ) -} diff --git a/src/test/scala/play/api/cache/redis/configuration/HostnameResolverSpec.scala b/src/test/scala/play/api/cache/redis/configuration/HostnameResolverSpec.scala index 79ac9649..1360552a 100644 --- a/src/test/scala/play/api/cache/redis/configuration/HostnameResolverSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/HostnameResolverSpec.scala @@ -1,8 +1,8 @@ package play.api.cache.redis.configuration -import org.specs2.mutable.Specification +import play.api.cache.redis.test.UnitSpec -class HostnameResolverSpec extends Specification { +class HostnameResolverSpec extends UnitSpec { import HostnameResolver._ "hostname is resolved to IP address" in { diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala index 312f7120..501480d4 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala @@ -1,35 +1,53 @@ package play.api.cache.redis.configuration -import play.api.cache.redis._ -import org.specs2.mutable.Specification import play.api.ConfigLoader +import play.api.cache.redis.test._ -class RedisHostSpec extends Specification { - import Implicits._ +import scala.language.implicitConversions + +class RedisHostSpec extends UnitSpec with ImplicitOptionMaterialization { private implicit val loader: ConfigLoader[RedisHost] = RedisHost - "host with database and password" in new WithConfiguration( - """ - |play.cache.redis { - | host: localhost - | port: 6378 - | database: 1 - | password: something - |} - """ - ) { - configuration.get[RedisHost]("play.cache.redis") mustEqual RedisHost("localhost", 6378, database = 1, password = "something") + "host with database, username, and password" in { + val configuration = Helpers.configuration.fromHocon { + """play.cache.redis { + | host: localhost + | port: 6378 + | database: 1 + | username: my-user + | password: something + |} + """.stripMargin + } + configuration.get[RedisHost]("play.cache.redis") mustEqual RedisHost( + host = "localhost", port = 6378, database = 1, username = "my-user", password = "something" + ) + } + + "host with database, password but without a username" in { + val configuration = Helpers.configuration.fromHocon { + """play.cache.redis { + | host: localhost + | port: 6378 + | database: 1 + | password: something + |} + """.stripMargin + } + configuration.get[RedisHost]("play.cache.redis") mustEqual RedisHost( + host = "localhost", port = 6378, database = 1, username = None, password = "something" + ) } - "host without database and password" in new WithConfiguration( - """ - |play.cache.redis { - | host: localhost - | port: 6378 - |} - """ - ) { + "host without database and password" in { + val configuration = Helpers.configuration.fromHocon { + """play.cache.redis { + | host: localhost + | port: 6378 + |} + """.stripMargin + } configuration.get[RedisHost]("play.cache.redis") mustEqual RedisHost("localhost", 6378, database = 0) } @@ -37,6 +55,8 @@ class RedisHostSpec extends Specification { RedisHost.fromConnectionString("redis://redis:something@localhost:6378") mustEqual RedisHost("localhost", 6378, username = "redis", password = "something") RedisHost.fromConnectionString("redis://localhost:6378") mustEqual RedisHost("localhost", 6378) // test invalid string - RedisHost.fromConnectionString("redis:/localhost:6378") must throwA[IllegalArgumentException] + assertThrows[IllegalArgumentException] { + RedisHost.fromConnectionString("redis:/localhost:6378") + } } } diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala index 5643a84f..35704f3a 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala @@ -1,34 +1,38 @@ package play.api.cache.redis.configuration -import scala.concurrent.duration._ +import play.api.cache.redis.test._ +import play.api.{ConfigLoader, Configuration} -import org.specs2.mutable.Specification +import scala.concurrent.duration._ +import scala.language.implicitConversions -class RedisInstanceManagerSpec extends Specification { - import play.api.cache.redis.Implicits._ +class RedisInstanceManagerSpec extends UnitSpec with ImplicitOptionMaterialization{ - private implicit def implicitlyInstance2resolved(instance: RedisInstance): RedisInstanceProvider = new ResolvedRedisInstance(instance) - private implicit def implicitlyString2unresolved(name: String): RedisInstanceProvider = new UnresolvedRedisInstance(name) + "default configuration" in new TestCase { - private val extras = RedisSettingsTest("my-dispatcher", "eager", RedisTimeouts(5.minutes, 5.seconds, 300.millis), "log-and-fail", "standalone", "redis.") + protected override def hocon: String = + """ + |play.cache.redis {} + """ - "default configuration" in new WithRedisInstanceManager( - """ - |play.cache.redis {} - """ - ) { - val defaultCache: RedisInstanceProvider = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort, database = 0), defaults) + private val defaultCache: RedisInstanceProvider = + RedisStandalone( + name = defaultCacheName, + host = RedisHost(localhost, defaultPort, database = 0), + settings = defaultsSettings + ) manager mustEqual RedisInstanceManagerTest(defaultCacheName)(defaultCache) manager.instanceOf(defaultCacheName) mustEqual defaultCache - manager.instanceOfOption(defaultCacheName) must beSome(defaultCache) - manager.instanceOfOption("other") must beNone + manager.instanceOfOption(defaultCacheName) mustEqual Some(defaultCache) + manager.instanceOfOption("other") mustEqual None manager.defaultInstance mustEqual defaultCache } - "single default instance" in new WithRedisInstanceManager( + "single default instance" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | host: redis.localhost.cz @@ -46,13 +50,15 @@ class RedisInstanceManagerSpec extends Specification { | recovery: log-and-fail |} """ - ) { + private val settings: RedisSettings = RedisSettingsTest(invocationContext = "my-dispatcher", invocationPolicy = "eager", timeout = RedisTimeouts(5.minutes, 5.seconds, 300.millis), recovery = "log-and-fail", source = "standalone", prefix = "redis.") + manager mustEqual RedisInstanceManagerTest(defaultCacheName)( - RedisStandalone(defaultCacheName, RedisHost("redis.localhost.cz", 6378, database = 2, password = "something"), extras) + RedisStandalone(defaultCacheName, RedisHost("redis.localhost.cz", 6378, database = 2, password = "something"), settings) ) } - "named caches" in new WithRedisInstanceManager( + "named caches" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -83,9 +89,12 @@ class RedisInstanceManagerSpec extends Specification { | default-cache: other |} """ - ) { - val defaultCache: RedisInstanceProvider = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort, database = 1), extras) - val otherCache: RedisInstanceProvider = RedisStandalone("other", RedisHost("redis.localhost.cz", 6378, database = 2, password = "something"), defaults) + + private val otherSettings: RedisSettings = RedisSettingsTest( + invocationContext = "my-dispatcher", invocationPolicy = "eager", timeout = RedisTimeouts(5.minutes, 5.seconds, 300.millis), recovery = "log-and-fail", source = "standalone", prefix = "redis.") + + private val defaultCache: RedisInstanceProvider = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort, database = 1), otherSettings) + private val otherCache: RedisInstanceProvider = RedisStandalone("other", RedisHost("redis.localhost.cz", 6378, database = 2, password = "something"), defaultsSettings) manager mustEqual RedisInstanceManagerTest("other")(defaultCache, otherCache) manager.instanceOf(defaultCacheName) mustEqual defaultCache @@ -93,7 +102,8 @@ class RedisInstanceManagerSpec extends Specification { manager.defaultInstance mustEqual otherCache } - "cluster mode" in new WithRedisInstanceManager( + "cluster mode" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -109,15 +119,16 @@ class RedisInstanceManagerSpec extends Specification { | } |} """ - ) { - def node(port: Int) = RedisHost(localhost, port) + private def node(port: Int) = RedisHost(localhost, port) manager mustEqual RedisInstanceManagerTest(defaultCacheName)( - RedisCluster(defaultCacheName, node(6380) :: node(6381) :: node(6382) :: node(6383) :: Nil, defaults.copy(source = "cluster")) + RedisCluster( + name = defaultCacheName, nodes = node(6380) :: node(6381) :: node(6382) :: node(6383) :: Nil, settings = defaultsSettings.copy(source = "cluster")) ) } - "AWS cluster mode" in new WithRedisInstanceManager( + "AWS cluster mode" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -128,13 +139,13 @@ class RedisInstanceManagerSpec extends Specification { | } |} """ - ) { - val provider = manager.defaultInstance.asInstanceOf[ResolvedRedisInstance] - val instance = provider.instance.asInstanceOf[RedisCluster] + private val provider = manager.defaultInstance.asInstanceOf[ResolvedRedisInstance] + private val instance = provider.instance.asInstanceOf[RedisCluster] instance.nodes must contain(RedisHost("127.0.0.1", 6379)) } - "sentinel mode" in new WithRedisInstanceManager( + "sentinel mode" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -152,48 +163,53 @@ class RedisInstanceManagerSpec extends Specification { | } |} """ - ) { - def node(port: Int) = RedisHost(localhost, port) + + private def node(port: Int) = RedisHost(localhost, port) manager mustEqual RedisInstanceManagerTest(defaultCacheName)( - RedisSentinel(defaultCacheName, "primary", node(6380) :: node(6381) :: node(6382) :: Nil, defaults.copy(source = "sentinel"), password = "my-password", database = 1) + RedisSentinel( + name = defaultCacheName, masterGroup = "primary", sentinels = node(6380) :: node(6381) :: node(6382) :: Nil, settings = defaultsSettings.copy(source = "sentinel"), password = "my-password", database = 1) ) } - "connection string mode" in new WithRedisInstanceManager( + "connection string mode" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | source: "connection-string" | connection-string: "redis://localhost:6379" |} """ - ) { - manager mustEqual RedisInstanceManagerTest(defaultCacheName)( - RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort), defaults.copy(source = "connection-string")) + + manager mustEqual RedisInstanceManagerTest(defaultCacheName)( + RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort), defaultsSettings.copy(source = "connection-string")) ) } - "custom mode" in new WithRedisInstanceManager( + "custom mode" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | source: custom |} """ - ) { + manager mustEqual RedisInstanceManagerTest(defaultCacheName)(defaultCacheName) } - "typo in mode with simple syntax" in new WithRedisInstanceManager( + "typo in mode with simple syntax" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | source: typo |} """ - ) { - manager.defaultInstance must throwA[IllegalStateException] + + the[IllegalStateException] thrownBy manager.defaultInstance } - "typo in mode with advanced syntax" in new WithRedisInstanceManager( + "typo in mode with advanced syntax" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -203,11 +219,12 @@ class RedisInstanceManagerSpec extends Specification { | } |} """ - ) { - manager.defaultInstance must throwA[IllegalStateException] + + the[IllegalStateException] thrownBy manager.defaultInstance } - "fail when requesting undefined cache" in new WithRedisInstanceManager( + "fail when requesting undefined cache" in new TestCase { + protected override def hocon: String = """ |play.cache.redis { | instances { @@ -219,12 +236,57 @@ class RedisInstanceManagerSpec extends Specification { | default-cache: other |} """ - ) { - manager.instanceOfOption(defaultCacheName) must beSome[RedisInstanceProvider] - manager.instanceOfOption("other") must beNone + manager.instanceOfOption(defaultCacheName) mustBe a[Some[_]] + manager.instanceOfOption("other") mustEqual None - manager.instanceOf("other") must throwA[IllegalArgumentException] - manager.defaultInstance must throwA[IllegalArgumentException] + the[IllegalArgumentException] thrownBy manager.instanceOf("other") + the[IllegalArgumentException] thrownBy manager.defaultInstance + } + + private trait TestCase { + + private implicit val loader: ConfigLoader[RedisInstanceManager] = RedisInstanceManager + + private val configuration: Configuration = Helpers.configuration.fromHocon(hocon) + + protected val manager: RedisInstanceManager = configuration.get[RedisInstanceManager]("play.cache.redis") + + protected def hocon: String + + protected implicit def implicitlyInstance2resolved(instance: RedisInstance): RedisInstanceProvider = + new ResolvedRedisInstance(instance) + + protected implicit def implicitlyString2unresolved(name: String): RedisInstanceProvider = + new UnresolvedRedisInstance(name) + + protected final case class RedisInstanceManagerTest( + default: String)( + providers: RedisInstanceProvider* + ) extends RedisInstanceManager { + + override def caches: Set[String] = providers.map(_.name).toSet + + override def instanceOfOption(name: String): Option[RedisInstanceProvider] = providers.find(_.name == name) + + override def defaultInstance: RedisInstanceProvider = providers.find(_.name == default) getOrElse { + throw new RuntimeException("Default instance is not defined.") + } + + } } } + + + + + + + + + + + + + + diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala deleted file mode 100644 index 7c81c3dd..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala +++ /dev/null @@ -1,25 +0,0 @@ -package play.api.cache.redis.configuration - -import play.api.ConfigLoader -import play.api.cache.redis._ - -/** - * Simple implementation for tests - */ -case class RedisInstanceManagerTest(default: String)(providers: RedisInstanceProvider*) extends RedisInstanceManager { - - def caches = providers.map(_.name).toSet - - def instanceOfOption(name: String) = providers.find(_.name == name) - - def defaultInstance = providers.find(_.name == default) getOrElse { - throw new RuntimeException("Default instance is not defined.") - } -} - -abstract class WithRedisInstanceManager(hocon: String) extends WithConfiguration(hocon) { - - private implicit val loader: ConfigLoader[RedisInstanceManager] = RedisInstanceManager - - protected val manager = configuration.get[RedisInstanceManager]("play.cache.redis") -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala index 0a9e016e..c3ff1d67 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala @@ -1,14 +1,15 @@ package play.api.cache.redis.configuration -import org.specs2.mutable.Specification +import play.api.cache.redis.test.UnitSpec -class RedisInstanceProviderSpec extends Specification { - import play.api.cache.redis.Implicits._ +class RedisInstanceProviderSpec extends UnitSpec { - val defaultCache = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort, database = 0), defaults) + private val defaultCache: RedisStandalone = + RedisStandalone( + name = defaultCacheName, host = RedisHost(localhost, defaultPort, database = Some(0)), settings = defaultsSettings) - implicit val resolver: RedisInstanceResolver = new RedisInstanceResolver { - def resolve = { +private implicit val resolver: RedisInstanceResolver = new RedisInstanceResolver { + def resolve: PartialFunction[String, RedisStandalone] = { case `defaultCacheName` => defaultCache } } @@ -22,6 +23,6 @@ class RedisInstanceProviderSpec extends Specification { } "fail when not able to resolve" in { - new UnresolvedRedisInstance("other").resolved must throwA[Exception] + the[Exception] thrownBy new UnresolvedRedisInstance("other").resolved } } diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpec.scala index e3eac464..c9d752a8 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpec.scala @@ -1,65 +1,74 @@ package play.api.cache.redis.configuration import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.test.{Helpers, ImplicitOptionMaterialization, UnitSpec} -import org.specs2.mutable.Specification - -class RedisTimeoutsSpec extends Specification { - import Implicits._ +class RedisTimeoutsSpec extends UnitSpec with ImplicitOptionMaterialization{ private def orDefault = RedisTimeouts(1.second, None, 500.millis) - "load defined timeouts" in new WithConfiguration( - """ - |play.cache.redis { - | - | sync-timeout: 5s - | redis-timeout: 7s - | connection-timeout: 300ms - |} + "load defined timeouts" in { + val configuration = Helpers.configuration.fromHocon { + """ + |play.cache.redis { + | + | sync-timeout: 5s + | redis-timeout: 7s + | connection-timeout: 300ms + |} """ - ) { - RedisTimeouts.load(config, "play.cache.redis")(RedisTimeouts.requiredDefault) mustEqual RedisTimeouts(5.seconds, 7.seconds, 300.millis) + } + val expected = RedisTimeouts(5.seconds, 7.seconds, 300.millis) + val actual = RedisTimeouts.load(configuration.underlying, "play.cache.redis")(RedisTimeouts.requiredDefault) + actual mustEqual expected } - "load defined high timeouts" in new WithConfiguration( + "load defined high timeouts" in { + val configuration = Helpers.configuration.fromHocon { + """ + |play.cache.redis { + | + | sync-timeout: 500s + | redis-timeout: 700s + | connection-timeout: 900s + |} """ - |play.cache.redis { - | - | sync-timeout: 500s - | redis-timeout: 700s - | connection-timeout: 900s - |} - """ - ) { - RedisTimeouts.load(config, "play.cache.redis")(RedisTimeouts.requiredDefault) mustEqual RedisTimeouts(500.seconds, 700.seconds, 900.seconds) + } + val expected = RedisTimeouts(500.seconds, 700.seconds, 900.seconds) + val actual = RedisTimeouts.load(configuration.underlying, "play.cache.redis")(RedisTimeouts.requiredDefault) + actual mustEqual expected } - "load with default timeouts" in new WithConfiguration( - """ - |play.cache.redis { - |} + "load with default timeouts" in { + val configuration = Helpers.configuration.fromHocon { + """ + |play.cache.redis { + |} """ - ) { - RedisTimeouts.load(config, "play.cache.redis")(orDefault) mustEqual RedisTimeouts(1.second, None, connection = 500.millis) + } + val expected = RedisTimeouts(1.second, None, connection = 500.millis) + val actual = RedisTimeouts.load(configuration.underlying, "play.cache.redis")(orDefault) + actual mustEqual expected } - "load with disabled timeouts" in new WithConfiguration( - """ - |play.cache.redis { - | redis-timeout: null - | connection-timeout: null - |} + "load with disabled timeouts" in { + val configuration = Helpers.configuration.fromHocon { + """ + |play.cache.redis { + | redis-timeout: null + | connection-timeout: null + |} """ - ) { - RedisTimeouts.load(config, "play.cache.redis")(orDefault) mustEqual RedisTimeouts(sync = 1.second, redis = None, connection = None) + } + val expected = RedisTimeouts(sync = 1.second, redis = None, connection = None) + val actual = RedisTimeouts.load(configuration.underlying, "play.cache.redis")(orDefault) + actual mustEqual expected } "load defaults" in { - RedisTimeouts.requiredDefault.sync must throwA[RuntimeException] - RedisTimeouts.requiredDefault.redis must beNone - RedisTimeouts.requiredDefault.connection must beNone + the[RuntimeException] thrownBy RedisTimeouts.requiredDefault.sync + RedisTimeouts.requiredDefault.redis mustEqual None + RedisTimeouts.requiredDefault.connection mustEqual None } } diff --git a/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpec.scala b/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpec.scala index 21ede99e..00214d1d 100644 --- a/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpec.scala @@ -1,45 +1,43 @@ package play.api.cache.redis.connector -import scala.concurrent.{ExecutionContext, Future} - import play.api.cache.redis._ +import play.api.cache.redis.test.{AsyncUnitSpec, SimulatedException} -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.{ExecutionContext, Future} -class ExpectedFutureSpec(implicit ee: ExecutionEnv) extends Specification { +class ExpectedFutureSpec extends AsyncUnitSpec { import ExpectedFutureSpec._ - class Suite(name: String)(implicit f: ExpectationBuilder[String]) { + private class TestSuite(name: String)(implicit f: ExpectationBuilder[String]) { - name >> { + name should { "expected value" in { - Future.successful("expected").asExpected must beEqualTo("ok").await + Future.successful("expected").asExpected.assertingEqual("ok") } "unexpected value" in { - Future.successful("unexpected").asExpected must throwA[UnexpectedResponseException].await + Future.successful("unexpected").asExpected.assertingFailure[UnexpectedResponseException] } "failing expectation" in { - Future.successful("failing").asExpected must throwA[ExecutionFailedException].await + Future.successful("failing").asExpected.assertingFailure[ExecutionFailedException] } "failing future inside redis" in { - Future.failed[String](TimeoutException(simulatedFailure)).asExpected must throwA[TimeoutException].await + Future.failed[String](TimeoutException(SimulatedException)).asExpected.assertingFailure[TimeoutException] } "failing future with runtime exception" in { - Future.failed[String](simulatedFailure).asExpected must throwA[ExecutionFailedException].await + Future.failed[String](SimulatedException).asExpected.assertingFailure[ExecutionFailedException] } } } - new Suite("Execution without the key")((future: Future[String]) => future.executing(cmd)) + new TestSuite("Execution without the key")((future: Future[String]) => future.executing(cmd)) - new Suite("Execution with the key")((future: Future[String]) => future.executing(cmd).withKey("key")) + new TestSuite("Execution with the key")((future: Future[String]) => future.executing(cmd).withKey("key")) "building a command" in { Future.successful("expected").executing(cmd).toString mustEqual s"ExpectedFuture($cmd)" @@ -52,20 +50,16 @@ class ExpectedFutureSpec(implicit ee: ExecutionEnv) extends Specification { object ExpectedFutureSpec { - val cmd = "TEST CMD" + private val cmd = "TEST CMD" - def simulatedFailure = new RuntimeException("Simulated runtime failure") - - val expectation: PartialFunction[Any, String] = { - case "failing" => throw simulatedFailure + private val expectation: PartialFunction[Any, String] = { + case "failing" => throw SimulatedException case "expected" => "ok" } - implicit class ExpectationBuilder[T](val f: Future[T] => ExpectedFuture[String]) extends AnyVal { - def apply(future: Future[T]): ExpectedFuture[String] = f(future) - } + private type ExpectationBuilder[T] = Future[T] => ExpectedFuture[String] - implicit class FutureBuilder[T](val future: Future[T]) extends AnyVal { + private implicit class FutureBuilder[T](private val future: Future[T]) extends AnyVal { def asExpected(implicit ev: ExpectationBuilder[T], context: ExecutionContext): Future[String] = ev(future).expects(expectation) def asCommand(implicit ev: ExpectationBuilder[T]): String = ev(future).toString } diff --git a/src/test/scala/play/api/cache/redis/connector/FailEagerlySpec.scala b/src/test/scala/play/api/cache/redis/connector/FailEagerlySpec.scala index b282804d..8f59cb8c 100644 --- a/src/test/scala/play/api/cache/redis/connector/FailEagerlySpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/FailEagerlySpec.scala @@ -1,45 +1,47 @@ package play.api.cache.redis.connector -import scala.concurrent.Future -import scala.concurrent.duration._ - -import play.api.cache.redis._ - -import akka.actor.ActorSystem -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import akka.actor.{ActorSystem, Scheduler} +import play.api.cache.redis.test._ -class FailEagerlySpec(implicit ee: ExecutionEnv) extends Specification with WithApplication { +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future, Promise} +class FailEagerlySpec extends AsyncUnitSpec with ImplicitFutureMaterialization { import FailEagerlySpec._ - import Implicits._ - import MockitoImplicits._ - "FailEagerly" should { + test("not fail regular requests when disconnected") { failEagerly => + val cmd = mock[RedisCommandTest[String]] + (() => cmd.returning).expects().returns("response") + // run the test + failEagerly.isConnected mustEqual false + failEagerly.send(cmd).assertingEqual("response") + } - "not fail regular requests when disconnected" in { - val impl = new FailEagerlyImpl - val cmd = mock[RedisCommandTest].returning returns "response" - // run the test - impl.isConnected must beFalse - impl.send[String](cmd) must beEqualTo("response").await - } + test("do fail long running requests when disconnected") { failEagerly => + val cmd = mock[RedisCommandTest[String]] + (() => cmd.returning).expects().returns(Promise[String]().future) + // run the test + failEagerly.isConnected mustEqual false + failEagerly.send(cmd).assertingFailure[redis.actors.NoConnectionException.type] + } - "do fail long running requests when disconnected" in { - val impl = new FailEagerlyImpl - val cmd = mock[RedisCommandTest].returning returns Future.after(seconds = 3, "response") - // run the test - impl.isConnected must beFalse - impl.send[String](cmd) must throwA[redis.actors.NoConnectionException.type].awaitFor(5.seconds) - } + test("not fail long running requests when connected ") { failEagerly => + val cmd = mock[RedisCommandTest[String]] + (() => cmd.returning).expects().returns(Promise[String]().future) + failEagerly.markConnected() + // run the test + failEagerly.isConnected mustEqual true + failEagerly.send(cmd).assertTimeout(200.millis) + } - "not fail long running requests when connected " in { - val impl = new FailEagerlyImpl - val cmd = mock[RedisCommandTest].returning returns Future.after(seconds = 3, "response") - impl.markConnected() - // run the test - impl.isConnected must beTrue - impl.send[String](cmd) must beEqualTo("response").awaitFor(5.seconds) + def test(name: String)(f: FailEagerlyImpl => Future[Assertion]): Unit = { + name in { + val system = ActorSystem("test", classLoader = Some(getClass.getClassLoader)) + val application = StoppableApplication(system) + application.runAsyncInApplication { + val impl = new FailEagerlyImpl()(system) + f(impl) + } } } } @@ -49,27 +51,27 @@ object FailEagerlySpec { import redis.RedisCommand import redis.protocol.RedisReply - trait RedisCommandTest extends RedisCommand[RedisReply, String] { - def returning: Future[String] + trait RedisCommandTest[T] extends RedisCommand[RedisReply, T] { + def returning: Future[T] } class FailEagerlyBase(implicit system: ActorSystem) extends RequestTimeout { - protected implicit val scheduler = system.scheduler - implicit val executionContext = system.dispatcher + protected implicit val scheduler: Scheduler = system.scheduler + implicit val executionContext: ExecutionContext = system.dispatcher - def send[T](redisCommand: RedisCommand[_ <: RedisReply, T]) = { - redisCommand.asInstanceOf[RedisCommandTest].returning.asInstanceOf[Future[T]] + def send[T](redisCommand: RedisCommand[_ <: RedisReply, T]): Future[T] = { + redisCommand.asInstanceOf[RedisCommandTest[T]].returning } } - class FailEagerlyImpl(implicit system: ActorSystem) extends FailEagerlyBase with FailEagerly { + final class FailEagerlyImpl(implicit system: ActorSystem) extends FailEagerlyBase with FailEagerly { - def connectionTimeout = Some(300.millis) + def connectionTimeout: Option[FiniteDuration] = Some(100.millis) - def isConnected = connected + def isConnected: Boolean = connected - def markConnected() = connected = true + def markConnected(): Unit = connected = true - def markDisconnected() = connected = false + def markDisconnected(): Unit = connected = false } } diff --git a/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala b/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala deleted file mode 100644 index 5d70f14e..00000000 --- a/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala +++ /dev/null @@ -1,30 +0,0 @@ -package play.api.cache.redis.connector - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ - -import play.api.cache.redis._ -import play.api.cache.redis.impl._ - -import org.specs2.execute.{AsResult, Result} -import org.specs2.specification.{Around, Scope} -import redis.RedisCommands - -abstract class MockedConnector extends Around with Scope with WithRuntime with WithApplication { - import MockitoImplicits._ - - protected val serializer = mock[AkkaSerializer] - - protected val commands = mock[RedisCommands] - - protected val connector: RedisConnector = new RedisConnectorImpl(serializer, commands) - - def around[T: AsResult](t: => T): Result = { - AsResult.effectively(t) - } -} - -trait WithRuntime { - - implicit protected val runtime: RedisRuntime = RedisRuntime("connector", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) -} diff --git a/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala index 3c3b8f79..c5658052 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala @@ -1,98 +1,88 @@ package play.api.cache.redis.connector -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ - +import akka.actor.ActorSystem import play.api.cache.redis._ import play.api.cache.redis.configuration._ import play.api.cache.redis.impl._ -import play.api.inject.ApplicationLifecycle - -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification -import org.specs2.specification.{AfterAll, BeforeAll} - -/** - *Specification of the low level connector implementing basic commands
- */ -class RedisClusterSpec(implicit ee: ExecutionEnv) extends Specification with WithApplication with ClusterRedisContainer { - - args(skipAll=true) - - import Implicits._ - - implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] +import play.api.cache.redis.test._ - implicit private val runtime: RedisRuntime = RedisRuntime("cluster", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) - - private val serializer = new AkkaSerializerImpl(system) - - private lazy val containerIpAddress = container.containerIpAddress - - private lazy val clusterInstance = RedisCluster( - name = defaultCacheName, - nodes = RedisHost(containerIpAddress, container.mappedPort(7000)) :: - RedisHost(containerIpAddress, container.mappedPort(7001)) :: - RedisHost(containerIpAddress, container.mappedPort(7002)) :: - RedisHost(containerIpAddress, container.mappedPort(7003)) :: - Nil, - settings = defaults - ) - - private lazy val connector: RedisConnector = new RedisConnectorProvider(clusterInstance, serializer).get - - val prefix = "cluster-test" - - "Redis cluster" should { +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} - "pong on ping" in new TestCase { - connector.ping() must not(throwA[Throwable]).await - } +class RedisClusterSpec extends IntegrationSpec with RedisClusterContainer { - "miss on get" in new TestCase { - connector.get[String](s"$prefix-$idx") must beNone.await - } + test("pong on ping") { connector => + connector.ping().assertingSuccess + } - "hit after set" in new TestCase { - connector.set(s"$prefix-$idx", "value") must beTrue.await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - connector.get[String](s"$prefix-$idx") must beSome("value").await - } + test("miss on get") { connector => + connector.get[String]("miss-on-get").assertingEqual(None) + } - "ignore set if not exists when already defined" in new TestCase { - connector.set(s"$prefix-if-not-exists-when-exists", "previous") must beTrue.await - connector.set(s"$prefix-if-not-exists-when-exists", "value", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists-when-exists") must beSome("previous").await - } + test("hit after set") { connector => + for { + _ <- connector.set("hit-after-set", "value").assertingEqual(true) + _ <- connector.get[String]("hit-after-set").assertingEqual(Some("value")) + } yield Passed + } - "perform set if not exists when undefined" in new TestCase { - connector.get[String](s"$prefix-if-not-exists") must beNone.await - connector.set(s"$prefix-if-not-exists", "value", ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - connector.set(s"$prefix-if-not-exists", "other", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - } + test("ignore set if not exists when already defined") { connector => + for { + _ <- connector.set("if-not-exists-when-exists", "previous").assertingEqual(true) + _ <- connector.set("if-not-exists-when-exists", "value", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String]("if-not-exists-when-exists").assertingEqual(Some("previous")) + } yield Passed + } - "perform set if not exists with expiration" in new TestCase { - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - connector.set(s"$prefix-if-not-exists-with-expiration", "value", 2.seconds, ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beSome("value").await - // wait until the first duration expires - Future.after(3) must not(throwA[Throwable]).awaitFor(4.seconds) - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - } + test("perform set if not exists when undefined") { connector => + for { + _ <- connector.get[String]("if-not-exists").assertingEqual(None) + _ <- connector.set("if-not-exists", "value", ifNotExists = true).assertingEqual(true) + _ <- connector.get[String]("if-not-exists").assertingEqual(Some("value")) + _ <- connector.set("if-not-exists", "other", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String]("if-not-exists").assertingEqual(Some("value")) + } yield Passed } - override def beforeAll() = { - super.beforeAll() - // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap { - keys => Future.sequence(keys.map(connector.remove(_))) - }.awaitForFuture + test("perform set if not exists with expiration") { connector => + for { + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(None) + _ <- connector.set("if-not-exists-with-expiration", "value", 500.millis, ifNotExists = true).assertingEqual(true) + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(Some("value")) + // wait until the first duration expires + _ <- Future.after(700.millis, ()) + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(None) + } yield Passed } - override def afterAll() = { - Shutdown.run.awaitForFuture - super.afterAll() + def test(name: String)(f: RedisConnector => Future[Assertion]): Unit = { + name in { + implicit val system: ActorSystem = ActorSystem("test", classLoader = Some(getClass.getClassLoader)) + implicit val runtime: RedisRuntime = RedisRuntime("cluster", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit val application: StoppableApplication = StoppableApplication(system) + val serializer = new AkkaSerializerImpl(system) + + lazy val clusterInstance = RedisCluster( + name = "play", + nodes = 0.until(redisMaster).map { i => + RedisHost(container.containerIpAddress, container.mappedPort(initialPort + i)) + }.toList, + settings = RedisSettings.load( + config = Helpers.configuration.default.underlying, + path = "play.cache.redis" + ) + ) + + application.runAsyncInApplication { + for { + connector <- Future(new RedisConnectorProvider(clusterInstance, serializer).get) + // initialize the connector by flushing the database + keys <- connector.matching("*") + _ <- Future.sequence(keys.map(connector.remove(_))) + // run the test + _ <- f(connector) + } yield Passed + } + } } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala index 6a2221d0..a12cfe26 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala @@ -1,182 +1,275 @@ package play.api.cache.redis.connector -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.reflect.ClassTag -import scala.util.Failure - import play.api.cache.redis._ - -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import play.api.cache.redis.test._ import redis._ +import redis.api.{BEFORE, ListPivot} -class RedisConnectorFailureSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - - import Implicits._ +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} +import scala.reflect.ClassTag +import scala.util.{Failure, Success} - import org.mockito.ArgumentMatchers._ +class RedisConnectorFailureSpec extends AsyncUnitSpec with ImplicitFutureMaterialization { - private val key = "key" - private val value = "value" private val score = 1D + private val encodedValue = "encoded" + private val disconnected = Future.failed(SimulatedException) - private val simulatedEx = new RuntimeException("Simulated failure.") - private val simulatedFailure = Failure(simulatedEx) + "Serializer fail" when { - private val someValue = Some(value) - - private val disconnected = Future.failed(new IllegalStateException("Simulated redis status: disconnected.")) + test("serialization fails") { (serializer, _, connector) => + for { + _ <- serializer.failOnEncode(cacheValue) + _ <- connector.set(cacheKey, cacheValue).assertingFailure[SerializationException] + } yield Passed + } - private def anySerializer = org.mockito.ArgumentMatchers.any[ByteStringSerializer[String]] - private def anyDeserializer = org.mockito.ArgumentMatchers.any[ByteStringDeserializer[String]] + test("decoder fails") { (serializer, commands, connector) => + for { + _ <- serializer.failOnDecode(cacheValue) + _ = (commands.get[String](_: String)(_: ByteStringDeserializer[String])).expects(cacheKey, *).returns(Some(cacheValue)) + _ <- connector.get[String](cacheKey).assertingFailure[SerializationException] + } yield Passed + } + } - "Serializer failure" should { + "Redis returns error code" when { - "fail when serialization fails" in new MockedConnector { - serializer.encode(any[Any]) returns simulatedFailure - // run the test - connector.set(key, value) must throwA[SerializationException].await + test("SET returning false") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.set[String](_: String, _: String, _: Option[Long], _: Option[Long], _: Boolean, _: Boolean)(_: ByteStringSerializer[String])) + .expects(cacheKey, encodedValue, None, None, false, false, *) + .returns(false) + _ <- connector.set(cacheKey, cacheValue).assertingEqual(false) + } yield Passed } - "fail when decoder fails" in new MockedConnector { - serializer.decode(anyString)(any()) returns simulatedFailure - commands.get[String](key) returns someValue - // run the test - connector.get[String](key) must throwA[SerializationException].await + test("EXPIRE returning false") { (_, commands, connector) => + for { + _ <- (commands.expire _).expects(cacheKey, 1.minute.toSeconds).returns(false) + _ <- connector.expire(cacheKey, 1.minute).assertingSuccess + } yield Passed } + } - "Redis returning error code" should { + "Connector fails" when { + + test("failed SET") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.set[String](_: String, _: String, _: Option[Long], _: Option[Long], _: Boolean, _: Boolean)(_: ByteStringSerializer[String])) + .expects(cacheKey, encodedValue, None, None, false, false, *) + .returns(disconnected) + + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.set[String](_: String, _: String, _: Option[Long], _: Option[Long], _: Boolean, _: Boolean)(_: ByteStringSerializer[String])) + .expects(cacheKey, encodedValue, None, Some(1.minute.toMillis), false, false, *) + .returns(disconnected) + + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.set[String](_: String, _: String, _: Option[Long], _: Option[Long], _: Boolean, _: Boolean)(_: ByteStringSerializer[String])) + .expects(cacheKey, encodedValue, None, None, true, false, *) + .returns(disconnected) + + _ <- connector.set(cacheKey, cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + _ <- connector.set(cacheKey, cacheValue, 1.minute).assertingFailure[ExecutionFailedException, SimulatedException] + _ <- connector.set(cacheKey, cacheValue, ifNotExists = true).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed + } - "SET returning false" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.set[String](anyString, anyString, any[Some[Long]], any[Some[Long]], anyBoolean, anyBoolean)(anySerializer) returns false - // run the test - connector.set(key, value) must not(throwA[Throwable]).await + test("failed MSET") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.mset[String](_: Map[String, String])(_: ByteStringSerializer[String])) + .expects(Map(cacheKey -> encodedValue), *) + .returns(disconnected) + _ <- connector.mSet(cacheKey -> cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "EXPIRE returning false" in new MockedConnector { - commands.expire(anyString, any[Long]) returns false - // run the test - connector.expire(key, 1.minute) must not(throwA[Throwable]).await + test("failed MSETNX") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.msetnx[String](_: Map[String, String])(_: ByteStringSerializer[String])) + .expects(Map(cacheKey -> encodedValue), *) + .returns(disconnected) + _ <- connector.mSetIfNotExist(cacheKey -> cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - } - "Connector failure" should { + test("failed EXPIRE") { (_, commands, connector) => + for { + _ <- (commands.expire(_: String, _: Long)).expects(cacheKey, 1.minute.toSeconds).returns(disconnected) + _ <- connector.expire(cacheKey, 1.minute).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed + } - "failed SET" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.set(anyString, anyString, any, any, anyBoolean, anyBoolean)(anySerializer) returns disconnected - // run the test - connector.set(key, value) must throwA[ExecutionFailedException].await - connector.set(key, value, 1.minute) must throwA[ExecutionFailedException].await - connector.set(key, value, ifNotExists = true) must throwA[ExecutionFailedException].await + test("failed INCRBY") { (_, commands, connector) => + for { + _ <- (commands.incrby(_: String, _: Long)).expects(cacheKey, 1L).returns(disconnected) + _ <- connector.increment(cacheKey, 1L).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed MSET" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.mset[String](any[Map[String, String]])(anySerializer) returns disconnected - // run the test - connector.mSet(key -> value) must throwA[ExecutionFailedException].await + test("failed LRANGE") { (_, commands, connector) => + for { + _ <- (commands.lrange[String](_: String, _: Long, _: Long)(_: ByteStringDeserializer[String])) + .expects(cacheKey, 0, -1, *) + .returns(disconnected) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed MSETNX" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.msetnx[String](any[Map[String, String]])(anySerializer) returns disconnected - // run the test - connector.mSetIfNotExist(key -> value) must throwA[ExecutionFailedException].await + test("failed LREM") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.lrem(_: String, _: Long, _: String)(_: ByteStringSerializer[String])) + .expects(cacheKey, 2L, encodedValue, *) + .returns(disconnected) + _ <- connector.listRemove(cacheKey, cacheValue, 2).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed EXPIRE" in new MockedConnector { - commands.expire(anyString, anyLong) returns disconnected - // run the test - connector.expire(key, 1.minute) must throwA[ExecutionFailedException].await + test("failed LTRIM") { (_, commands, connector) => + for { + _ <- (commands.ltrim(_: String, _: Long, _: Long)).expects(cacheKey, 1L, 5L).returns(disconnected) + _ <- connector.listTrim(cacheKey, 1, 5).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed INCRBY" in new MockedConnector { - commands.incrby(anyString, anyLong) returns disconnected - // run the test - connector.increment(key, 1L) must throwA[ExecutionFailedException].await + test("failed LINSERT") { (serializer, commands, connector) => + for { + _ <- serializer.encode("pivot", "encodedPivot") + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.linsert[String](_: String, _: ListPivot, _: String, _: String)(_: ByteStringSerializer[String])) + .expects(cacheKey, BEFORE, "encodedPivot", encodedValue, *) + .returns(disconnected) + // run the test + _ <- connector.listInsert(cacheKey, "pivot", cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed LRANGE" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.lrange[String](anyString, anyLong, anyLong)(anyDeserializer) returns disconnected - // run the test - connector.listSlice[String](key, 0, -1) must throwA[ExecutionFailedException].await + test("failed HINCRBY") { (_, commands, connector) => + for { + _ <- (commands.hincrby(_: String, _: String, _: Long)).expects(cacheKey, "field", 1L).returns(disconnected) + // run the test + _ <- connector.hashIncrement(cacheKey, "field", 1).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed LREM" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.lrem(anyString, anyLong, anyString)(anySerializer) returns disconnected - // run the test - connector.listRemove(key, value, 2) must throwA[ExecutionFailedException].await + test("failed HSET") { (serializer, commands, connector) => +for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.hset[String](_: String, _: String, _: String)(_: ByteStringSerializer[String])) + .expects(cacheKey, "field", encodedValue, *) + .returns(disconnected) + _ <- connector.hashSet(cacheKey, "field", cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed LTRIM" in new MockedConnector { - commands.ltrim(anyString, anyLong, anyLong) returns disconnected - // run the test - connector.listTrim(key, 1, 5) must throwA[ExecutionFailedException].await + test("failed ZADD") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.zaddMock[String](_: String, _: Seq[(Double, String)])(_: ByteStringSerializer[String])) + .expects(cacheKey, Seq((score, encodedValue)), *) + .returns(disconnected) + _ <- connector.sortedSetAdd(cacheKey, (score, cacheValue)).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed LINSERT" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.linsert[String](anyString, any[api.ListPivot], anyString, anyString)(anySerializer) returns disconnected - // run the test - connector.listInsert(key, "pivot", value) must throwA[ExecutionFailedException].await + test("failed ZCARD") { (_, commands, connector) => + for { + _ <- (commands.zcard(_: String)).expects(cacheKey).returns(disconnected) + _ <- connector.sortedSetSize(cacheKey).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed HINCRBY" in new MockedConnector { - commands.hincrby(anyString, anyString, anyLong) returns disconnected - // run the test - connector.hashIncrement(key, "field", 1) must throwA[ExecutionFailedException].await + test("failed ZSCORE") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.zscore[String](_: String, _: String)(_: ByteStringSerializer[String])) + .expects(cacheKey, encodedValue, *) + .returns(disconnected) + _ <- connector.sortedSetScore(cacheKey, cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed HSET" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.hset[String](anyString, anyString, anyString)(anySerializer) returns disconnected - // run the test - connector.hashSet(key, "field", value) must throwA[ExecutionFailedException].await + test("failed ZREM") { (serializer, commands, connector) => + for { + _ <- serializer.encode(cacheValue, encodedValue) + _ = (commands.zremMock(_: String, _: Seq[String])(_: ByteStringSerializer[String])) + .expects(cacheKey, Seq(encodedValue), *) + .returns(disconnected) + _ <- connector.sortedSetRemove(cacheKey, cacheValue).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed ZADD" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.zadd[String](anyString, any[(Double, String)])(anySerializer) returns disconnected - // run the test - connector.sortedSetAdd(key, (score, value)) must throwA[ExecutionFailedException].await + test("failed ZRANGE") { (_, commands, connector) => + for { + _ <- (commands.zrange[String](_: String, _: Long, _: Long)(_: ByteStringDeserializer[String])) + .expects(cacheKey, 1, 5, *) + .returns(disconnected) + _ <- connector.sortedSetRange[String](cacheKey, 1, 5).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } - "failed ZCARD" in new MockedConnector { - commands.zcard(anyString) returns disconnected - // run the test - connector.sortedSetSize(key) must throwA[ExecutionFailedException].await + test("failed ZREVRANGE") { (_, commands, connector) => + for { + _ <- (commands.zrevrange[String](_: String, _: Long, _: Long)(_: ByteStringDeserializer[String])) + .expects(cacheKey, 1, 5, *) + .returns(disconnected) + _ <- connector.sortedSetReverseRange[String](cacheKey, 1, 5).assertingFailure[ExecutionFailedException, SimulatedException] + } yield Passed } + } + + private def test(name: String)(f: (SerializerAssertions, RedisCommandsMock, RedisConnector) => Future[Assertion]): Unit = { + name in { + implicit val runtime: RedisRuntime = mock[RedisRuntime] + val serializer = mock[AkkaSerializer] + val commands = mock[RedisCommandsMock] + val connector: RedisConnector = new RedisConnectorImpl(serializer, commands) - "failed ZSCORE" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.zscore[String](anyString, anyString)(anySerializer) returns disconnected - // run the test - connector.sortedSetScore(key, value) must throwA[ExecutionFailedException].await + (() => runtime.context).expects().returns(ExecutionContext.global).anyNumberOfTimes() + + f(new SerializerAssertions(serializer), commands, connector) } + } + + private class SerializerAssertions(mock: AkkaSerializer) { - "failed ZREM" in new MockedConnector { - serializer.encode(anyString) returns "encoded" - commands.zrem[String](anyString, anyString)(anySerializer) returns disconnected - // run the test - connector.sortedSetRemove(key, value) must throwA[ExecutionFailedException].await + def failOnEncode[T](value: T): Future[Unit] = { + Future.successful { + (mock.encode(_: Any)).expects(value).returns(Failure(SimulatedException)) + } } - "failed ZRANGE" in new MockedConnector { - commands.zrange[String](anyString, anyLong, anyLong)(anyDeserializer) returns disconnected - // run the test - connector.sortedSetRange[String](key, 1, 5) must throwA[ExecutionFailedException].await + def encode[T](value: T, encoded: String): Future[Unit] = { + Future.successful { + (mock.encode(_: Any)).expects(value).returns(Success(encoded)) + } } - "failed ZREVRANGE" in new MockedConnector { - commands.zrevrange[String](anyString, anyLong, anyLong)(anyDeserializer) returns disconnected - // run the test - connector.sortedSetReverseRange[String](key, 1, 5) must throwA[ExecutionFailedException].await + def failOnDecode(value: String): Future[Unit] = { + Future.successful { + (mock.decode(_: String)(_: ClassTag[String])).expects(value, *).returns(Failure(SimulatedException)) + } } } + + private trait RedisCommandsMock extends RedisCommands { + + final override def zadd[V: ByteStringSerializer](key: String, scoreMembers: (Double, V)*): Future[Long] = + zaddMock(key, scoreMembers) + + def zaddMock[V: ByteStringSerializer](key: String, scoreMembers: Seq[(Double, V)]): Future[Long] + + override final def zrem[V: ByteStringSerializer](key: String, members: V*): Future[Long] = + zremMock(key, members) + + def zremMock[V: ByteStringSerializer](key: String, members: Seq[V]): Future[Long] + } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala deleted file mode 100644 index dcbfc156..00000000 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala +++ /dev/null @@ -1,511 +0,0 @@ -package play.api.cache.redis.connector - -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification -import play.api.cache.redis._ -import play.api.cache.redis.configuration.{RedisHost, RedisStandalone} -import play.api.cache.redis.impl._ -import play.api.inject.ApplicationLifecycle - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} - -/** - *Specification of the low level connector implementing basic commands
- */ -class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with WithApplication with StandaloneRedisContainer { - import Implicits._ - - implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] - - implicit private val runtime: RedisRuntime = RedisRuntime("connector", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) - - private val serializer = new AkkaSerializerImpl(system) - - private lazy val connector: RedisConnector = new RedisConnectorProvider( - RedisStandalone(defaultCacheName, RedisHost(container.containerIpAddress, container.mappedPort(defaultPort)), defaults), - serializer - ).get - - val prefix = "connector-test" - - "RedisConnector" should { - - "pong on ping" in new TestCase { - connector.ping() must not(throwA[Throwable]).await - } - - "miss on get" in new TestCase { - connector.get[String](s"$prefix-$idx") must beNone.await - } - - "hit after set" in new TestCase { - connector.set(s"$prefix-$idx", "value") must beTrue.await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - connector.get[String](s"$prefix-$idx") must beSome("value").await - } - - "ignore set if not exists when already defined" in new TestCase { - connector.set(s"$prefix-if-not-exists-when-exists", "previous") must beTrue.await - connector.set(s"$prefix-if-not-exists-when-exists", "value", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists-when-exists") must beSome("previous").await - } - - "perform set if not exists when undefined" in new TestCase { - connector.get[String](s"$prefix-if-not-exists") must beNone.await - connector.set(s"$prefix-if-not-exists", "value", ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - connector.set(s"$prefix-if-not-exists", "other", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - } - - "perform set if not exists with expiration" in new TestCase { - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - connector.set(s"$prefix-if-not-exists-with-expiration", "value", 2.seconds, ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beSome("value").await - // wait until the first duration expires - Future.after(3) must not(throwA[Throwable]).awaitFor(4.seconds) - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - } - - "hit after mset" in new TestCase { - connector.mSet(s"$prefix-mset-$idx-1" -> "value-1", s"$prefix-mset-$idx-2" -> "value-2").awaitForFuture - connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), Some("value-2"), None)).await - connector.mSet(s"$prefix-mset-$idx-3" -> "value-3", s"$prefix-mset-$idx-2" -> null).awaitForFuture - connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), None, Some("value-3"))).await - connector.mSet(s"$prefix-mset-$idx-3" -> null).awaitForFuture - connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), None, None)).await - } - - "ignore msetnx if already defined" in new TestCase { - connector.mSetIfNotExist(s"$prefix-msetnx-$idx-1" -> "value-1", s"$prefix-msetnx-$idx-2" -> "value-2") must beTrue.await - connector.mGet[String](s"$prefix-msetnx-$idx-1", s"$prefix-msetnx-$idx-2") must beEqualTo(List(Some("value-1"), Some("value-2"))).await - connector.mSetIfNotExist(s"$prefix-msetnx-$idx-3" -> "value-3", s"$prefix-msetnx-$idx-2" -> "value-2") must beFalse.await - } - - "expire refreshes expiration" in new TestCase { - connector.set(s"$prefix-$idx", "value", 2.second).await - connector.get[String](s"$prefix-$idx") must beSome("value").await - connector.expire(s"$prefix-$idx", 1.minute).awaitForFuture - // wait until the first duration expires - Future.after(3) must not(throwA[Throwable]).awaitFor(4.seconds) - connector.get[String](s"$prefix-$idx") must beSome("value").await - } - - "expires in returns finite duration" in new TestCase { - connector.set(s"$prefix-$idx", "value", 2.second).await - connector.expiresIn(s"$prefix-$idx") must beSome(beLessThanOrEqualTo(Duration("2 s"))).await - } - - "expires in returns infinite duration" in new TestCase { - connector.set(s"$prefix-$idx", "value").await - connector.expiresIn(s"$prefix-$idx") must beSome(Duration.Inf: Duration).await - } - - "expires in returns not defined key" in new TestCase { - connector.expiresIn(s"$prefix-$idx") must beNone.await - connector.set(s"$prefix-$idx", "value", 1.second).await - connector.expiresIn(s"$prefix-$idx") must beSome[Duration].await - // wait until the first duration expires - Future.after(2) must not(throwA[Throwable]).awaitFor(3.seconds) - connector.expiresIn(s"$prefix-$idx") must beNone.await - } - - "positive exists on existing keys" in new TestCase { - connector.set(s"$prefix-$idx", "value").await - connector.exists(s"$prefix-$idx") must beTrue.await - } - - "negative exists on expired and missing keys" in new TestCase { - connector.set(s"$prefix-$idx-1", "value", 1.second).await - // wait until the duration expires - Future.after(2) must not(throwA[Throwable]).awaitFor(3.seconds) - connector.exists(s"$prefix-$idx-1") must beFalse.await - connector.exists(s"$prefix-$idx-2") must beFalse.await - } - - "miss after remove" in new TestCase { - connector.set(s"$prefix-$idx", "value").await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - connector.remove(s"$prefix-$idx") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-$idx") must beNone.await - } - - "remove on empty key" in new TestCase { - connector.get[String](s"$prefix-$idx-A") must beNone.await - connector.remove(s"$prefix-$idx-A") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-$idx-A") must beNone.await - } - - "remove with empty args" in new TestCase { - val toBeRemoved = List.empty - connector.remove(toBeRemoved: _*) must not(throwA[Throwable]).await - } - - "clear with setting null" in new TestCase { - connector.set(s"$prefix-$idx", "value").await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - connector.set(s"$prefix-$idx", null).await - connector.get[String](s"$prefix-$idx") must beNone.await - } - - "miss after timeout" in new TestCase { - // set - connector.set(s"$prefix-$idx", "value", 1.second).await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - // wait until it expires - Future.after(2) must not(throwA[Throwable]).awaitFor(3.seconds) - // miss - connector.get[String](s"$prefix-$idx") must beNone.await - } - - "find all matching keys" in new TestCase { - connector.set(s"$prefix-$idx-key-A", "value", 3.second).await - connector.set(s"$prefix-$idx-note-A", "value", 3.second).await - connector.set(s"$prefix-$idx-key-B", "value", 3.second).await - connector.matching(s"$prefix-$idx*").map(_.toSet) must beEqualTo(Set(s"$prefix-$idx-key-A", s"$prefix-$idx-note-A", s"$prefix-$idx-key-B")).await - connector.matching(s"$prefix-$idx*A").map(_.toSet) must beEqualTo(Set(s"$prefix-$idx-key-A", s"$prefix-$idx-note-A")).await - connector.matching(s"$prefix-$idx-key-*").map(_.toSet) must beEqualTo(Set(s"$prefix-$idx-key-A", s"$prefix-$idx-key-B")).await - connector.matching(s"$prefix-${idx}A*") must beEqualTo(Seq.empty).await - } - - "remove multiple keys at once" in new TestCase { - connector.set(s"$prefix-remove-multiple-1", "value").await - connector.get[String](s"$prefix-remove-multiple-1") must beSome[Any].await - connector.set(s"$prefix-remove-multiple-2", "value").await - connector.get[String](s"$prefix-remove-multiple-2") must beSome[Any].await - connector.set(s"$prefix-remove-multiple-3", "value").await - connector.get[String](s"$prefix-remove-multiple-3") must beSome[Any].await - connector.remove(s"$prefix-remove-multiple-1", s"$prefix-remove-multiple-2", s"$prefix-remove-multiple-3").awaitForFuture - connector.get[String](s"$prefix-remove-multiple-1") must beNone.await - connector.get[String](s"$prefix-remove-multiple-2") must beNone.await - connector.get[String](s"$prefix-remove-multiple-3") must beNone.await - } - - "remove in batch" in new TestCase { - connector.set(s"$prefix-remove-batch-1", "value").await - connector.get[String](s"$prefix-remove-batch-1") must beSome[Any].await - connector.set(s"$prefix-remove-batch-2", "value").await - connector.get[String](s"$prefix-remove-batch-2") must beSome[Any].await - connector.set(s"$prefix-remove-batch-3", "value").await - connector.get[String](s"$prefix-remove-batch-3") must beSome[Any].await - connector.remove(s"$prefix-remove-batch-1", s"$prefix-remove-batch-2", s"$prefix-remove-batch-3").awaitForFuture - connector.get[String](s"$prefix-remove-batch-1") must beNone.await - connector.get[String](s"$prefix-remove-batch-2") must beNone.await - connector.get[String](s"$prefix-remove-batch-3") must beNone.await - } - - "set a zero when not exists and then increment" in new TestCase { - connector.increment(s"$prefix-incr-null", 1) must beEqualTo(1).await - } - - "throw an exception when not integer" in new TestCase { - connector.set(s"$prefix-incr-string", "value").await - connector.increment(s"$prefix-incr-string", 1) must throwA[ExecutionFailedException].await - } - - "increment by one" in new TestCase { - connector.set(s"$prefix-incr-by-one", 5).await - connector.increment(s"$prefix-incr-by-one", 1) must beEqualTo(6).await - connector.increment(s"$prefix-incr-by-one", 1) must beEqualTo(7).await - connector.increment(s"$prefix-incr-by-one", 1) must beEqualTo(8).await - } - - "increment by some" in new TestCase { - connector.set(s"$prefix-incr-by-some", 5).await - connector.increment(s"$prefix-incr-by-some", 1) must beEqualTo(6).await - connector.increment(s"$prefix-incr-by-some", 2) must beEqualTo(8).await - connector.increment(s"$prefix-incr-by-some", 3) must beEqualTo(11).await - } - - "decrement by one" in new TestCase { - connector.set(s"$prefix-decr-by-one", 5).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(4).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(3).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(2).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(1).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(0).await - connector.increment(s"$prefix-decr-by-one", -1) must beEqualTo(-1).await - } - - "decrement by some" in new TestCase { - connector.set(s"$prefix-decr-by-some", 5).await - connector.increment(s"$prefix-decr-by-some", -1) must beEqualTo(4).await - connector.increment(s"$prefix-decr-by-some", -2) must beEqualTo(2).await - connector.increment(s"$prefix-decr-by-some", -3) must beEqualTo(-1).await - } - - "append like set when value is undefined" in new TestCase { - connector.get[String](s"$prefix-append-to-null") must beNone.await - connector.append(s"$prefix-append-to-null", "value").awaitForFuture - connector.get[String](s"$prefix-append-to-null") must beSome("value").await - } - - "append to existing string" in new TestCase { - connector.set(s"$prefix-append-to-some", "some").await - connector.get[String](s"$prefix-append-to-some") must beSome("some").await - connector.append(s"$prefix-append-to-some", " value").awaitForFuture - connector.get[String](s"$prefix-append-to-some") must beSome("some value").await - } - - "list push left" in new TestCase { - connector.listPrepend(s"$prefix-list-prepend", "A", "B", "C") must beEqualTo(3).await - connector.listPrepend(s"$prefix-list-prepend", "D", "E", "F") must beEqualTo(6).await - connector.listSlice[String](s"$prefix-list-prepend", 0, -1) must beEqualTo(List("F", "E", "D", "C", "B", "A")).await - } - - "list push right" in new TestCase { - connector.listAppend(s"$prefix-list-append", "A", "B", "C") must beEqualTo(3).await - connector.listAppend(s"$prefix-list-append", "D", "E", "A") must beEqualTo(6).await - connector.listSlice[String](s"$prefix-list-append", 0, -1) must beEqualTo(List("A", "B", "C", "D", "E", "A")).await - } - - "list size" in new TestCase { - connector.listSize(s"$prefix-list-size") must beEqualTo(0).await - connector.listPrepend(s"$prefix-list-size", "A", "B", "C") must beEqualTo(3).await - connector.listSize(s"$prefix-list-size") must beEqualTo(3).await - } - - "list overwrite at index" in new TestCase { - connector.listPrepend(s"$prefix-list-set", "C", "B", "A") must beEqualTo(3).await - connector.listSetAt(s"$prefix-list-set", 1, "D").awaitForFuture - connector.listSlice[String](s"$prefix-list-set", 0, -1) must beEqualTo(List("A", "D", "C")).await - connector.listSetAt(s"$prefix-list-set", 3, "D") must throwA[IndexOutOfBoundsException].await - } - - "list pop head" in new TestCase { - connector.listHeadPop[String](s"$prefix-list-pop") must beNone.await - connector.listPrepend(s"$prefix-list-pop", "C", "B", "A") must beEqualTo(3).await - connector.listHeadPop[String](s"$prefix-list-pop") must beSome("A").await - connector.listHeadPop[String](s"$prefix-list-pop") must beSome("B").await - connector.listHeadPop[String](s"$prefix-list-pop") must beSome("C").await - connector.listHeadPop[String](s"$prefix-list-pop") must beNone.await - } - - "list slice view" in new TestCase { - connector.listSlice[String](s"$prefix-list-slice", 0, -1) must beEqualTo(List.empty).await - connector.listPrepend(s"$prefix-list-slice", "C", "B", "A") must beEqualTo(3).await - connector.listSlice[String](s"$prefix-list-slice", 0, -1) must beEqualTo(List("A", "B", "C")).await - connector.listSlice[String](s"$prefix-list-slice", 0, 0) must beEqualTo(List("A")).await - connector.listSlice[String](s"$prefix-list-slice", -2, -1) must beEqualTo(List("B", "C")).await - } - - "list remove by value" in new TestCase { - connector.listRemove(s"$prefix-list-remove", "A", count = 1) must beEqualTo(0).await - connector.listPrepend(s"$prefix-list-remove", "A", "B", "C") must beEqualTo(3).await - connector.listRemove(s"$prefix-list-remove", "A", count = 1) must beEqualTo(1).await - connector.listSize(s"$prefix-list-remove") must beEqualTo(2).await - } - - "list trim" in new TestCase { - connector.listPrepend(s"$prefix-list-trim", "C", "B", "A") must beEqualTo(3).await - connector.listTrim(s"$prefix-list-trim", 1, 2).awaitForFuture - connector.listSize(s"$prefix-list-trim") must beEqualTo(2).await - connector.listSlice[String](s"$prefix-list-trim", 0, -1) must beEqualTo(List("B", "C")).await - } - - "list insert" in new TestCase { - connector.listSize(s"$prefix-list-insert-1") must beEqualTo(0).await - connector.listInsert(s"$prefix-list-insert-1", "C", "B") must beNone.await - connector.listPrepend(s"$prefix-list-insert-1", "C", "A") must beEqualTo(2).await - connector.listInsert(s"$prefix-list-insert-1", "C", "B") must beSome(3L).await - connector.listInsert(s"$prefix-list-insert-1", "E", "D") must beNone.await - connector.listSlice[String](s"$prefix-list-insert-1", 0, -1) must beEqualTo(List("A", "B", "C")).await - } - - "list set to invalid type" in new TestCase { - connector.set(s"$prefix-list-invalid-$idx", "value") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-list-invalid-$idx") must beSome("value").await - connector.listPrepend(s"$prefix-list-invalid-$idx", "A") must throwA[IllegalArgumentException].await - connector.listAppend(s"$prefix-list-invalid-$idx", "C", "B") must throwA[IllegalArgumentException].await - connector.listInsert(s"$prefix-list-invalid-$idx", "C", "B") must throwA[IllegalArgumentException].await - } - - "set add" in new TestCase { - connector.setSize(s"$prefix-set-add") must beEqualTo(0).await - connector.setAdd(s"$prefix-set-add", "A", "B") must beEqualTo(2).await - connector.setSize(s"$prefix-set-add") must beEqualTo(2).await - connector.setAdd(s"$prefix-set-add", "C", "B") must beEqualTo(1).await - connector.setSize(s"$prefix-set-add") must beEqualTo(3).await - } - - "set add into invalid type" in new TestCase { - connector.set(s"$prefix-set-invalid-$idx", "value") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-set-invalid-$idx") must beSome("value").await - connector.setAdd(s"$prefix-set-invalid-$idx", "A", "B") must throwA[IllegalArgumentException].await - } - - "set rank" in new TestCase { - connector.setSize(s"$prefix-set-rank") must beEqualTo(0).await - connector.setAdd(s"$prefix-set-rank", "A", "B") must beEqualTo(2).await - connector.setSize(s"$prefix-set-rank") must beEqualTo(2).await - - connector.setIsMember(s"$prefix-set-rank", "A") must beTrue.await - connector.setIsMember(s"$prefix-set-rank", "B") must beTrue.await - connector.setIsMember(s"$prefix-set-rank", "C") must beFalse.await - - connector.setAdd(s"$prefix-set-rank", "C", "B") must beEqualTo(1).await - - connector.setIsMember(s"$prefix-set-rank", "A") must beTrue.await - connector.setIsMember(s"$prefix-set-rank", "B") must beTrue.await - connector.setIsMember(s"$prefix-set-rank", "C") must beTrue.await - } - - "set size" in new TestCase { - connector.setSize(s"$prefix-set-size") must beEqualTo(0).await - connector.setAdd(s"$prefix-set-size", "A", "B") must beEqualTo(2).await - connector.setSize(s"$prefix-set-size") must beEqualTo(2).await - } - - "set rem" in new TestCase { - connector.setSize(s"$prefix-set-rem") must beEqualTo(0).await - connector.setAdd(s"$prefix-set-rem", "A", "B", "C") must beEqualTo(3).await - connector.setSize(s"$prefix-set-rem") must beEqualTo(3).await - - connector.setRemove(s"$prefix-set-rem", "A") must beEqualTo(1).await - connector.setSize(s"$prefix-set-rem") must beEqualTo(2).await - connector.setRemove(s"$prefix-set-rem", "B", "C", "D") must beEqualTo(2).await - connector.setSize(s"$prefix-set-rem") must beEqualTo(0).await - } - - "set slice" in new TestCase { - connector.setSize(s"$prefix-set-slice") must beEqualTo(0).await - connector.setAdd(s"$prefix-set-slice", "A", "B", "C") must beEqualTo(3).await - connector.setSize(s"$prefix-set-slice") must beEqualTo(3).await - - connector.setMembers[String](s"$prefix-set-slice") must beEqualTo(Set("A", "B", "C")).await - - connector.setSize(s"$prefix-set-slice") must beEqualTo(3).await - } - - "hash set values" in new TestCase { - val key = s"$prefix-hash-set" - - connector.hashSize(key) must beEqualTo(0).await - connector.hashGetAll[String](key) must beEqualTo(Map.empty).await - connector.hashKeys(key) must beEqualTo(Set.empty).await - connector.hashValues[String](key) must beEqualTo(Set.empty).await - - connector.hashGet[String](key, "KA") must beNone.await - connector.hashSet(key, "KA", "VA1") must beTrue.await - connector.hashGet[String](key, "KA") must beSome("VA1").await - connector.hashSet(key, "KA", "VA2") must beFalse.await - connector.hashGet[String](key, "KA") must beSome("VA2").await - connector.hashSet(key, "KB", "VB") must beTrue.await - - connector.hashGet[String](key, Seq("KA", "KB", "KC")) must beEqualTo(Seq(Some("VA2"), Some("VB"), None)).await - - connector.hashExists(key, "KB") must beTrue.await - connector.hashExists(key, "KC") must beFalse.await - - connector.hashSize(key) must beEqualTo(2).await - connector.hashGetAll[String](key) must beEqualTo(Map("KA" -> "VA2", "KB" -> "VB")).await - connector.hashKeys(key) must beEqualTo(Set("KA", "KB")).await - connector.hashValues[String](key) must beEqualTo(Set("VA2", "VB")).await - - connector.hashRemove(key, "KB") must beEqualTo(1).await - connector.hashRemove(key, "KC") must beEqualTo(0).await - connector.hashExists(key, "KB") must beFalse.await - connector.hashExists(key, "KA") must beTrue.await - - connector.hashSize(key) must beEqualTo(1).await - connector.hashGetAll[String](key) must beEqualTo(Map("KA" -> "VA2")).await - connector.hashKeys(key) must beEqualTo(Set("KA")).await - connector.hashValues[String](key) must beEqualTo(Set("VA2")).await - - connector.hashSet(key, "KD", 5) must beTrue.await - connector.hashIncrement(key, "KD", 2) must beEqualTo(7).await - connector.hashGet[Int](key, "KD") must beSome(7).await - } - - "hash set into invalid type" in new TestCase { - connector.set(s"$prefix-hash-invalid-$idx", "value") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-hash-invalid-$idx") must beSome("value").await - connector.hashSet(s"$prefix-hash-invalid-$idx", "KA", "VA1") must throwA[IllegalArgumentException].await - } - - "sorted set add" in new TestCase { - connector.sortedSetAdd(s"$prefix-sorted-set-add", (1, "A")) must beEqualTo(1).await - connector.sortedSetSize(s"$prefix-sorted-set-add") must beEqualTo(1).await - connector.sortedSetSize(s"$prefix-sorted-set-add") must beEqualTo(1).await - connector.sortedSetAdd(s"$prefix-sorted-set-add", (2, "B"), (3, "C")) must beEqualTo(2).await - connector.sortedSetSize(s"$prefix-sorted-set-add") must beEqualTo(3).await - connector.sortedSetAdd(s"$prefix-sorted-set-add", (1, "A")) must beEqualTo(0).await - connector.sortedSetSize(s"$prefix-sorted-set-add") must beEqualTo(3).await - } - - "sorted set add invalid type" in new TestCase { - connector.set(s"$prefix-sorted-set-invalid-$idx", "value") must not(throwA[Throwable]).await - connector.get[String](s"$prefix-sorted-set-invalid-$idx") must beSome("value").await - connector.sortedSetAdd(s"$prefix-sorted-set-invalid-$idx", 1D -> "VA1") must throwA[IllegalArgumentException].await - } - - "sorted set score" in new TestCase { - connector.sortedSetSize(s"$prefix-sorted-set-score") must beEqualTo(0).await - connector.sortedSetAdd(s"$prefix-sorted-set-score", 1D -> "A", 3D -> "B") must beEqualTo(2).await - connector.sortedSetSize(s"$prefix-sorted-set-score") must beEqualTo(2).await - - connector.sortedSetScore(s"$prefix-sorted-set-score", "A") must beSome(1D).await - connector.sortedSetScore(s"$prefix-sorted-set-score", "B") must beSome(3D).await - connector.sortedSetScore(s"$prefix-sorted-set-score", "C") must beNone.await - - connector.sortedSetAdd(s"$prefix-sorted-set-score", 2D -> "C", 4D -> "B") must beEqualTo(1).await - - connector.sortedSetScore(s"$prefix-sorted-set-score", "A") must beSome(1D).await - connector.sortedSetScore(s"$prefix-sorted-set-score", "B") must beSome(4D).await - connector.sortedSetScore(s"$prefix-sorted-set-score", "C") must beSome(2D).await - } - - "sorted set size" in new TestCase { - connector.sortedSetSize(s"$prefix-sorted-set-size") must beEqualTo(0).await - connector.sortedSetAdd(s"$prefix-sorted-set-size", (1, "A"), (2, "B")) must beEqualTo(2).await - connector.sortedSetSize(s"$prefix-sorted-set-size") must beEqualTo(2).await - } - - "sorted set remove" in new TestCase { - connector.sortedSetSize(s"$prefix-sorted-set-rem") must beEqualTo(0).await - connector.sortedSetAdd(s"$prefix-sorted-set-rem", 1D -> "A", 2D -> "B", 3D -> "C") must beEqualTo(3).await - connector.sortedSetSize(s"$prefix-sorted-set-rem") must beEqualTo(3).await - - connector.sortedSetRemove(s"$prefix-sorted-set-rem", "A") must beEqualTo(1).await - connector.sortedSetSize(s"$prefix-sorted-set-rem") must beEqualTo(2).await - connector.sortedSetRemove(s"$prefix-sorted-set-rem", "B", "C", "D") must beEqualTo(2).await - connector.sortedSetSize(s"$prefix-sorted-set-rem") must beEqualTo(0).await - } - - "sorted set range" in new TestCase { - connector.sortedSetSize(s"$prefix-sorted-set-range") must beEqualTo(0).await - connector.sortedSetAdd(s"$prefix-sorted-set-range", 1D -> "A", 2D -> "B", 4D -> "C") must beEqualTo(3).await - connector.sortedSetSize(s"$prefix-sorted-set-range") must beEqualTo(3).await - - connector.sortedSetRange[String](s"$prefix-sorted-set-range", 0, 1) must beEqualTo(Vector("A", "B")).await - connector.sortedSetRange[String](s"$prefix-sorted-set-range", 0, 4) must beEqualTo(Vector("A", "B", "C")).await - connector.sortedSetRange[String](s"$prefix-sorted-set-range", 1, 9) must beEqualTo(Vector("B", "C")).await - - connector.sortedSetSize(s"$prefix-sorted-set-range") must beEqualTo(3).await - } - - "sorted set reverse range" in new TestCase { - connector.sortedSetSize(s"$prefix-sorted-set-reverse-range") must beEqualTo(0).await - connector.sortedSetAdd(s"$prefix-sorted-set-reverse-range", 1D -> "A", 2D -> "B", 4D -> "C") must beEqualTo(3).await - connector.sortedSetSize(s"$prefix-sorted-set-reverse-range") must beEqualTo(3).await - - connector.sortedSetReverseRange[String](s"$prefix-sorted-set-reverse-range", 0, 1) must beEqualTo(Vector("C", "B")).await - connector.sortedSetReverseRange[String](s"$prefix-sorted-set-reverse-range", 0, 4) must beEqualTo(Vector("C", "B", "A")).await - connector.sortedSetReverseRange[String](s"$prefix-sorted-set-reverse-range", 1, 9) must beEqualTo(Vector("B", "A")).await - - connector.sortedSetSize(s"$prefix-sorted-set-reverse-range") must beEqualTo(3).await - } - } - - override def beforeAll(): Unit = { - super.beforeAll() - // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap(connector.remove).awaitForFuture - } - - override def afterAll(): Unit = { - Shutdown.run.awaitForFuture - super.afterAll() - } -} diff --git a/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala index 93037280..dfb0c659 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala @@ -1,48 +1,46 @@ package play.api.cache.redis.connector -import scala.concurrent.Future -import scala.concurrent.duration._ - -import play.api.cache.redis._ +import akka.actor.{ActorSystem, Scheduler} +import play.api.cache.redis.test.{AsyncUnitSpec, StoppableApplication} +import redis.RedisCommand +import redis.protocol.RedisReply -import akka.actor.ActorSystem -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContextExecutor, Future, Promise} -class RedisRequestTimeoutSpec(implicit ee: ExecutionEnv) extends Specification with WithApplication { +class RedisRequestTimeoutSpec extends AsyncUnitSpec { - import Implicits._ - import MockitoImplicits._ - import RedisRequestTimeoutSpec._ + override protected def testTimeout: FiniteDuration = 3.seconds - "RedisRequestTimeout" should { + "fail long running requests when connected but timeout defined" in { + implicit val system: ActorSystem = ActorSystem("test") + val application = StoppableApplication(system) - "fail long running requests when connected but timeout defined" in { - val impl = new RedisRequestTimeoutImpl(timeout = 1.second) - val cmd = mock[RedisCommandTest].returning returns Future.after(seconds = 3, "response") + application.runAsyncInApplication { + val redisCommandMock = mock[RedisCommandTest[String]] + (() => redisCommandMock.returning).expects().returns(Promise[String]().future) + val redisRequest = new RedisRequestTimeoutImpl(timeout = Some(1.second)) // run the test - impl.send[String](cmd) must throwA[redis.actors.NoConnectionException.type].awaitFor(5.seconds) + redisRequest.send[String](redisCommandMock).assertingFailure[redis.actors.NoConnectionException.type] } } -} - -object RedisRequestTimeoutSpec { - - import redis.RedisCommand - import redis.protocol.RedisReply - trait RedisCommandTest extends RedisCommand[RedisReply, String] { - def returning: Future[String] + private trait RedisCommandTest[T] extends RedisCommand[RedisReply, T] { + def returning: Future[T] } - class RequestTimeoutBase(implicit system: ActorSystem) extends RequestTimeout { - protected implicit val scheduler = system.scheduler - implicit val executionContext = system.dispatcher + private class RequestTimeoutBase(implicit system: ActorSystem) extends RequestTimeout { + protected implicit val scheduler: Scheduler = system.scheduler + implicit val executionContext: ExecutionContextExecutor = system.dispatcher - def send[T](redisCommand: RedisCommand[_ <: RedisReply, T]) = { - redisCommand.asInstanceOf[RedisCommandTest].returning.asInstanceOf[Future[T]] + def send[T](redisCommand: RedisCommand[_ <: RedisReply, T]): Future[T] = { + redisCommand.asInstanceOf[RedisCommandTest[T]].returning } } - class RedisRequestTimeoutImpl(val timeout: Option[FiniteDuration])(implicit system: ActorSystem) extends RequestTimeoutBase with RedisRequestTimeout + private class RedisRequestTimeoutImpl( + override val timeout: Option[FiniteDuration] + )(implicit + system: ActorSystem + ) extends RequestTimeoutBase with RedisRequestTimeout } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala index 6532f101..6f73d0d7 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala @@ -1,83 +1,95 @@ package play.api.cache.redis.connector -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification -import org.specs2.specification.{AfterAll, BeforeAll} +import akka.actor.ActorSystem +import org.scalatest.Ignore import play.api.cache.redis._ -import play.api.cache.redis.configuration.{RedisHost, RedisSentinel} +import play.api.cache.redis.configuration._ import play.api.cache.redis.impl._ -import play.api.inject.ApplicationLifecycle +import play.api.cache.redis.test._ import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Future} -/** - *Specification of the low level connector implementing basic commands
- */ -class RedisSentinelSpec(implicit ee: ExecutionEnv) extends Specification with BeforeAll with AfterAll with WithApplication { +@Ignore +class RedisSentinelSpec extends IntegrationSpec with RedisSentinelContainer { - args(skipAll=true) - - import Implicits._ - - implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] - - implicit private val runtime: RedisRuntime = RedisRuntime("sentinel", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) - - private val serializer = new AkkaSerializerImpl(system) - - private lazy val sentinelInstance = RedisSentinel(defaultCacheName, masterGroup = "sentinel5000", sentinels = RedisHost(dockerIp, 5000) :: RedisHost(dockerIp, 5001) :: RedisHost(dockerIp, 5002) :: Nil, defaults) - - private lazy val connector: RedisConnector = new RedisConnectorProvider(sentinelInstance, serializer).get - - val prefix = "sentinel-test" - - "Redis sentinel (separate)" should { - - "pong on ping" in new TestCase { - connector.ping() must not(throwA[Throwable]).await - } - - "miss on get" in new TestCase { - connector.get[String](s"$prefix-$idx") must beNone.await - } + test("pong on ping") { connector => + for { + _ <- connector.ping().assertingSuccess + } yield Passed + } - "hit after set" in new TestCase { - connector.set(s"$prefix-$idx", "value") must beTrue.await - connector.get[String](s"$prefix-$idx") must beSome[Any].await - connector.get[String](s"$prefix-$idx") must beSome("value").await - } + test("miss on get") { connector => + for { + _ <- connector.get[String]("miss-on-get").assertingEqual(None) + } yield Passed + } - "ignore set if not exists when already defined" in new TestCase { - connector.set(s"$prefix-if-not-exists-when-exists", "previous") must beTrue.await - connector.set(s"$prefix-if-not-exists-when-exists", "value", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists-when-exists") must beSome("previous").await - } + test("hit after set") { connector => + for { + _ <- connector.set("hit-after-set", "value").assertingEqual(true) + _ <- connector.get[String]("hit-after-set").assertingEqual(Some("value")) + } yield Passed + } - "perform set if not exists when undefined" in new TestCase { - connector.get[String](s"$prefix-if-not-exists") must beNone.await - connector.set(s"$prefix-if-not-exists", "value", ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - connector.set(s"$prefix-if-not-exists", "other", ifNotExists = true) must beFalse.await - connector.get[String](s"$prefix-if-not-exists") must beSome("value").await - } + test("ignore set if not exists when already defined") { connector => + for { + _ <- connector.set("if-not-exists-when-exists", "previous").assertingEqual(true) + _ <- connector.set("if-not-exists-when-exists", "value", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String]("if-not-exists-when-exists").assertingEqual(Some("previous")) + } yield Passed + } - "perform set if not exists with expiration" in new TestCase { - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - connector.set(s"$prefix-if-not-exists-with-expiration", "value", 2.seconds, ifNotExists = true) must beTrue.await - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beSome("value").await - // wait until the first duration expires - Future.after(3) must not(throwA[Throwable]).awaitFor(4.seconds) - connector.get[String](s"$prefix-if-not-exists-with-expiration") must beNone.await - } + test("perform set if not exists when undefined") { connector => + for { + _ <- connector.get[String]("if-not-exists").assertingEqual(None) + _ <- connector.set("if-not-exists", "value", ifNotExists = true).assertingEqual(true) + _ <- connector.get[String]("if-not-exists").assertingEqual(Some("value")) + _ <- connector.set("if-not-exists", "other", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String]("if-not-exists").assertingEqual(Some("value")) + } yield Passed } - def beforeAll(): Unit = { - // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap(connector.remove).awaitForFuture + test("perform set if not exists with expiration") { connector => + for { + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(None) + _ <- connector.set("if-not-exists-with-expiration", "value", 300.millis, ifNotExists = true).assertingEqual(true) + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(Some("value")) + // wait until the first duration expires + _ <- Future.after(700.millis, ()) + _ <- connector.get[String]("if-not-exists-with-expiration").assertingEqual(None) + } yield Passed } - def afterAll(): Unit = { - Shutdown.run + def test(name: String)(f: RedisConnector => Future[Assertion]): Unit = { + name in { + implicit val system: ActorSystem = ActorSystem("test", classLoader = Some(getClass.getClassLoader)) + implicit val runtime: RedisRuntime = RedisRuntime("sentinel", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit val application: StoppableApplication = StoppableApplication(system) + val serializer = new AkkaSerializerImpl(system) + + lazy val sentinelInstance = RedisSentinel( + name = "sentinel", + masterGroup = master, + sentinels = 0.until(nodes).map { i => + RedisHost(container.containerIpAddress, container.mappedPort(sentinelPort + i)) + }.toList, + settings = RedisSettings.load( + config = Helpers.configuration.default.underlying, + path = "play.cache.redis" + ) + ) + + application.runAsyncInApplication { + val connector: RedisConnector = new RedisConnectorProvider(sentinelInstance, serializer).get + for { + // initialize the connector by flushing the database + keys <- connector.matching("*") + _ <- Future.sequence(keys.map(connector.remove(_))) + // run the test + _ <- f(connector) + } yield Passed + } + } } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisStandaloneSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisStandaloneSpec.scala new file mode 100644 index 00000000..b37ee667 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/RedisStandaloneSpec.scala @@ -0,0 +1,618 @@ +package play.api.cache.redis.connector + +import akka.actor.ActorSystem +import play.api.cache.redis._ +import play.api.cache.redis.configuration._ +import play.api.cache.redis.impl._ +import play.api.cache.redis.test._ + +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} + +class RedisStandaloneSpec extends IntegrationSpec with RedisStandaloneContainer { + + test("pong on ping") { (_, connector) => + for { + _ <- connector.ping().assertingSuccess + } yield Passed + } + + test("miss on get") { (cacheKey, connector) => + for { + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("hit after set") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value").assertingEqual(true) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + } yield Passed + } + + test("ignore set if not exists when already defined") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "previous").assertingEqual(true) + _ <- connector.set(cacheKey, "value", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String](cacheKey).assertingEqual(Some("previous")) + } yield Passed + } + + test("perform set if not exists when undefined") { (cacheKey, connector) => + for { + _ <- connector.get[String](cacheKey).assertingEqual(None) + _ <- connector.set(cacheKey, "value", ifNotExists = true).assertingEqual(true) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.set(cacheKey, "other", ifNotExists = true).assertingEqual(false) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + } yield Passed + } + + test("perform set if not exists with expiration") { (cacheKey, connector) => + for { + _ <- connector.get[String](cacheKey).assertingEqual(None) + _ <- connector.set(cacheKey, "value", 300.millis, ifNotExists = true).assertingEqual(true) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + // wait until the first duration expires + _ <- Future.waitFor(400.millis) + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("hit after mset") { (cacheKey, connector) => + for { + _ <- connector.mSet(s"$cacheKey-1" -> "value-1", s"$cacheKey-2" -> "value-2") + _ <- connector.mGet[String](s"$cacheKey-1", s"$cacheKey-2", s"$cacheKey-3").assertingEqual(List(Some("value-1"), Some("value-2"), None)) + _ <- connector.mSet(s"$cacheKey-3" -> "value-3", s"$cacheKey-2" -> null) + _ <- connector.mGet[String](s"$cacheKey-1", s"$cacheKey-2", s"$cacheKey-3").assertingEqual(List(Some("value-1"), None, Some("value-3"))) + _ <- connector.mSet(s"$cacheKey-3" -> null) + _ <- connector.mGet[String](s"$cacheKey-1", s"$cacheKey-2", s"$cacheKey-3").assertingEqual(List(Some("value-1"), None, None)) + } yield Passed + } + + test("ignore msetnx if already defined") { (cacheKey, connector) => + for { + _ <- connector.mSetIfNotExist(s"$cacheKey-1" -> "value-1", s"$cacheKey-2" -> "value-2").assertingEqual(true) + _ <- connector.mGet[String](s"$cacheKey-1", s"$cacheKey-2").assertingEqual(List(Some("value-1"), Some("value-2"))) + _ <- connector.mSetIfNotExist(s"$cacheKey-3" -> "value-3", s"$cacheKey-2" -> "value-2").assertingEqual(false) + } yield Passed + } + + test("expire refreshes expiration") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value", 200.millis) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.expire(cacheKey, 1000.millis) + // wait until the first duration expires + _ <- Future.waitFor(300.millis) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + } yield Passed + } + + test("expires in returns finite duration") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value", 2.second) + _ <- connector.expiresIn(cacheKey).assertingCondition(_.exists(_ <= 2.seconds)) + } yield Passed + } + + test("expires in returns infinite duration") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value") + _ <- connector.expiresIn(cacheKey).assertingEqual(Some(Duration.Inf)) + } yield Passed + } + + test("expires in returns not defined key") { (cacheKey, connector) => + for { + _ <- connector.expiresIn(cacheKey).assertingEqual(None) + _ <- connector.set(cacheKey, "value", 200.millis) + _ <- connector.expiresIn(cacheKey).assertingCondition(_.exists(_ <= 200.millis)) + // wait until the first duration expires + _ <- Future.waitFor(300.millis) + _ <- connector.expiresIn(cacheKey).assertingEqual(None) + } yield Passed + } + + test("positive exists on existing keys") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value") + _ <- connector.exists(cacheKey).assertingEqual(true) + } yield Passed + } + + test("negative exists on expired and missing keys") { (cacheKey, connector) => + for { + _ <- connector.set(s"$cacheKey-1", "value", 200.millis) + _ <- connector.exists(s"$cacheKey-1").assertingEqual(true) + // wait until the duration expires + _ <- Future.waitFor(250.millis) + _ <- connector.exists(s"$cacheKey-1").assertingEqual(false) + _ <- connector.exists(s"$cacheKey-2").assertingEqual(false) + } yield Passed + } + + test("miss after remove") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value") + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.remove(cacheKey).assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("remove on empty key") { (cacheKey, connector) => + for { + _ <- connector.get[String](cacheKey).assertingEqual(None) + _ <- connector.remove(cacheKey).assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("remove with empty args") { (_, connector) => + for { + _ <- connector.remove(List.empty: _*).assertingSuccess + } yield Passed + } + + test("clear with setting null") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value") + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.set(cacheKey, null) + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("miss after timeout") { (cacheKey, connector) => + for { + // set + _ <- connector.set(cacheKey, "value", 200.millis) + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + // wait until it expires + _ <- Future.waitFor(250.millis) + // miss + _ <- connector.get[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("find all matching keys") { (cacheKey, connector) => + for { + _ <- connector.set(s"$cacheKey-key-A", "value", 3.second) + _ <- connector.set(s"$cacheKey-note-A", "value", 3.second) + _ <- connector.set(s"$cacheKey-key-B", "value", 3.second) + _ <- connector.matching(s"$cacheKey-*").map(_.toSet).assertingEqual(Set(s"$cacheKey-key-A", s"$cacheKey-note-A", s"$cacheKey-key-B")) + _ <- connector.matching(s"$cacheKey-*A").map(_.toSet).assertingEqual(Set(s"$cacheKey-key-A", s"$cacheKey-note-A")) + _ <- connector.matching(s"$cacheKey-key-*").map(_.toSet).assertingEqual(Set(s"$cacheKey-key-A", s"$cacheKey-key-B")) + _ <- connector.matching(s"$cacheKey-* A * ").assertingEqual(Seq.empty) + } + yield Passed + } + + test("remove multiple keys at once") { (cacheKey, connector) => + for { + _ <- connector.set(s"$cacheKey-1", "value") + _ <- connector.get[String](s"$cacheKey-1").assertingEqual(Some("value")) + _ <- connector.set(s"$cacheKey-2", "value") + _ <- connector.get[String](s"$cacheKey-2").assertingEqual(Some("value")) + _ <- connector.set(s"$cacheKey-3", "value") + _ <- connector.get[String](s"$cacheKey-3").assertingEqual(Some("value")) + _ <- connector.remove(s"$cacheKey-1", s"$cacheKey-2", s"$cacheKey-3") + _ <- connector.get[String](s"$cacheKey-1").assertingEqual(None) + _ <- connector.get[String](s"$cacheKey-2").assertingEqual(None) + _ <- connector.get[String](s"$cacheKey-3").assertingEqual(None) + } yield Passed + } + + test("remove in batch") { (cacheKey, connector) => + for { + _ <- connector.set(s"$cacheKey-1", "value") + _ <- connector.get[String](s"$cacheKey-1").assertingEqual(Some("value")) + _ <- connector.set(s"$cacheKey-2", "value") + _ <- connector.get[String](s"$cacheKey-2").assertingEqual(Some("value")) + _ <- connector.set(s"$cacheKey-3", "value") + _ <- connector.get[String](s"$cacheKey-3").assertingEqual(Some("value")) + _ <- connector.remove(s"$cacheKey-1", s"$cacheKey-2", s"$cacheKey-3") + _ <- connector.get[String](s"$cacheKey-1").assertingEqual(None) + _ <- connector.get[String](s"$cacheKey-2").assertingEqual(None) + _ <- connector.get[String](s"$cacheKey-3").assertingEqual(None) + } yield Passed + } + + test("set a zero when not exists and then increment") { (cacheKey, connector) => + for { + _ <- connector.increment(cacheKey, 1).assertingEqual(1) + } yield Passed + } + + test("throw an exception when not integer") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value") + _ <- connector.increment(cacheKey, 1).assertingFailure[ExecutionFailedException] + } yield Passed + } + + test("increment by one") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, 5) + _ <- connector.increment(cacheKey, 1).assertingEqual(6) + _ <- connector.increment(cacheKey, 1).assertingEqual(7) + _ <- connector.increment(cacheKey, 1).assertingEqual(8) + } yield Passed + } + + test("increment by some") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, 5) + _ <- connector.increment(cacheKey, 1).assertingEqual(6) + _ <- connector.increment(cacheKey, 2).assertingEqual(8) + _ <- connector.increment(cacheKey, 3).assertingEqual(11) + } yield Passed + } + + test("decrement by one") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, 5) + _ <- connector.increment(cacheKey, -1).assertingEqual(4) + _ <- connector.increment(cacheKey, -1).assertingEqual(3) + _ <- connector.increment(cacheKey, -1).assertingEqual(2) + _ <- connector.increment(cacheKey, -1).assertingEqual(1) + _ <- connector.increment(cacheKey, -1).assertingEqual(0) + _ <- connector.increment(cacheKey, -1).assertingEqual(-1) + } yield Passed + } + + test("decrement by some") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, 5) + _ <- connector.increment(cacheKey, -1).assertingEqual(4) + _ <- connector.increment(cacheKey, -2).assertingEqual(2) + _ <- connector.increment(cacheKey, -3).assertingEqual(-1) + } yield Passed + } + + test("append like set when value is undefined") { (cacheKey, connector) => + for { + _ <- connector.get[String](cacheKey).assertingEqual(None) + _ <- connector.append(cacheKey, "value") + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + } yield Passed + } + + test("append to existing string") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "some") + _ <- connector.get[String](cacheKey).assertingEqual(Some("some")) + _ <- connector.append(cacheKey, " value") + _ <- connector.get[String](cacheKey).assertingEqual(Some("some value")) + } yield Passed + } + + test("list push left") { (cacheKey, connector) => + for { + _ <- connector.listPrepend(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.listPrepend(cacheKey, "D", "E", "F").assertingEqual(6) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("F", "E", "D", "C", "B", "A")) + } yield Passed + } + + test("list push right") { (cacheKey, connector) => + for { + _ <- connector.listAppend(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.listAppend(cacheKey, "D", "E", "A").assertingEqual(6) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("A", "B", "C", "D", "E", "A")) + } yield Passed + } + + test("list size") { (cacheKey, connector) => + for { + _ <- connector.listSize(cacheKey).assertingEqual(0) + _ <- connector.listPrepend(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.listSize(cacheKey).assertingEqual(3) + } yield Passed + } + + test("list overwrite at index") { (cacheKey, connector) => + for { + _ <- connector.listPrepend(cacheKey, "C", "B", "A").assertingEqual(3) + _ <- connector.listSetAt(cacheKey, 1, "D") + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("A", "D", "C")) + _ <- connector.listSetAt(cacheKey, 3, "D").assertingFailure[IndexOutOfBoundsException] + } yield Passed + } + + test("list pop head") { (cacheKey, connector) => + for { + _ <- connector.listHeadPop[String](cacheKey).assertingEqual(None) + _ <- connector.listPrepend(cacheKey, "C", "B", "A").assertingEqual(3) + _ <- connector.listHeadPop[String](cacheKey).assertingEqual(Some("A")) + _ <- connector.listHeadPop[String](cacheKey).assertingEqual(Some("B")) + _ <- connector.listHeadPop[String](cacheKey).assertingEqual(Some("C")) + _ <- connector.listHeadPop[String](cacheKey).assertingEqual(None) + } yield Passed + } + + test("list slice view") { (cacheKey, connector) => + for { + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List.empty) + _ <- connector.listPrepend(cacheKey, "C", "B", "A").assertingEqual(3) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("A", "B", "C")) + _ <- connector.listSlice[String](cacheKey, 0, 0).assertingEqual(List("A")) + _ <- connector.listSlice[String](cacheKey, -2, -1).assertingEqual(List("B", "C")) + } yield Passed + } + + test("list remove by value") { (cacheKey, connector) => + for { + _ <- connector.listRemove(cacheKey, "A", count = 1).assertingEqual(0) + _ <- connector.listPrepend(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.listRemove(cacheKey, "A", count = 1).assertingEqual(1) + _ <- connector.listSize(cacheKey).assertingEqual(2) + } yield Passed + } + + test("list trim") { (cacheKey, connector) => + for { + _ <- connector.listPrepend(cacheKey, "C", "B", "A").assertingEqual(3) + _ <- connector.listTrim(cacheKey, 1, 2) + _ <- connector.listSize(cacheKey).assertingEqual(2) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("B", "C")) + } yield Passed + } + + test("list insert") { (cacheKey, connector) => + for { + _ <- connector.listSize(cacheKey).assertingEqual(0) + _ <- connector.listInsert(cacheKey, "C", "B").assertingEqual(None) + _ <- connector.listPrepend(cacheKey, "C", "A").assertingEqual(2) + _ <- connector.listInsert(cacheKey, "C", "B").assertingEqual(Some(3L)) + _ <- connector.listInsert(cacheKey, "E", "D").assertingEqual(None) + _ <- connector.listSlice[String](cacheKey, 0, -1).assertingEqual(List("A", "B", "C")) + } yield Passed + } + + test("list set to invalid type") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value").assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.listPrepend(cacheKey, "A").assertingFailure[IllegalArgumentException] + _ <- connector.listAppend(cacheKey, "C", "B").assertingFailure[IllegalArgumentException] + _ <- connector.listInsert(cacheKey, "C", "B").assertingFailure[IllegalArgumentException] + } yield Passed + } + + test("set add") { (cacheKey, connector) => + for { + _ <- connector.setSize(cacheKey).assertingEqual(0) + _ <- connector.setAdd(cacheKey, "A", "B").assertingEqual(2) + _ <- connector.setSize(cacheKey).assertingEqual(2) + _ <- connector.setAdd(cacheKey, "C", "B").assertingEqual(1) + _ <- connector.setSize(cacheKey).assertingEqual(3) + } yield Passed + } + + test("set add into invalid type") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value").assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.setAdd(cacheKey, "A", "B").assertingFailure[IllegalArgumentException] + } yield Passed + } + + test("set rank") { (cacheKey, connector) => + for { + _ <- connector.setSize(cacheKey).assertingEqual(0) + _ <- connector.setAdd(cacheKey, "A", "B").assertingEqual(2) + _ <- connector.setSize(cacheKey).assertingEqual(2) + + _ <- connector.setIsMember(cacheKey, "A").assertingEqual(true) + _ <- connector.setIsMember(cacheKey, "B").assertingEqual(true) + _ <- connector.setIsMember(cacheKey, "C").assertingEqual(false) + + _ <- connector.setAdd(cacheKey, "C", "B").assertingEqual(1) + + _ <- connector.setIsMember(cacheKey, "A").assertingEqual(true) + _ <- connector.setIsMember(cacheKey, "B").assertingEqual(true) + _ <- connector.setIsMember(cacheKey, "C").assertingEqual(true) + } yield Passed + } + + test("set size") { (cacheKey, connector) => + for { + _ <- connector.setSize(cacheKey).assertingEqual(0) + _ <- connector.setAdd(cacheKey, "A", "B").assertingEqual(2) + _ <- connector.setSize(cacheKey).assertingEqual(2) + } yield Passed + } + + test("set rem") { (cacheKey, connector) => + for { + _ <- connector.setSize(cacheKey).assertingEqual(0) + _ <- connector.setAdd(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.setSize(cacheKey).assertingEqual(3) + + _ <- connector.setRemove(cacheKey, "A").assertingEqual(1) + _ <- connector.setSize(cacheKey).assertingEqual(2) + _ <- connector.setRemove(cacheKey, "B", "C", "D").assertingEqual(2) + _ <- connector.setSize(cacheKey).assertingEqual(0) + } yield Passed + } + + test("set slice") { (cacheKey, connector) => + for { + _ <- connector.setSize(cacheKey).assertingEqual(0) + _ <- connector.setAdd(cacheKey, "A", "B", "C").assertingEqual(3) + _ <- connector.setSize(cacheKey).assertingEqual(3) + + _ <- connector.setMembers[String](cacheKey).assertingEqual(Set("A", "B", "C")) + + _ <- connector.setSize(cacheKey).assertingEqual(3) + } yield Passed + } + + test("hash set values") { (cacheKey, connector) => + for { + _ <- connector.hashSize(cacheKey).assertingEqual(0) + _ <- connector.hashGetAll[String] (cacheKey).assertingEqual(Map.empty) + _ <- connector.hashKeys(cacheKey).assertingEqual(Set.empty) + _ <- connector.hashValues[String] (cacheKey).assertingEqual(Set.empty) + + _ <- connector.hashGet[String] (cacheKey, "KA").assertingEqual(None) + _ <- connector.hashSet(cacheKey, "KA", "VA1").assertingEqual(true) + _ <- connector.hashGet[String] (cacheKey, "KA").assertingEqual(Some("VA1")) + _ <- connector.hashSet(cacheKey, "KA", "VA2").assertingEqual(false) + _ <- connector.hashGet[String] (cacheKey, "KA").assertingEqual(Some("VA2")) + _ <- connector.hashSet(cacheKey, "KB", "VB").assertingEqual(true) + + _ <- connector.hashGet[String] (cacheKey, Seq("KA", "KB", "KC")).assertingEqual(Seq(Some("VA2"), Some("VB"), None)) + + _ <- connector.hashExists(cacheKey, "KB").assertingEqual(true) + _ <- connector.hashExists(cacheKey, "KC").assertingEqual(false) + + _ <- connector.hashSize(cacheKey).assertingEqual(2) + _ <- connector.hashGetAll[String] (cacheKey).assertingEqual(Map("KA" -> "VA2", "KB" -> "VB")) + _ <- connector.hashKeys(cacheKey).assertingEqual(Set("KA", "KB")) + _ <- connector.hashValues[String] (cacheKey).assertingEqual(Set("VA2", "VB")) + + _ <- connector.hashRemove(cacheKey, "KB").assertingEqual(1) + _ <- connector.hashRemove(cacheKey, "KC").assertingEqual(0) + _ <- connector.hashExists(cacheKey, "KB").assertingEqual(false) + _ <- connector.hashExists(cacheKey, "KA").assertingEqual(true) + + _ <- connector.hashSize(cacheKey).assertingEqual(1) + _ <- connector.hashGetAll[String] (cacheKey).assertingEqual(Map("KA" -> "VA2")) + _ <- connector.hashKeys(cacheKey).assertingEqual(Set("KA")) + _ <- connector.hashValues[String] (cacheKey).assertingEqual(Set("VA2")) + + _ <- connector.hashSet(cacheKey, "KD", 5).assertingEqual(true) + _ <- connector.hashIncrement(cacheKey, "KD", 2).assertingEqual(7) + _ <- connector.hashGet[Int](cacheKey, "KD").assertingEqual(Some(7)) + } yield Passed + } + + test("hash set into invalid type") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value").assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.hashSet(cacheKey, "KA", "VA1").assertingFailure[IllegalArgumentException] + } yield Passed + } + + test("sorted set add") { (cacheKey, connector) => + for { + _ <- connector.sortedSetAdd(cacheKey, (1, "A")).assertingEqual(1) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(1) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(1) + _ <- connector.sortedSetAdd(cacheKey, (2, "B"), (3, "C")).assertingEqual(2) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + _ <- connector.sortedSetAdd(cacheKey, (1, "A")).assertingEqual(0) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + } yield Passed + } + + test("sorted set add invalid type") { (cacheKey, connector) => + for { + _ <- connector.set(cacheKey, "value").assertingSuccess + _ <- connector.get[String](cacheKey).assertingEqual(Some("value")) + _ <- connector.sortedSetAdd(cacheKey, 1D -> "VA1").assertingFailure[IllegalArgumentException] + } yield Passed + } + + test("sorted set score") { (cacheKey, connector) => + for { + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + _ <- connector.sortedSetAdd(cacheKey, 1D -> "A", 3D -> "B").assertingEqual(2) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(2) + + _ <- connector.sortedSetScore(cacheKey, "A").assertingEqual(Some(1D)) + _ <- connector.sortedSetScore(cacheKey, "B").assertingEqual(Some(3D)) + _ <- connector.sortedSetScore(cacheKey, "C").assertingEqual(None) + + _ <- connector.sortedSetAdd(cacheKey, 2D -> "C", 4D -> "B").assertingEqual(1) + + _ <- connector.sortedSetScore(cacheKey, "A").assertingEqual(Some(1D)) + _ <- connector.sortedSetScore(cacheKey, "B").assertingEqual(Some(4D)) + _ <- connector.sortedSetScore(cacheKey, "C").assertingEqual(Some(2D)) + } yield Passed + } + + test("sorted set size") { (cacheKey, connector) => + for { + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + _ <- connector.sortedSetAdd(cacheKey, (1, "A"), (2, "B")).assertingEqual(2) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(2) + } yield Passed + } + + test("sorted set remove") { (cacheKey, connector) => + for { + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + _ <- connector.sortedSetAdd(cacheKey, 1D -> "A", 2D -> "B", 3D -> "C").assertingEqual(3) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + + _ <- connector.sortedSetRemove(cacheKey, "A").assertingEqual(1) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(2) + _ <- connector.sortedSetRemove(cacheKey, "B", "C", "D").assertingEqual(2) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + } yield Passed + } + + test("sorted set range") { (cacheKey, connector) => + for { + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + _ <- connector.sortedSetAdd(cacheKey, 1D -> "A", 2D -> "B", 4D -> "C").assertingEqual(3) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + + _ <- connector.sortedSetRange[String](cacheKey, 0, 1).assertingEqual(Vector("A", "B")) + _ <- connector.sortedSetRange[String](cacheKey, 0, 4).assertingEqual(Vector("A", "B", "C")) + _ <- connector.sortedSetRange[String](cacheKey, 1, 9).assertingEqual(Vector("B", "C")) + + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + } yield Passed + } + + test("sorted set reverse range") { (cacheKey, connector) => + for { + _ <- connector.sortedSetSize(cacheKey).assertingEqual(0) + _ <- connector.sortedSetAdd(cacheKey, 1D -> "A", 2D -> "B", 4D -> "C").assertingEqual(3) + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + + _ <- connector.sortedSetReverseRange[String](cacheKey, 0, 1).assertingEqual(Vector("C", "B")) + _ <- connector.sortedSetReverseRange[String](cacheKey, 0, 4).assertingEqual(Vector("C", "B", "A")) + _ <- connector.sortedSetReverseRange[String](cacheKey, 1, 9).assertingEqual(Vector("B", "A")) + + _ <- connector.sortedSetSize(cacheKey).assertingEqual(3) + } yield Passed + } + + def test(name: String)(f: (String, RedisConnector) => Future[Assertion]): Unit = { + name in { + implicit val system: ActorSystem = ActorSystem("test", classLoader = Some(getClass.getClassLoader)) + implicit val runtime: RedisRuntime = RedisRuntime("standalone", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit val application: StoppableApplication = StoppableApplication(system) + val serializer = new AkkaSerializerImpl(system) + + lazy val instance = RedisStandalone( + name = "play", + host = RedisHost(container.containerIpAddress, container.mappedPort(defaultPort)), + settings = RedisSettings.load( + config = Helpers.configuration.default.underlying, + path = "play.cache.redis" + ) + ) + + val cacheKey = name.toLowerCase().replace(" ", "-") + + application.runAsyncInApplication { + for { + connector <- Future(new RedisConnectorProvider(instance, serializer).get) + // initialize the connector by flushing the database + _ <- connector.invalidate() + // run the test + _ <- f(cacheKey, connector) + } yield Passed + } + } + } + + } diff --git a/src/test/scala/play/api/cache/redis/connector/SerializerImplicits.scala b/src/test/scala/play/api/cache/redis/connector/SerializerImplicits.scala deleted file mode 100644 index 281d4a55..00000000 --- a/src/test/scala/play/api/cache/redis/connector/SerializerImplicits.scala +++ /dev/null @@ -1,21 +0,0 @@ -package play.api.cache.redis.connector - -import scala.reflect.ClassTag - -object SerializerImplicits { - - implicit class ValueEncoder(val any: Any) extends AnyVal { - def encoded(implicit serializer: AkkaSerializer): String = serializer.encode(any).get - } - - implicit class StringDecoder(val string: String) extends AnyVal { - def decoded[T: ClassTag](implicit serializer: AkkaSerializer): T = serializer.decode[T](string).get - } - - implicit class StringOps(val string: String) extends AnyVal { - def removeAllWhitespaces = string.replaceAll("\\s", "") - } - - /** Plain test object to be cached */ - case class SimpleObject(key: String, value: Int) -} diff --git a/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala b/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala index a148cf35..838ec754 100644 --- a/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala @@ -1,129 +1,201 @@ package play.api.cache.redis.connector -import java.util.Date - -import play.api.inject.guice.GuiceApplicationBuilder +import akka.actor.ActorSystem import play.api.cache.redis._ +import play.api.cache.redis.test._ -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification - -class SerializerSpec extends Specification with Mockito { - import SerializerImplicits._ +import java.util.Date +import scala.reflect.ClassTag +import scala.util.Random - private val system = GuiceApplicationBuilder().build().actorSystem +class SerializerSpec extends AsyncUnitSpec { - private implicit val serializer: AkkaSerializer = new AkkaSerializerImpl(system) + import SerializerSpec._ - "AkkaEncoder" should "encode" >> { + "encode" when { - "byte" in { + test("byte") { implicit serializer => 0xAB.toByte.encoded mustEqual "-85" JavaTypes.byteValue.encoded mustEqual "5" } - "byte[]" in { + test("byte[]") { implicit serializer => JavaTypes.bytesValue.encoded mustEqual "AQID" } - "char" in { + test("char") { implicit serializer => 'a'.encoded mustEqual "a" 'b'.encoded mustEqual "b" 'Å¡'.encoded mustEqual "Å¡" } - "boolean" in { + test("boolean") { implicit serializer => true.encoded mustEqual "true" } - "short" in { + test("short") { implicit serializer => 12.toShort.toByte.encoded mustEqual "12" } - "int" in { + test("int") { implicit serializer => 15.encoded mustEqual "15" } - "long" in { + test("long") { implicit serializer => 144L.encoded mustEqual "144" } - "float" in { + test("float") { implicit serializer => 1.23f.encoded mustEqual "1.23" } - "double" in { + test("double") { implicit serializer => 3.14.encoded mustEqual "3.14" } - "string" in { + test("string") { implicit serializer => "some string".encoded mustEqual "some string" } - "date" in { + test("date") { implicit serializer => new Date(123).encoded mustEqual "rO0ABXNyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAAAAAAB7eA==" } - "null" in { - new ValueEncoder(null).encoded must throwA[UnsupportedOperationException] + test("null") { implicit serializer => + assertThrows[UnsupportedOperationException] { + new ValueEncoder(null).encoded + } + } + + test("custom classes") { implicit serializer => + SimpleObject("B", 3).encoded mustEqual + """ + |rO0ABXNyADpwbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplclNwZWMkU2ltc + |GxlT2JqZWN0LqzdylZUVb0CAAJJAAV2YWx1ZUwAA2tleXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAA + |AAA3QAAUI= + """.stripMargin.withoutWhitespaces + } + + test("list") { implicit serializer => + List("A", "B", "C").encoded mustEqual + """ + |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmdlbmVyaWMuRGVmYXVsdFNlcmlhbGl6YXRpb25Qcm94e + |QAAAAAAAAADAwABTAAHZmFjdG9yeXQAGkxzY2FsYS9jb2xsZWN0aW9uL0ZhY3Rvcnk7eHBzcgAqc2 + |NhbGEuY29sbGVjdGlvbi5JdGVyYWJsZUZhY3RvcnkkVG9GYWN0b3J5AAAAAAAAAAMCAAFMAAdmYWN + |0b3J5dAAiTHNjYWxhL2NvbGxlY3Rpb24vSXRlcmFibGVGYWN0b3J5O3hwc3IAJnNjYWxhLnJ1bnRp + |bWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqY + |XZhL2xhbmcvQ2xhc3M7eHB2cgAgc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUuTGlzdCQAAAAAAA + |AAAwIAAHhwdwT/////dAABQXQAAUJ0AAFDc3EAfgAGdnIAJnNjYWxhLmNvbGxlY3Rpb24uZ2VuZXJ + |pYy5TZXJpYWxpemVFbmQkAAAAAAAAAAMCAAB4cHg= + """.stripMargin.withoutWhitespaces } } - "AkkaDecoder" should "decode" >> { + "decode" when { - "byte" in { + test("byte") { implicit serializer => "-85".decoded[Byte] mustEqual 0xAB.toByte } - "byte[]" in { + test("byte[]") { implicit serializer => "YWJj".decoded[Array[Byte]] mustEqual Array("a".head.toByte, "b".head.toByte, "c".head.toByte) } - "char" in { + test("char") { implicit serializer => "a".decoded[Char] mustEqual 'a' "b".decoded[Char] mustEqual 'b' "Å¡".decoded[Char] mustEqual 'Å¡' } - "boolean" in { + test("boolean") { implicit serializer => "true".decoded[Boolean] mustEqual true } - "short" in { + test("short") { implicit serializer => "12".decoded[Short] mustEqual 12.toShort.toByte } - "int" in { + test("int") { implicit serializer => "15".decoded[Int] mustEqual 15 } - "long" in { + test("long") { implicit serializer => "144".decoded[Long] mustEqual 144L } - "float" in { + test("float") { implicit serializer => "1.23".decoded[Float] mustEqual 1.23f } - "double" in { + test("double") { implicit serializer => "3.14".decoded[Double] mustEqual 3.14 } - "string" in { + test("string") { implicit serializer => "some string".decoded[String] mustEqual "some string" } - "null" in { - "".decoded[String] must beNull + test("null") { implicit serializer => + "".decoded[String] mustEqual null } - "date" in { + test("date") { implicit serializer => "rO0ABXNyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAAAAAAB7eA==".decoded[Date] mustEqual new Date(123) } - "invalid type" in { - def decoded: Date = "something".decoded[Date] - decoded must throwA[IllegalArgumentException] + test("invalid type") { implicit serializer => + assertThrows[IllegalArgumentException] { + "something".decoded[Date] + } + } + + test("custom classes") { implicit serializer => + """ + |rO0ABXNyADpwbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplclNwZWMkU2ltc + |GxlT2JqZWN0LqzdylZUVb0CAAJJAAV2YWx1ZUwAA2tleXQAEkxqYXZhL2xhbmcvU3RyaW5nO3hwAA + |AAA3QAAUI= + """.stripMargin.withoutWhitespaces.decoded[SimpleObject] mustEqual SimpleObject("B", 3) + } + + test("list") { implicit serializer => + """ + |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmdlbmVyaWMuRGVmYXVsdFNlcmlhbGl6YXRpb25Qcm94e + |QAAAAAAAAADAwABTAAHZmFjdG9yeXQAGkxzY2FsYS9jb2xsZWN0aW9uL0ZhY3Rvcnk7eHBzcgAqc2 + |NhbGEuY29sbGVjdGlvbi5JdGVyYWJsZUZhY3RvcnkkVG9GYWN0b3J5AAAAAAAAAAMCAAFMAAdmYWN + |0b3J5dAAiTHNjYWxhL2NvbGxlY3Rpb24vSXRlcmFibGVGYWN0b3J5O3hwc3IAJnNjYWxhLnJ1bnRp + |bWUuTW9kdWxlU2VyaWFsaXphdGlvblByb3h5AAAAAAAAAAECAAFMAAttb2R1bGVDbGFzc3QAEUxqY + |XZhL2xhbmcvQ2xhc3M7eHB2cgAgc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUuTGlzdCQAAAAAAA + |AAAwIAAHhwdwT/////dAABQXQAAUJ0AAFDc3EAfgAGdnIAJnNjYWxhLmNvbGxlY3Rpb24uZ2VuZXJ + |pYy5TZXJpYWxpemVFbmQkAAAAAAAAAAMCAAB4cHg= + """.stripMargin.withoutWhitespaces.decoded[List[String]] mustEqual List("A", "B", "C") } } + + private def test(name: String)(f: AkkaSerializer => Unit): Unit = { + name in { + val system = ActorSystem.apply(s"test-${Random.nextInt()}", classLoader = Some(getClass.getClassLoader)) + val serializer: AkkaSerializer = new AkkaSerializerImpl(system) + f(serializer) + system.terminate().map(_ => Passed) + } + } +} + +object SerializerSpec { + + private implicit class ValueEncoder(private val any: Any) extends AnyVal { + def encoded(implicit s: AkkaSerializer): String = s.encode(any).get + } + + private implicit class StringDecoder(private val string: String) extends AnyVal { + def decoded[T: ClassTag](implicit s: AkkaSerializer): T = s.decode[T](string).get + } + + private implicit class StringOps(private val string: String) extends AnyVal { + def withoutWhitespaces: String = string.replaceAll("\\s", "") + } + + /** Plain test object to be cached */ + private final case class SimpleObject(key: String, value: Int) } + diff --git a/src/test/scala/play/api/cache/redis/connector/TestCase.scala b/src/test/scala/play/api/cache/redis/connector/TestCase.scala deleted file mode 100644 index 7cbca8e9..00000000 --- a/src/test/scala/play/api/cache/redis/connector/TestCase.scala +++ /dev/null @@ -1,20 +0,0 @@ -package play.api.cache.redis.connector - -import java.util.concurrent.atomic.AtomicInteger - -import org.specs2.execute.{AsResult, Result} -import org.specs2.specification.{Around, Scope} - -abstract class TestCase extends Around with Scope { - - protected val idx = TestCase.last.incrementAndGet() - - override def around[T: AsResult](t: => T): Result = { - AsResult.effectively(t) - } -} - -object TestCase { - - val last = new AtomicInteger() -} diff --git a/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala b/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala index 572b0e84..34bbc0a9 100644 --- a/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala @@ -1,321 +1,383 @@ package play.api.cache.redis.impl -import java.util.Optional - -import scala.concurrent.duration.Duration - import play.api.cache.redis._ +import play.api.cache.redis.test._ +import play.api.{Environment, Mode} import play.cache.redis._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification - -class AsyncJavaRedisSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import JavaCompatibility._ - import RedisCacheImplicits._ - - import org.mockito.ArgumentMatchers._ - - "Java Redis Cache" should { - - "get and miss" in new MockedJavaRedis { - async.get[String](anyString)(anyClassTag) returns None - cache.get[String](key).asScala must beEqualTo(Optional.empty).await - } - - "get and hit" in new MockedJavaRedis { - async.get[String](beEq(key))(anyClassTag) returns Some(value) - async.get[String](beEq(classTagKey))(anyClassTag) returns Some(classTag) - cache.get[String](key).asScala must beEqualTo(Optional.of(value)).await - } - - "get null" in new MockedJavaRedis { - async.get[String](beEq(classTagKey))(anyClassTag) returns Some("null") - cache.get[String](key).asScala must beEqualTo(Optional.empty).await - there was one(async).get[String](classTagKey) - } +import java.util.Optional +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters.IterableHasAsScala + +class AsyncJavaRedisSpec extends AsyncUnitSpec with AsyncRedisMock with RedisRuntimeMock { +import Helpers._ + + private val expiration = 5.seconds + private val expirationLong = expiration.toSeconds + private val expirationInt = expirationLong.intValue + private val classTag = "java.lang.String" + + test("get and miss") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- cache.get[String](cacheKey).assertingEqual(Optional.empty) + } yield Passed + } - "set" in new MockedJavaRedis { - async.set(anyString, anyString, any[Duration]) returns execDone - cache.set(key, value).asScala must beDone.await - there was one(async).set(key, value, Duration.Inf) - there was one(async).set(classTagKey, classTag, Duration.Inf) - } + test("get and hit") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + _ <- cache.get[String](cacheKey).assertingEqual(Optional.of(cacheValue)) + } yield Passed + } - "set with expiration" in new MockedJavaRedis { - async.set(anyString, anyString, any[Duration]) returns execDone - cache.set(key, value, expiration.toSeconds.toInt).asScala must beDone.await - there was one(async).set(key, value, expiration) - there was one(async).set(classTagKey, classTag, expiration) + test("get null") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, Some("null")) + _ <- cache.get[String](cacheKey).assertingEqual(Optional.empty) + } yield Passed } - "set null" in new MockedJavaRedis { - async.set(anyString, any, any[Duration]) returns execDone - cache.set(key, null: AnyRef).asScala must beDone.await - there was one(async).set(key, null, Duration.Inf) - there was one(async).set(classTagKey, "null", Duration.Inf) + test("set") { (async, cache) => + for { + _ <- async.expect.set(cacheKey, cacheValue, Duration.Inf) + _ <- cache.set(cacheKey, cacheValue).assertingDone + } yield Passed } - "get or else (hit)" in new MockedJavaRedis with OrElse { - async.get[String](beEq(key))(anyClassTag) returns Some(value) - async.get[String](beEq(classTagKey))(anyClassTag) returns Some(classTag) - cache.getOrElse(key, doElse(value)).asScala must beEqualTo(value).await - cache.getOrElseUpdate(key, doFuture(value).asJava).asScala must beEqualTo(value).await - orElse mustEqual 0 - there was two(async).get[String](key) - there was two(async).get[String](classTagKey) + test("set with expiration") { (async, cache) => + for { + _ <- async.expect.set(cacheKey, cacheValue, expiration) + _ <- cache.set(cacheKey, cacheValue, expiration.toSeconds.toInt).assertingDone + } yield Passed } - "get or else (miss)" in new MockedJavaRedis with OrElse { - async.get[String](beEq(classTagKey))(anyClassTag) returns None - async.set(anyString, anyString, any[Duration]) returns execDone - cache.getOrElse(key, doElse(value)).asScala must beEqualTo(value).await - cache.getOrElseUpdate(key, doFuture(value).asJava).asScala must beEqualTo(value).await - orElse mustEqual 2 - there was two(async).get[String](classTagKey) - there was two(async).set(key, value, Duration.Inf) - there was two(async).set(classTagKey, classTag, Duration.Inf) - } + test("set null") { (async, cache) => + for { + _ <- async.expect.set[AnyRef](cacheKey, null, Duration.Inf) + _ <- cache.set(cacheKey, null: AnyRef).assertingDone + } yield Passed + } - "get or else with expiration (hit)" in new MockedJavaRedis with OrElse { - async.get[String](beEq(key))(anyClassTag) returns Some(value) - async.get[String](beEq(classTagKey))(anyClassTag) returns Some(classTag) - cache.getOrElse(key, doElse(value), expiration.toSeconds.toInt).asScala must beEqualTo(value).await - cache.getOrElseUpdate(key, doFuture(value).asJava, expiration.toSeconds.toInt).asScala must beEqualTo(value).await - orElse mustEqual 0 - there was two(async).get[String](key) - } + test("get or else (sync)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- async.expect.set(cacheKey, cacheValue, Duration.Inf) + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + orElse = probe.orElse.const(cacheValue) + _ <- cache.getOrElse(cacheKey, orElse.execute _).assertingEqual(cacheValue) + _ <- cache.getOrElse(cacheKey, orElse.execute _).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else with expiration (miss)" in new MockedJavaRedis with OrElse { - async.get[String](beEq(classTagKey))(anyClassTag) returns None - async.set(anyString, anyString, any[Duration]) returns execDone - cache.getOrElse(key, doElse(value), expiration.toSeconds.toInt).asScala must beEqualTo(value).await - cache.getOrElseUpdate(key, doFuture(value).asJava, expiration.toSeconds.toInt).asScala must beEqualTo(value).await - orElse mustEqual 2 - there was two(async).get[String](classTagKey) - there was two(async).set(key, value, expiration) - there was two(async).set(classTagKey, classTag, expiration) - } + test("get or else (async)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- async.expect.set(cacheKey, cacheValue, Duration.Inf) + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + orElse = probe.orElse.asyncJava(cacheValue) + _ <- cache.getOrElseUpdate(cacheKey, orElse.execute _).assertingEqual(cacheValue) + _ <- cache.getOrElseUpdate(cacheKey, orElse.execute _).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get optional (none)" in new MockedJavaRedis { - async.get[String](anyString)(anyClassTag) returns None - cache.getOptional[String](key).asScala must beEqualTo(Optional.ofNullable(null)).await - } + test("get or else with expiration (sync)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- async.expect.set(cacheKey, cacheValue, expiration) + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + orElse = probe.orElse.const(cacheValue) + _ <- cache.getOrElse(cacheKey, orElse.execute _, expiration.toSeconds.toInt).assertingEqual(cacheValue) + _ <- cache.getOrElse(cacheKey, orElse.execute _, expiration.toSeconds.toInt).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get optional (some)" in new MockedJavaRedis { - async.get[String](anyString)(anyClassTag) returns Some("value") - async.get[String](beEq(classTagKey))(anyClassTag) returns Some(classTag) - cache.getOptional[String](key).asScala must beEqualTo(Optional.ofNullable("value")).await - } + test("get or else with expiration (async)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- async.expect.set(cacheKey, cacheValue, expiration) + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + orElse = probe.orElse.asyncJava(cacheValue) + _ <- cache.getOrElseUpdate(cacheKey, orElse.execute _, expiration.toSeconds.toInt).assertingEqual(cacheValue) + _ <- cache.getOrElseUpdate(cacheKey, orElse.execute _, expiration.toSeconds.toInt).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "remove" in new MockedJavaRedis { - async.remove(anyString) returns execDone - cache.remove(key).asScala must beDone.await - there was one(async).remove(key) - there was one(async).remove(classTagKey) - } + test("get optional (none)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, None) + _ <- cache.getOptional[String](cacheKey).assertingEqual(Optional.ofNullable(null)) + } yield Passed + } - "remove all" in new MockedJavaRedis { - async.invalidate() returns execDone - cache.removeAll().asScala must beDone.await - there was one(async).invalidate() + test("get optional (some)") { (async, cache) => + for { + _ <- async.expect.getClassTag(cacheKey, Some(classTag)) + _ <- async.expect.get[String](cacheKey, Some(cacheValue)) + _ <- cache.getOptional[String](cacheKey).assertingEqual(Optional.ofNullable(cacheValue)) + } yield Passed } - "get and set 'byte'" in new MockedJavaRedis { - val byte = JavaTypes.byteValue - - // set a value - // note: there should be hit on "byte" but the value is wrapped instead - async.set(anyString, beEq(byte), any[Duration]) returns execDone - async.set(anyString, beEq("byte"), any[Duration]) returns execDone - async.set(anyString, beEq("java.lang.Byte"), any[Duration]) returns execDone - cache.set(key, byte).asScala must beDone.await + test("remove") { (async, cache) => + for { + _ <- async.expect.remove(cacheKey) + _ <- cache.remove(cacheKey).assertingDone + } yield Passed + } + test("get and set 'byte'") { (async, cache) => + val byte = JavaTypes.byteValue + for { + // set a cacheValue + // note: there should be hit on "byte" but the cacheValue is wrapped instead + _ <- async.expect.setValue(cacheKey, byte, Duration.Inf) + _ <- async.expect.setClassTag(cacheKey, "java.lang.Byte", Duration.Inf) + _ <- cache.set(cacheKey, byte).assertingDone // hit on GET - async.get[Byte](beEq(key))(anyClassTag) returns Some(byte) - async.get[String](beEq(classTagKey))(anyClassTag) returns Some("java.lang.Byte") - cache.get[Byte](key).asScala must beEqualTo(Optional.ofNullable(byte)).await - } + _ <- async.expect.getClassTag(cacheKey, Some("java.lang.Byte")) + _ <- async.expect.get[java.lang.Byte](cacheKey, Some(byte)) + _ <- cache.get[Byte](cacheKey).assertingEqual(Optional.ofNullable(byte)) + } yield Passed + } - "get and set 'byte[]'" in new MockedJavaRedis { - val bytes = JavaTypes.bytesValue + test("get and set 'byte[]'") { (async, cache) => + val scalaBytes = JavaTypes.bytesValue + val javaBytes = scalaBytes.map[java.lang.Byte](x => x) + for { + // set a cacheValue + _ <- async.expect.setValue(cacheKey, scalaBytes, Duration.Inf) + _ <- async.expect.setClassTag(cacheKey, "byte[]", Duration.Inf) + _ <- cache.set(cacheKey, scalaBytes).assertingDone + // hit on GET + _ <- async.expect.getClassTag(cacheKey, Some("byte[]")) + _ <- async.expect.get[Array[java.lang.Byte]](cacheKey, Some(javaBytes)) + _ <- cache.get[Array[java.lang.Byte]](cacheKey).assertingEqual(Optional.ofNullable(javaBytes)) + } yield Passed + } - // set a value - async.set(anyString, beEq(bytes), any[Duration]) returns execDone - async.set(anyString, beEq("byte[]"), any[Duration]) returns execDone - cache.set(key, bytes).asScala must beDone.await + test("get all") { (async, cache) => + for { + _ <- async.expect.getAllKeys[String](Iterable(cacheKey, cacheKey, cacheKey), Seq(Some(cacheValue), None, None)) + _ <- cache + .getAll(classOf[String], cacheKey, cacheKey, cacheKey) + .asserting(_.asScala.toList mustEqual List(Optional.of(cacheValue), Optional.empty, Optional.empty)) + } yield Passed + } - // hit on GET - async.get[Array[Byte]](beEq(key))(anyClassTag) returns Some(bytes) - async.get[String](beEq(classTagKey))(anyClassTag) returns Some("byte[]") - cache.get[Array[Byte]](key).asScala must beEqualTo(Optional.ofNullable(bytes)).await - } + test("get all (keys in a collection)") { (async, cache) => + import JavaCompatibility.JavaList + for { + _ <- async.expect.getAllKeys[String](Iterable(cacheKey, cacheKey, cacheKey), Seq(Some(cacheValue), None, None)) + _ <- cache + .getAll(classOf[String], JavaList(cacheKey, cacheKey, cacheKey)) + .asserting(_.asScala.toList mustEqual List(Optional.of(cacheValue), Optional.empty, Optional.empty)) + } yield Passed + } - "get all" in new MockedJavaRedis { - async.getAll[String](beEq(Iterable(key, key, key)))(anyClassTag) returns Seq(Some(value), None, None) - cache.getAll(classOf[String], key, key, key).asScala.map(_.asScala) must beEqualTo(Seq(Optional.of(value), Optional.empty, Optional.empty)).await - } + test("set if not exists (exists)") { (async, cache) => + for { + _ <- async.expect.setIfNotExists(cacheKey, cacheValue, Duration.Inf, exists = false) + _ <- cache.setIfNotExists(cacheKey, cacheValue).assertingEqual(false) + } yield Passed + } - "get all (keys in a collection)" in new MockedJavaRedis { - async.getAll[String](beEq(Iterable(key, key, key)))(anyClassTag) returns Seq(Some(value), None, None) - cache.getAll(classOf[String], JavaList(key, key, key)).asScala.map(_.asScala) must beEqualTo(Seq(Optional.of(value), Optional.empty, Optional.empty)).await - } + test("set if not exists (not exists)") { (async, cache) => + for { + _ <- async.expect.setIfNotExists(cacheKey, cacheValue, Duration.Inf, exists = true) + _ <- cache.setIfNotExists(cacheKey, cacheValue).assertingEqual(true) + } yield Passed + } - "set if not exists (exists)" in new MockedJavaRedis { - async.setIfNotExists(beEq(key), beEq(value), any[Duration]) returns false - async.setIfNotExists(beEq(classTagKey), beEq(classTag), any[Duration]) returns false - cache.setIfNotExists(key, value).asScala.map(Boolean.unbox) must beFalse.await - there was one(async).setIfNotExists(key, value, null) - there was one(async).setIfNotExists(classTagKey, classTag, null) - } + test("set if not exists (exists) with expiration") { (async, cache) => + for { + _ <- async.expect.setIfNotExists(cacheKey, cacheValue, expiration, exists = false) + _ <- cache.setIfNotExists(cacheKey, cacheValue, expirationInt).assertingEqual(false) + } yield Passed + } - "set if not exists (not exists)" in new MockedJavaRedis { - async.setIfNotExists(beEq(key), beEq(value), any[Duration]) returns true - async.setIfNotExists(beEq(classTagKey), beEq(classTag), any[Duration]) returns true - cache.setIfNotExists(key, value).asScala.map(Boolean.unbox) must beTrue.await - there was one(async).setIfNotExists(key, value, null) - there was one(async).setIfNotExists(classTagKey, classTag, null) - } + test("set if not exists (not exists) with expiration") { (async, cache) => + for { + _ <- async.expect.setIfNotExists(cacheKey, cacheValue, expiration, exists = true) + _ <- cache.setIfNotExists(cacheKey, cacheValue, expirationInt).assertingEqual(true) + } yield Passed + } - "set if not exists (exists) with expiration" in new MockedJavaRedis { - async.setIfNotExists(beEq(key), beEq(value), any[Duration]) returns false - async.setIfNotExists(beEq(classTagKey), beEq(classTag), any[Duration]) returns false - cache.setIfNotExists(key, value, expirationInt).asScala.map(Boolean.unbox) must beFalse.await - there was one(async).setIfNotExists(key, value, expiration) - there was one(async).setIfNotExists(classTagKey, classTag, expiration) - } + test("set all") { (async, cache) => + val values = Seq((cacheKey, cacheValue), (otherKey, otherValue)) + for { + _ <- async.expect.setAll(values: _*) + javaValues = values.map { case (k, v) => new KeyValue(k, v) } + _ <- cache.setAll(javaValues: _*).assertingDone + } yield Passed + } - "set if not exists (not exists) with expiration" in new MockedJavaRedis { - async.setIfNotExists(beEq(key), beEq(value), any[Duration]) returns true - async.setIfNotExists(beEq(classTagKey), beEq(classTag), any[Duration]) returns true - cache.setIfNotExists(key, value, expirationInt).asScala.map(Boolean.unbox) must beTrue.await - there was one(async).setIfNotExists(key, value, expiration) - there was one(async).setIfNotExists(classTagKey, classTag, expiration) - } + test("set all if not exists (exists)") { (async, cache) => + val values = Seq((cacheKey, cacheValue), (otherKey, otherValue)) + for { + _ <- async.expect.setAllIfNotExist(values, exists = false) + javaValues = values.map { case (k, v) => new KeyValue(k, v) } + _ <- cache.setAllIfNotExist(javaValues: _*).assertingEqual(false) + } yield Passed + } - "set all" in new MockedJavaRedis { - async.setAll(anyVarArgs) returns Done - cache.setAll(new KeyValue(key, value), new KeyValue(other, value)).asScala must beDone.await - there was one(async).setAll((key, value), (classTagKey, classTag), (other, value), (classTagOther, classTag)) - } + test("set all if not exists (not exists)") { (async, cache) => + val values = Seq((cacheKey, cacheValue), (otherKey, otherValue)) + for { + _ <- async.expect.setAllIfNotExist(values, exists = true) + javaValues = values.map { case (k, v) => new KeyValue(k, v) } + _ <- cache.setAllIfNotExist(javaValues: _*).assertingEqual(true) + } yield Passed + } - "set all if not exists (exists)" in new MockedJavaRedis { - async.setAllIfNotExist(anyVarArgs) returns false - cache.setAllIfNotExist(new KeyValue(key, value), new KeyValue(other, value)).asScala.map(Boolean.unbox) must beFalse.await - there was one(async).setAllIfNotExist((key, value), (classTagKey, classTag), (other, value), (classTagOther, classTag)) - } + test("append") { (async, cache) => + for { + _ <- async.expect.append(cacheKey, cacheValue, Duration.Inf) + _ <- async.expect.setClassTagIfNotExists(cacheKey, cacheValue, Duration.Inf, exists = false) + _ <- cache.append(cacheKey, cacheValue).assertingDone + } yield Passed + } - "set all if not exists (not exists)" in new MockedJavaRedis { - async.setAllIfNotExist(anyVarArgs) returns true - cache.setAllIfNotExist(new KeyValue(key, value), new KeyValue(other, value)).asScala.map(Boolean.unbox) must beTrue.await - there was one(async).setAllIfNotExist((key, value), (classTagKey, classTag), (other, value), (classTagOther, classTag)) - } + test("append with expiration") { (async, cache) => + for { + _ <- async.expect.append(cacheKey, cacheValue, expiration) + _ <- async.expect.setClassTagIfNotExists(cacheKey, cacheValue, expiration, exists = false) + _ <- cache.append(cacheKey, cacheValue, expirationInt).assertingDone + } yield Passed + } - "append" in new MockedJavaRedis { - async.append(anyString, anyString, any[Duration]) returns Done - async.setIfNotExists(anyString, anyString, any[Duration]) returns false - cache.append(key, value).asScala must beDone.await - there was one(async).append(key, value, null) - there was one(async).setIfNotExists(classTagKey, classTag, null) - } + test("expire") { (async, cache) => + for { + _ <- async.expect.expire(cacheKey, expiration) + _ <- cache.expire(cacheKey, expirationInt).assertingDone + } yield Passed + } - "append with expiration" in new MockedJavaRedis { - async.append(anyString, anyString, any[Duration]) returns Done - async.setIfNotExists(anyString, anyString, any[Duration]) returns false - cache.append(key, value, expirationInt).asScala must beDone.await - there was one(async).append(key, value, expiration) - there was one(async).setIfNotExists(classTagKey, classTag, expiration) - } + test("expires in (defined)") { (async, cache) => + for { + _ <- async.expect.expiresIn(cacheKey, Some(expiration)) + _ <- cache.expiresIn(cacheKey).assertingEqual(Optional.of(expirationLong)) + } yield Passed + } - "expire" in new MockedJavaRedis { - async.expire(anyString, any[Duration]) returns Done - cache.expire(key, expirationInt).asScala must beDone.await - there was one(async).expire(key, expiration) - there was one(async).expire(classTagKey, expiration) - } + test("expires in (undefined)") { (async, cache) => + for { + _ <- async.expect.expiresIn(cacheKey, None) + _ <- cache.expiresIn(cacheKey).assertingEqual(Optional.empty) + } yield Passed + } - "expires in (defined)" in new MockedJavaRedis { - async.expiresIn(anyString) returns Some(expiration) - cache.expiresIn(key).asScala must beEqualTo(Optional.of(expirationLong)).await - there was one(async).expiresIn(key) - there was no(async).expiresIn(classTagKey) - } + test("matching") { (async, cache) => + for { + _ <- async.expect.matching("pattern", Seq(cacheKey)) + _ <- cache.matching("pattern").asserting(_.asScala.toList mustEqual List(cacheKey)) + } yield Passed + } - "expires in (undefined)" in new MockedJavaRedis { - async.expiresIn(anyString) returns None - cache.expiresIn(key).asScala must beEqualTo(Optional.empty).await - there was one(async).expiresIn(key) - there was no(async).expiresIn(classTagKey) - } + test("remove multiple") { (async, cache) => + for { + _ <- async.expect.removeAll(cacheKey, otherKey) + _ <- cache.remove(cacheKey, otherKey).assertingDone + } yield Passed + } - "matching" in new MockedJavaRedis { - async.matching(anyString) returns Seq(key) - cache.matching("pattern").asScala.map(_.asScala) must beEqualTo(Seq(key)).await - there was one(async).matching("pattern") - } + test("remove all (invalidate)") { (async, cache) => + for { + _ <- async.expect.invalidate() + _ <- cache.removeAll().assertingDone + } yield Passed + } - "remove multiple" in new MockedJavaRedis { - async.removeAll(anyVarArgs) returns Done - cache.remove(key, key, key, key).asScala must beDone.await - there was one(async).removeAll(key, classTagKey, key, classTagKey, key, classTagKey, key, classTagKey) - } + test("remove all (some keys provided)") { (async, cache) => + for { + _ <- async.expect.removeAll(cacheKey, otherKey) + _ <- cache.removeAllKeys(cacheKey, otherKey).assertingDone + } yield Passed + } - "remove all" in new MockedJavaRedis { - async.removeAll(anyVarArgs) returns Done - cache.removeAllKeys(key, key, key, key).asScala must beDone.await - there was one(async).removeAll(key, classTagKey, key, classTagKey, key, classTagKey, key, classTagKey) + test("remove matching") { (async, cache) => + for { + _ <- async.expect.removeMatching("pattern") + _ <- cache.removeMatching("pattern").assertingDone + } yield Passed } - "remove matching" in new MockedJavaRedis { - async.removeMatching(anyString) returns Done - cache.removeMatching("pattern").asScala must beDone.await - there was one(async).removeMatching("pattern") - there was one(async).removeMatching("classTag::pattern") - } + test("exists") { (async, cache) => + for { + _ <- async.expect.exists(cacheKey, exists = true) + _ <- cache.exists(cacheKey).assertingEqual(true) + } yield Passed + } - "exists" in new MockedJavaRedis { - async.exists(beEq(key)) returns true - cache.exists(key).asScala.map(Boolean.unbox) must beTrue.await - there was one(async).exists(key) - there was no(async).exists(classTagKey) - } + test("increment") { (async, cache) => + for { + _ <- async.expect.increment(cacheKey, 1L, result = 10L) + _ <- async.expect.increment(cacheKey, 2L, result = 20L) + _ <- cache.increment(cacheKey).assertingEqual(10L) + _ <- cache.increment(cacheKey, 2L).assertingEqual(20L) + } yield Passed + } - "increment" in new MockedJavaRedis { - async.increment(beEq(key), anyLong) returns 10L - cache.increment(key).asScala.map(Long.unbox) must beEqualTo(10L).await - cache.increment(key, 2L).asScala.map(Long.unbox) must beEqualTo(10L).await - there was one(async).increment(key, by = 1L) - there was one(async).increment(key, by = 2L) - } + test("decrement") { (async, cache) => + for { + _ <- async.expect.decrement(cacheKey, 1L, result = 10L) + _ <- async.expect.decrement(cacheKey, 2L, result = 20L) + _ <- cache.decrement(cacheKey).assertingEqual(10L) + _ <- cache.decrement(cacheKey, 2L).assertingEqual(20L) + } yield Passed + } - "decrement" in new MockedJavaRedis { - async.decrement(beEq(key), anyLong) returns 10L - cache.decrement(key).asScala.map(Long.unbox) must beEqualTo(10L).await - cache.decrement(key, 2L).asScala.map(Long.unbox) must beEqualTo(10L).await - there was one(async).decrement(key, by = 1L) - there was one(async).decrement(key, by = 2L) - } + test("create list") { (async, cache) => + trait RedisListMock extends RedisList[String, Future] + val list = mock[RedisListMock] + for { + _ <- async.expect.list[String](cacheKey, list) + _ <- cache.list(cacheKey, classOf[String]) mustBe a[AsyncRedisList[_]] + } yield Passed + } - "create list" in new MockedJavaRedis { - private val list = mock[RedisList[String, Future]] - async.list(beEq(key))(anyClassTag[String]) returns list - cache.list(key, classOf[String]) must beAnInstanceOf[AsyncRedisList[String]] - there was one(async).list[String](key) - } + test("create set") { (async, cache) => + trait RedisSetMock extends RedisSet[String, Future] + val set = mock[RedisSetMock] + for { + _ <- async.expect.set[String](cacheKey, set) + _ <- cache.set(cacheKey, classOf[String]) mustBe a[AsyncRedisSet[_]] + } yield Passed + } - "create set" in new MockedJavaRedis { - private val set = mock[RedisSet[String, Future]] - async.set(beEq(key))(anyClassTag[String]) returns set - cache.set(key, classOf[String]) must beAnInstanceOf[AsyncRedisSet[String]] - there was one(async).set[String](key) - } + test("create map") { (async, cache) => + trait RedisMapMock extends RedisMap[String, Future] + val map = mock[RedisMapMock] + for { + _ <- async.expect.map[String](cacheKey, map) + _ <- cache.map(cacheKey, classOf[String]) mustBe a[AsyncRedisMap[_]] + } yield Passed + } - "create map" in new MockedJavaRedis { - private val map = mock[RedisMap[String, Future]] - async.map(beEq(key))(anyClassTag[String]) returns map - cache.map(key, classOf[String]) must beAnInstanceOf[AsyncRedisMap[String]] - there was one(async).map[String](key) + private def test(name: String)(f: (AsyncRedisMock, play.cache.redis.AsyncCacheApi) => Future[Assertion]): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = recoveryPolicy.default, + ) + implicit val environment: Environment = Environment( + rootPath = new java.io.File("."), + classLoader = getClass.getClassLoader, + mode = Mode.Test + ) + val async = mock[AsyncRedisMock] + val cache: play.cache.redis.AsyncCacheApi = new AsyncJavaRedis(async) + + f(async, cache) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala b/src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala new file mode 100644 index 00000000..a60d7bb6 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala @@ -0,0 +1,244 @@ +package play.api.cache.redis.impl + +import akka.Done +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis._ + +import scala.concurrent.Future +import scala.concurrent.duration.Duration +import scala.reflect.ClassTag + +private[impl] trait AsyncRedisMock { this: AsyncMockFactoryBase => + + protected[impl] trait AsyncRedisMock extends AsyncRedis { + + final override def removeAll(keys: String*): AsynchronousResult[Done] = + removeAllKeys(keys) + + def removeAllKeys(keys: Seq[String]): AsynchronousResult[Done] + + final override def getAll[T: ClassTag](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]] = + getAllKeys(keys) + + def getAllKeys[T: ClassTag](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]] + } + + final protected implicit class AsyncRedisOps(async: AsyncRedisMock) { + def expect: AsyncRedisExpectation = + new AsyncRedisExpectation(async) + } + + protected final class AsyncRedisExpectation(async: AsyncRedisMock) { + + private def classTagKey(key: String): String = s"classTag::$key" + + private def classTagValue: Any => String = { + case null => "null" + case v if v.getClass == classOf[String] => "java.lang.String" + case other => throw new IllegalArgumentException(s"Unexpected value for classTag: ${other.getClass.getSimpleName}") + } + + def getClassTag(key: String, value: Option[String]): Future[Unit] = + get(classTagKey(key), value) + + def get[T: ClassTag](key: String, value: Option[T]): Future[Unit] = + Future.successful { + (async.get(_: String)(_: ClassTag[_])) + .expects(key, implicitly[ClassTag[T]]) + .returning(Future.successful(value)) + .once() + } + + def getAllKeys[T: ClassTag](keys: Iterable[String], values: Seq[Option[T]]): Future[Unit] = + Future.successful { + (async.getAllKeys(_: Iterable[String])(_: ClassTag[_])) + .expects(keys, implicitly[ClassTag[T]]) + .returning(Future.successful(values)) + .once() + } + + def setValue[T](key: String, value: T, duration: Duration): Future[Unit] = + Future.successful { + (async.set(_: String, _: Any, _: Duration)) + .expects(key, if (value == null) * else value, duration) + .returning(Future.successful(Done)) + .once() + } + + def setClassTag[T: ClassTag](key: String, value: T, duration: Duration): Future[Unit] = + setValue(classTagKey(key), value, duration) + + def set[T: ClassTag](key: String, value: T, duration: Duration): Future[Unit] = + for { + _ <- setValue(key, value, duration) + _ <- setClassTag(key, classTagValue(value), duration) + } yield () + + def setValueIfNotExists[T: ClassTag](key: String, value: T, duration: Duration, exists: Boolean): Future[Unit] = + Future.successful { + (async.setIfNotExists(_: String, _: Any, _: Duration)) + .expects(key, if (value == null) * else value, duration) + .returning(Future.successful(exists)) + .once() + } + + def setClassTagIfNotExists[T: ClassTag](key: String, value: T, duration: Duration, exists: Boolean): Future[Unit] = + setValueIfNotExists(classTagKey(key), classTagValue(value), duration, exists) + + def setIfNotExists[T: ClassTag](key: String, value: T, duration: Duration, exists: Boolean): Future[Unit] = + for { + _ <- setValueIfNotExists(key, value, duration, exists) + _ <- setClassTagIfNotExists(key, value, duration, exists) + } yield () + + def setAll[T: ClassTag](values: (String, Any)*): Future[Unit] = + Future.successful { + val valuesWithClassTags = values.flatMap { + case (k, v) => Seq((k, v), (classTagKey(k), classTagValue(v))) + } + (async.setAll _) + .expects(valuesWithClassTags) + .returning(Future.successful(Done)) + .once() + } + + def setAllIfNotExist[T: ClassTag](values: Seq[(String, Any)], exists: Boolean): Future[Unit] = + Future.successful { + val valuesWithClassTags = values.flatMap { + case (k, v) => Seq((k, v), (classTagKey(k), classTagValue(v))) + } + (async.setAllIfNotExist _) + .expects(valuesWithClassTags) + .returning(Future.successful(exists)) + .once() + } + + def expire(key: String, duration: Duration): Future[Unit] = + Future.successful { + (async.expire(_: String, _: Duration)) + .expects(classTagKey(key), duration) + .returning(Future.successful(Done)) + .once() + (async.expire(_: String, _: Duration)) + .expects(key, duration) + .returning(Future.successful(Done)) + .once() + } + + def expiresIn(key: String, duration: Option[Duration]): Future[Unit] = + Future.successful { + (async.expiresIn(_: String)) + .expects(key) + .returning(Future.successful(duration)) + .once() + } + + def matching(pattern: String, keys: Seq[String]): Future[Unit] = + Future.successful { + (async.matching(_: String)) + .expects(pattern) + .returning(Future.successful(keys)) + .once() + } + + def removeMatching(pattern: String): Future[Unit] = { + def removePattern(patternToRemove: String) = + (async.removeMatching(_: String)) + .expects(patternToRemove) + .returning(Future.successful(Done)) + .once() + + Future.successful { + removePattern(pattern) + removePattern(classTagKey(pattern)) + } + } + + def exists(key: String, exists: Boolean): Future[Unit] = + Future.successful { + (async.exists(_: String)) + .expects(key) + .returning(Future.successful(exists)) + .once() + } + + def increment(key: String, by: Long, result: Long): Future[Unit] = + Future.successful { + (async.increment(_: String, _: Long)) + .expects(key, by) + .returning(Future.successful(result)) + .once() + } + + def decrement(key: String, by:Long, result:Long): Future[Unit] = + Future.successful { + (async.decrement(_: String, _:Long)) + .expects(key, by) + .returning(Future.successful(result)) + .once() + } + + def remove(key: String): Future[Unit] = + Future.successful { + (async.remove(_: String)) + .expects(classTagKey(key)) + .returning(Future.successful(Done)) + .once() + (async.remove(_: String)) + .expects(key) + .returning(Future.successful(Done)) + .once() + } + + def removeAll(keys: String*): Future[Unit] = + Future.successful { + val keysWithClassTags = keys.flatMap { + key => Seq(key, classTagKey(key)) + } + (async.removeAllKeys _) + .expects(keysWithClassTags) + .returning(Future.successful(Done)) + .once() + } + + def append(key: String, value: String, expiration: Duration): Future[Unit] = + Future.successful { + (async.append(_: String, _: String, _: Duration)) + .expects(key, value, expiration) + .returning(Future.successful(Done)) + .once() + } + + def invalidate(): Future[Unit] = + Future.successful { + (() => async.invalidate()) + .expects() + .returning(Future.successful(Done)) + .once() + } + + def list[T: ClassTag](key: String, mock: RedisList[T, AsynchronousResult]): Future[Unit] = + Future.successful { + (async.list[T](_: String)(_: ClassTag[T])) + .expects(key, implicitly[ClassTag[T]]) + .returning(mock) + .once() + } + + def set[T: ClassTag](key: String, mock: RedisSet[T, AsynchronousResult]): Future[Unit] = + Future.successful { + (async.set[T](_: String)(_: ClassTag[T])) + .expects(key, implicitly[ClassTag[T]]) + .returning(mock) + .once() + } + + def map[T: ClassTag](key: String, mock: RedisMap[T, AsynchronousResult]): Future[Unit] = + Future.successful { + (async.map[T](_: String)(_: ClassTag[T])) + .expects(key, implicitly[ClassTag[T]]) + .returning(mock) + .once() + } + } +} \ No newline at end of file diff --git a/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala index d98d4311..d32a6111 100644 --- a/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala @@ -1,68 +1,83 @@ package play.api.cache.redis.impl import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class AsyncRedisSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import RedisCacheImplicits._ - import org.mockito.ArgumentMatchers._ +class AsyncRedisSpec extends AsyncUnitSpec with RedisConnectorMock with RedisRuntimeMock with ImplicitFutureMaterialization { + import Helpers._ - "AsyncRedis" should { + test("removeAll") { (connector, cache) => + for { + _ <- connector.expect.invalidate() + _ <- cache.removeAll().assertingDone + } yield Passed + } - "removeAll" in new MockedAsyncRedis { - connector.invalidate() returns unit - cache.removeAll() must beDone.await - there was one(connector).invalidate() - } + test("getOrElseUpdate (hit)") { (connector, cache) => + for { + _ <- connector.expect.get(cacheKey, Some(cacheValue)) + orElse = probe.orElse.async(cacheValue) + _ <- cache.getOrElseUpdate[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 0 + } yield Passed + } - "getOrElseUpdate (hit)" in new MockedAsyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns Some(value) - cache.getOrElseUpdate(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 0 - } + test("getOrElseUpdate (miss)") { (connector, cache) => + for { + _ <- connector.expect.get[String](cacheKey, None) + _ <- connector.expect.set(cacheKey, cacheValue, Duration.Inf, result = true) + orElse = probe.orElse.async(cacheValue) + _ <- cache.getOrElseUpdate[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "getOrElseUpdate (miss)" in new MockedAsyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], anyBoolean) returns true - cache.getOrElseUpdate(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("getOrElseUpdate (failure)") { (connector, cache) => + for { + _ <- connector.expect.get[String](cacheKey, SimulatedException.asRedis) + orElse = probe.orElse.async(cacheValue) + _ <- cache.getOrElseUpdate[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "getOrElseUpdate (failure)" in new MockedAsyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns ex - cache.getOrElseUpdate(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("getOrElseUpdate (failing orElse)") { (connector, cache) => + for { + _ <- connector.expect.get[String](cacheKey, None) + orElse = probe.orElse.failing(SimulatedException.asRedis) + _ <- cache.getOrElseUpdate[String](cacheKey)(orElse.execute()).assertingFailure[TimeoutException] + _ = orElse.calls mustEqual 2 + } yield Passed + } - "getOrElseUpdate (failing orElse)" in new MockedAsyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - cache.getOrElseUpdate[String](key)(failedFuture) must throwA[TimeoutException].await - orElse mustEqual 2 - } + test("getOrElseUpdate (rerun)", policy = recoveryPolicy.rerun) { (connector, cache) => + for { + _ <- connector.expect.get[String](cacheKey, None) + _ <- connector.expect.get[String](cacheKey, None) + _ <- connector.expect.set(cacheKey, cacheValue, Duration.Inf, result = true) + orElse = probe.orElse.generic( + Future.failed(SimulatedException.asRedis), + Future.successful(cacheValue), + ) + _ <- cache.getOrElseUpdate[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 2 + } yield Passed + } + + private def test(name: String, policy: RecoveryPolicy = recoveryPolicy.default)(f: (RedisConnectorMock, AsyncRedis) => Future[Assertion]): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + val connector = mock[RedisConnectorMock] + val cache: AsyncRedis = new AsyncRedisImpl(connector) - "getOrElseUpdate (rerun)" in new MockedAsyncRedis with OrElse with Attempts { - override protected def policy = new RecoveryPolicy { - def recoverFrom[T](rerun: => Future[T], default: => Future[T], failure: RedisException) = rerun - } - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], anyBoolean) returns true - // run the test - cache.getOrElseUpdate(key) { - attempts match { - case 0 => attempt(failedFuture) - case _ => attempt(doFuture(value)) - } - } must beEqualTo(value).await - // verification - orElse mustEqual 2 - there were two(connector).get[String](anyString)(anyClassTag) - there was one(connector).set(key, value, Duration.Inf, ifNotExists = false) + f(connector, cache) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala b/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala index 7ea241d5..e3725e81 100644 --- a/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala @@ -1,29 +1,32 @@ package play.api.cache.redis.impl -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} - -import play.api.cache.redis._ - import akka.pattern.AskTimeoutException -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification -import org.specs2.specification._ +import play.api.cache.redis._ +import play.api.cache.redis.test._ -class BuildersSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito with WithApplication { +import scala.concurrent.duration._ +import scala.concurrent.{Future, Promise} +class BuildersSpec extends AsyncUnitSpec with RedisRuntimeMock { import Builders._ - import BuildersSpec._ - import Implicits._ - def defaultTask = Future.successful("default") + private case class Task( + response: String, + execution: () => Future[String], + ) extends (() => Future[String]) { - def regularTask = Future("response") + def this(response: String)(f: String => Future[String]) = + this(response, () => f(response)) - def longTask = Future.after(seconds = 2, "response") + def apply(): Future[String] = execution() + } - def failingTask = Future.failed(TimeoutException(new IllegalArgumentException("Simulated failure."))) + private object Task { + val resolved: Task = new Task("default response")(Future.successful) + val regular: Task = new Task("regular response")(Future(_)) + def failing(): Future[String] = Future.failed(TimeoutException(SimulatedException)) + def infinite(): Future[String] = Promise[String]().future + } "AsynchronousBuilder" should { @@ -31,26 +34,29 @@ class BuildersSpec(implicit ee: ExecutionEnv) extends Specification with Reduced AsynchronousBuilder.name mustEqual "AsynchronousBuilder" } - "run" in new RuntimeMock { - AsynchronousBuilder.toResult(regularTask, defaultTask) must beEqualTo("response").await + "run regular task" in { + implicit val runtime: RedisRuntime = redisRuntime() + AsynchronousBuilder.toResult(Task.regular(), Task.resolved()).assertingEqual(Task.regular.response) } - "run long running task" in new RuntimeMock { - AsynchronousBuilder.toResult(longTask, defaultTask) must beEqualTo("response").awaitFor(3.seconds) + "run resolved task" in { + implicit val runtime: RedisRuntime = redisRuntime() + AsynchronousBuilder.toResult(Task.resolved(),Task.regular()).assertingEqual(Task.resolved.response) } - "recover with default policy" in new RuntimeMock { - runtime.policy returns defaultPolicy - AsynchronousBuilder.toResult(failingTask, defaultTask) must beEqualTo("default").await + "recover with default policy" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.default) + AsynchronousBuilder.toResult(Task.failing(),Task.resolved()).assertingEqual(Task.resolved.response) } - "recover with fail through policy" in new RuntimeMock { - runtime.policy returns failThrough - AsynchronousBuilder.toResult(failingTask, defaultTask) must throwA[TimeoutException].await + "recover with fail through policy" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.failThrough) + AsynchronousBuilder.toResult(Task.failing(),Task.resolved()).assertingFailure[TimeoutException] } - "map value" in new RuntimeMock { - AsynchronousBuilder.map(Future(5))(_ + 5) must beEqualTo(10).await + "map value" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.failThrough) + AsynchronousBuilder.map(Future(5))(_ + 5).assertingEqual(10) } } @@ -60,51 +66,55 @@ class BuildersSpec(implicit ee: ExecutionEnv) extends Specification with Reduced SynchronousBuilder.name mustEqual "SynchronousBuilder" } - "run" in new RuntimeMock { - SynchronousBuilder.toResult(regularTask, defaultTask) must beEqualTo("response") + "run regular task" in { + implicit val runtime: RedisRuntime = redisRuntime() + SynchronousBuilder.toResult(Task.regular(), Task.resolved()) mustEqual Task.regular.response } - "recover from failure with default policy" in new RuntimeMock { - runtime.policy returns defaultPolicy - SynchronousBuilder.toResult(failingTask, defaultTask) must beEqualTo("default") + "run resolved task" in { + implicit val runtime: RedisRuntime = redisRuntime() + SynchronousBuilder.toResult(Task.resolved(), Task.regular()) mustEqual Task.resolved.response } - "recover from failure with fail through policy" in new RuntimeMock { - runtime.policy returns failThrough - SynchronousBuilder.toResult(failingTask, defaultTask) must throwA[TimeoutException] + "recover from failure with default policy" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.default) + SynchronousBuilder.toResult(Task.failing(), Task.resolved()) mustEqual Task.resolved.response } - "recover from timeout due to long running task" in new RuntimeMock { - runtime.policy returns failThrough - SynchronousBuilder.toResult(longTask, defaultTask) must throwA[TimeoutException] + "don't recover from failure with fail through policy" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.failThrough) + assertThrows[TimeoutException] { + SynchronousBuilder.toResult(Task.failing(), Task.resolved()) + } } - "recover from akka ask timeout" in new RuntimeMock { - runtime.policy returns failThrough - val actorFailure = Future.failed(new AskTimeoutException("Simulated actor ask timeout")) - SynchronousBuilder.toResult(actorFailure, defaultTask) must throwA[TimeoutException] + "don't recover on timeout due to long running task with fail through policy" in { + implicit val runtime: RedisRuntime = redisRuntime( + recoveryPolicy = recoveryPolicy.failThrough, + timeout = 1.millis + ) + assertThrows[TimeoutException] { + SynchronousBuilder.toResult(Task.infinite(), Task.resolved()) + } } - "map value" in new RuntimeMock { - SynchronousBuilder.map(5)(_ + 5) must beEqualTo(10) + "recover from timeout due to long running task with default policy" in { + implicit val runtime: RedisRuntime = redisRuntime( + recoveryPolicy = recoveryPolicy.default, + timeout = 1.millis + ) + SynchronousBuilder.toResult(Task.infinite(), Task.resolved()) mustEqual Task.resolved.response } - } -} - -object BuildersSpec { - - trait RuntimeMock extends Scope { - - import MockitoImplicits._ - - private val timeout = akka.util.Timeout(1.second) - implicit protected val runtime: RedisRuntime = mock[RedisRuntime] - runtime.timeout returns timeout - runtime.context returns ExecutionContext.global - - protected def failThrough = new FailThrough {} + "recover from akka ask timeout" in { + implicit val runtime: RedisRuntime = redisRuntime(recoveryPolicy = recoveryPolicy.default) + val actorFailure = Future.failed(new AskTimeoutException("Simulated actor ask timeout")) + SynchronousBuilder.toResult(actorFailure, Task.resolved()) mustEqual Task.resolved.response + } - protected def defaultPolicy = new RecoverWithDefault {} + "map value" in { + implicit val runtime: RedisRuntime = redisRuntime() + SynchronousBuilder.map(5)(_ + 5) mustEqual 10 + } } } diff --git a/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpec.scala b/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpec.scala index dc9297e5..1595ad5e 100644 --- a/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpec.scala @@ -1,39 +1,34 @@ package play.api.cache.redis.impl -import scala.concurrent.Future -import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.test.UnitSpec -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification - -class InvocationPolicySpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito with WithApplication { - - import Implicits._ - import RedisCacheImplicits._ +import scala.concurrent._ +import scala.util.Success - val andThen = "then" +class InvocationPolicySpec extends UnitSpec { - def longTask(andThen: => Unit) = Future.after(seconds = 2, { andThen; "result" }) + private implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.parasitic - "InvocationPolicy" should { + private class Probe { + private val promise = Promise[Unit]() + def resolve(): Unit = promise.success(()) + def run(): Future[Unit] = promise.future + } - "invoke lazily, i.e., slowly" in { - var resolved = false - val promise = longTask { resolved = true } - LazyInvocation.invoke(promise, andThen) must beEqualTo(andThen).awaitFor(3.seconds) - resolved mustEqual true - promise.isCompleted mustEqual true - } + "invoke lazily, i.e., slowly" in { + val probe = new Probe + val outcome = LazyInvocation.invoke(probe.run(), Done) + outcome.value mustEqual None + probe.resolve() + outcome.value mustEqual Some(Success(Done)) + } - "invoke eagerly, i.e., return immediately" in { - var resolved = false - val promise = longTask { resolved = true } - EagerInvocation.invoke(promise, andThen) must beEqualTo(andThen).awaitFor(3.seconds) - resolved mustEqual false - promise must beEqualTo("result").awaitFor(3.seconds) - resolved mustEqual true - } + "invoke eagerly, i.e., return immediately" in { + val probe = new Probe + val outcome = EagerInvocation.invoke(probe.run(), Done) + outcome.isCompleted mustEqual true + probe.resolve() + outcome.isCompleted mustEqual true } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala b/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala deleted file mode 100644 index 7db3b3c6..00000000 --- a/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala +++ /dev/null @@ -1,166 +0,0 @@ -package play.api.cache.redis.impl - -import scala.collection.mutable -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} -import scala.reflect.ClassTag - -import play.api.Environment -import play.api.cache.redis._ - -import org.mockito.InOrder -import org.specs2.matcher.Matchers -import org.specs2.specification.Scope - -object RedisCacheImplicits { - import MockitoImplicits._ - - type Future[T] = scala.concurrent.Future[T] - - def anyClassTag[T: ClassTag] = org.mockito.ArgumentMatchers.any[ClassTag[T]] - - //noinspection UnitMethodIsParameterless - def unit: Unit = () - - def execDone: Done = Done - def beDone = Matchers.beEqualTo(Done) - - def anyVarArgs[T] = org.mockito.ArgumentMatchers.any[T] - - def beEq[T](value: T) = org.mockito.ArgumentMatchers.eq(value) - - def there = MockitoImplicits.there - def one[T <: AnyRef](mock: T)(implicit anOrder: Option[InOrder] = inOrder()) = MockitoImplicits.one(mock) - def two[T <: AnyRef](mock: T)(implicit anOrder: Option[InOrder] = inOrder()) = MockitoImplicits.two(mock) - - val ex = TimeoutException(new IllegalArgumentException("Simulated failure.")) - - object NoSuchElementException extends NoSuchElementException - - trait AbstractMocked extends Scope { - protected val key = "key" - protected val value = "value" - protected val other = "other" - - protected def invocation = LazyInvocation - - protected def policy: RecoveryPolicy = new RecoverWithDefault {} - - protected implicit val runtime: RedisRuntime = mock[RedisRuntime] - runtime.context returns ExecutionContext.global - runtime.invocation returns invocation - runtime.prefix returns RedisEmptyPrefix - runtime.policy returns policy - } - - class MockedConnector extends AbstractMocked { - protected val connector = mock[RedisConnector] - } - - class MockedCache extends MockedConnector { - protected lazy val cache = new RedisCache(connector, Builders.AsynchronousBuilder) - } - - class MockedList extends MockedCache { - protected val data = new mutable.ListBuffer[String] - data.appendAll(Iterable(other, value, value)) - - protected val list = cache.list[String]("key") - } - - class MockedSet extends MockedCache { - protected val data = mutable.Set[String](other, value) - - protected val set = cache.set[String]("key") - } - - class MockedSortedSet extends MockedCache { - protected val scoreValue: (Double, String) = (1.0, "value") - protected val otherScoreValue: (Double, String) = (2.0, "other") - - protected val set = cache.zset[String]("key") - } - - class MockedMap extends MockedCache { - protected val field = "field" - - protected val map = cache.map[String]("key") - } - - class MockedAsyncRedis extends MockedConnector { - protected val cache: AsyncRedis = new AsyncRedisImpl(connector) - } - - class MockedSyncRedis extends MockedConnector { - protected val cache = new SyncRedis(connector) - runtime.timeout returns akka.util.Timeout(1.second) - } - - class MockedJavaRedis extends AbstractMocked { - protected val expiration = 5.seconds - protected val expirationLong = expiration.toSeconds - protected val expirationInt = expirationLong.intValue - protected val classTag = "java.lang.String" - protected val classTagKey = s"classTag::$key" - protected val classTagOther = s"classTag::$other" - - protected implicit val environment: Environment = mock[Environment] - protected val async = mock[AsyncRedis] - protected val cache: play.cache.redis.AsyncCacheApi = new AsyncJavaRedis(async) - - environment.classLoader returns getClass.getClassLoader - } - - class MockedJavaList extends AbstractMocked { - protected val internal = mock[RedisList[String, Future]] - protected val view = mock[internal.RedisListView] - protected val modifier = mock[internal.RedisListModification] - protected val list = new RedisListJavaImpl(internal) - internal.view returns view - internal.modify returns modifier - } - - class MockedJavaSet extends MockedJavaRedis { - protected val internal = mock[RedisSet[String, Future]] - protected val set = new RedisSetJavaImpl(internal) - } - - class MockedJavaMap extends MockedJavaRedis { - val field = "field" - protected val internal = mock[RedisMap[String, Future]] - protected val map = new RedisMapJavaImpl(internal) - } - - trait OrElse extends Scope { - - protected var orElse = 0 - - def doElse[T](value: T): T = { - orElse += 1 - value - } - - def doFuture[T](value: T): Future[T] = { - Future.successful(doElse(value)) - } - - def failedFuture: Future[Nothing] = { - orElse += 1 - Future.failed(failure) - } - - def fail = throw failure - - private def failure = TimeoutException(new IllegalArgumentException("This should no be reached")) - } - - trait Attempts extends Scope { - - protected var attempts = 0 - - def attempt[T](f: => T): T = { - attempts += 1 - f - } - } -} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala index 2a6a0ac7..9c71c56c 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala @@ -1,318 +1,433 @@ package play.api.cache.redis.impl -import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification - -class RedisCacheSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import RedisCacheImplicits._ +import scala.concurrent.Future +import scala.concurrent.duration.Duration - import org.mockito.ArgumentMatchers._ +class RedisCacheSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { + import Helpers._ - val expiration = 1.second + test("get and miss") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- cache.get[String](cacheKey).assertingEqual(None) + } yield Passed + } - "Redis Cache" should { + test("get and hit") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = Some(cacheValue)) + _ <- cache.get[String](cacheKey).assertingEqual(Some(cacheValue)) + } yield Passed + } - "get and miss" in new MockedCache { - connector.get[String](anyString)(anyClassTag) returns None - cache.get[String](key) must beNone.await - } + test("get recover with default") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = failure) + _ <- cache.get[String](cacheKey).assertingEqual(None) + } yield Passed + } - "get and hit" in new MockedCache { - connector.get[String](anyString)(anyClassTag) returns Some(value) - cache.get[String](key) must beSome(value).await - } + test("get all") { (cache, connector) => + for { + _ <- connector.expect.mGet[String](Seq(cacheKey, cacheKey, cacheKey), result = Seq(Some(cacheValue), None, None)) + _ <- cache.getAll[String](cacheKey, cacheKey, cacheKey).assertingEqual(Seq(Some(cacheValue), None, None)) + } yield Passed + } - "get recover with default" in new MockedCache { - connector.get[String](anyString)(anyClassTag) returns ex - cache.get[String](key) must beNone.await - } + test("get all recover with default") { (cache, connector) => + for { + _ <- connector.expect.mGet[String](Seq(cacheKey, cacheKey, cacheKey), result = failure) + _ <- cache.getAll[String](cacheKey, cacheKey, cacheKey).assertingEqual(Seq(None, None, None)) + } yield Passed + } - "get all" in new MockedCache { - connector.mGet[String](anyVarArgs)(anyClassTag) returns Seq(Some(value), None, None) - cache.getAll[String](key, key, key) must beEqualTo(Seq(Some(value), None, None)).await - } + test("get all (keys in a collection)") { (cache, connector) => + for { + _ <- connector.expect.mGet[String](Seq(cacheKey, cacheKey, cacheKey), result = Seq(Some(cacheValue), None, None)) + _ <- cache.getAll[String](Seq(cacheKey, cacheKey, cacheKey)).assertingEqual(Seq(Some(cacheValue), None, None)) + } yield Passed + } - "get all recover with default" in new MockedCache { - connector.mGet[String](anyVarArgs)(anyClassTag) returns ex - cache.getAll[String](key, key, key) must beEqualTo(Seq(None, None, None)).await + test("set") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + _ <- cache.set(cacheKey, cacheValue).assertingDone + } yield Passed } - "get all (keys in a collection)" in new MockedCache { - connector.mGet[String](anyVarArgs)(anyClassTag) returns Seq(Some(value), None, None) - cache.getAll[String](Seq(key, key, key)) must beEqualTo(Seq(Some(value), None, None)).await - } + test("set recover with default") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, result = failure) + _ <- cache.set(cacheKey, cacheValue).assertingDone + } yield Passed + } - "set" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(false)) returns true - cache.set(key, value) must beDone.await - } + test("set if not exists (exists)") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, setIfNotExists=true,result = false) + _ <- cache.setIfNotExists(cacheKey, cacheValue).assertingEqual(false) + } yield Passed + } - "set recover with default" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(false)) returns ex - cache.set(key, value) must beDone.await - } + test("set if not exists (not exists)") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, setIfNotExists = true, result = true) + _ <- cache.setIfNotExists(cacheKey, cacheValue).assertingEqual(true) + } yield Passed + } - "set if not exists (exists)" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(true)) returns false - cache.setIfNotExists(key, value) must beFalse.await - } + test("set if not exists (exists) with expiration") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, cacheExpiration, setIfNotExists = true, result = false) + _ <- cache.setIfNotExists(cacheKey, cacheValue, cacheExpiration).assertingEqual(false) + } yield Passed + } - "set if not exists (not exists)" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(true)) returns true - cache.setIfNotExists(key, value) must beTrue.await - } + test("set if not exists (not exists) with expiration") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, cacheExpiration, setIfNotExists = true, result = true) + _ <- cache.setIfNotExists(cacheKey, cacheValue, cacheExpiration).assertingEqual(true) + } yield Passed + } - "set if not exists (exists) with expiration" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(true)) returns false - connector.expire(anyString, any[Duration]) returns unit - cache.setIfNotExists(key, value, expiration) must beFalse.await - } + test("set if not exists recover with default") { (cache, connector) => + for { + _ <- connector.expect.set(cacheKey, cacheValue, setIfNotExists = true, result = failure) + _ <- cache.setIfNotExists(cacheKey, cacheValue).assertingEqual(true) + } yield Passed + } - "set if not exists (not exists) with expiration" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(true)) returns true - connector.expire(anyString, any[Duration]) returns unit - cache.setIfNotExists(key, value, expiration) must beTrue.await - } + test("set all") { (cache, connector) => + for { + _ <- connector.expect.mSet(Seq(cacheKey -> cacheValue)) + _ <- cache.setAll(cacheKey -> cacheValue).assertingDone + } yield Passed + } - "set if not exists recover with default" in new MockedCache { - connector.set(anyString, anyString, any[Duration], beEq(true)) returns ex - cache.setIfNotExists(key, value) must beTrue.await - } + test("set all recover with default") { (cache, connector) => + for { + _ <- connector.expect.mSet(Seq(cacheKey -> cacheValue), result = failure) + _ <- cache.setAll(cacheKey -> cacheValue).assertingDone + } yield Passed + } - "set all" in new MockedCache { - connector.mSet(anyVarArgs) returns unit - cache.setAll(key -> value) must beDone.await - } + test("set all if not exists (exists)") { (cache, connector) => + for { + _ <- connector.expect.mSetIfNotExist(Seq(cacheKey -> cacheValue), result = false) + _ <- cache.setAllIfNotExist(cacheKey -> cacheValue).assertingEqual(false) + } yield Passed + } - "set all recover with default" in new MockedCache { - connector.mSet(anyVarArgs) returns ex - cache.setAll(key -> value) must beDone.await - } + test("set all if not exists (not exists)") { (cache, connector) => + for { + _ <- connector.expect.mSetIfNotExist(Seq(cacheKey -> cacheValue), result = true) + _ <- cache.setAllIfNotExist(cacheKey -> cacheValue).assertingEqual(true) + } yield Passed + } - "set all if not exists (exists)" in new MockedCache { - connector.mSetIfNotExist(anyVarArgs) returns false - cache.setAllIfNotExist(key -> value) must beFalse.await - } + test("set all if not exists recover with default") { (cache, connector) => + for { + _ <- connector.expect.mSetIfNotExist(Seq(cacheKey -> cacheValue), result = failure) + _ <- cache.setAllIfNotExist(cacheKey -> cacheValue).assertingEqual(true) + } yield Passed + } - "set all if not exists (not exists)" in new MockedCache { - connector.mSetIfNotExist(anyVarArgs) returns true - cache.setAllIfNotExist(key -> value) must beTrue.await - } + test("append") { (cache, connector) => + for { + _ <- connector.expect.append(cacheKey, cacheValue, result = 10L) + _ <- cache.append(cacheKey, cacheValue).assertingDone + } yield Passed + } - "set all if not exists recover with default" in new MockedCache { - connector.mSetIfNotExist(anyVarArgs) returns ex - cache.setAllIfNotExist(key -> value) must beTrue.await - } + test("append with expiration (newly set key)") { (cache, connector) => + for { + _ <- connector.expect.append(cacheKey, cacheValue, result = cacheValue.length.toLong) + _ <- connector.expect.expire(cacheKey, cacheExpiration) + _ <- cache.append(cacheKey, cacheValue, cacheExpiration).assertingDone + } yield Passed + } - "append" in new MockedCache { - connector.append(anyString, anyString) returns 5L - cache.append(key, value) must beDone.await - } + test("append with expiration (already existing key)") { (cache, connector) => + for { + _ <- connector.expect.append(cacheKey, cacheValue, result = cacheValue.length.toLong + 10) + _ <- cache.append(cacheKey, cacheValue, cacheExpiration).assertingDone + } yield Passed + } - "append with expiration" in new MockedCache { - connector.append(anyString, anyString) returns 5L - connector.expire(anyString, any[Duration]) returns unit - cache.append(key, value, expiration) must beDone.await - } + test("append recover with default") { (cache, connector) => + for { + _ <- connector.expect.append(cacheKey, cacheValue, result = failure) + _ <- cache.append(cacheKey, cacheValue).assertingDone + } yield Passed + } - "append recover with default" in new MockedCache { - connector.append(anyString, anyString) returns ex - cache.append(key, value) must beDone.await - } + test("expire") { (cache, connector) => + for { + _ <- connector.expect.expire(cacheKey, cacheExpiration) + _ <- cache.expire(cacheKey, cacheExpiration).assertingDone + } yield Passed + } - "expire" in new MockedCache { - connector.expire(anyString, any[Duration]) returns unit - cache.expire(key, expiration) must beDone.await - } + test("expire recover with default") { (cache, connector) => + for { + _ <- connector.expect.expire(cacheKey, cacheExpiration, result = failure) + _ <- cache.expire(cacheKey, cacheExpiration).assertingDone + } yield Passed + } - "expire recover with default" in new MockedCache { - connector.expire(anyString, any[Duration]) returns ex - cache.expire(key, expiration) must beDone.await - } + test("expires in") { (cache, connector) => + for { + _ <- connector.expect.expiresIn(cacheKey, result = Some(Duration("1500 ms"))) + _ <- cache.expiresIn(cacheKey).assertingEqual(Some(Duration("1500 ms"))) + } yield Passed + } - "expires in" in new MockedCache { - connector.expiresIn(anyString) returns Some(Duration("1500 ms")) - cache.expiresIn(key) must beSome(Duration("1500 ms")).await - } + test("expires in recover with default") { (cache, connector) => + for { + _ <- connector.expect.expiresIn(cacheKey, result = failure) + _ <- cache.expiresIn(cacheKey).assertingEqual(None) + } yield Passed + } - "expires in recover with default" in new MockedCache { - connector.expiresIn(anyString) returns ex - cache.expiresIn(key) must beNone.await - } + test("matching") { (cache, connector) => + for { + _ <- connector.expect.matching("pattern", result = Seq(cacheKey)) + _ <- cache.matching("pattern").assertingEqual(Seq(cacheKey)) + } yield Passed + } - "matching" in new MockedCache { - connector.matching(anyString) returns Seq(key) - cache.matching("pattern") must beEqualTo(Seq(key)).await - } + test("matching recover with default") { (cache, connector) => + for { + _ <- connector.expect.matching("pattern", result = failure) + _ <- cache.matching("pattern").assertingEqual(Seq.empty) + } yield Passed + } - "matching recover with default" in new MockedCache { - connector.matching(anyString) returns ex - cache.matching("pattern") must beEqualTo(Seq.empty).await - } + test("matching with a prefix", prefix = Some("the-prefix")) { (cache, connector) => + for { + _ <- connector.expect.matching(s"the-prefix:pattern", result = Seq(s"the-prefix:$cacheKey")) + _ <- cache.matching("pattern").assertingEqual(Seq(cacheKey)) + } yield Passed + } - "matching with a prefix" in new MockedCache { - // define a non-empty prefix - val prefix = "prefix" - runtime.prefix returns new RedisPrefixImpl(prefix) - connector.matching(beEq(s"$prefix:pattern")) returns Seq(s"$prefix:$key") - cache.matching("pattern") must beEqualTo(Seq(key)).await - } + test("get or else (hit)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = Some(cacheValue)) + orElse = probe.orElse.const(otherValue) + _ <- cache.getOrElse[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 0 + } yield Passed + } - "get or else (hit)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns Some(value) - cache.getOrElse(key)(doElse(value)) must beEqualTo(value).await - orElse mustEqual 0 - } + test("get or else (miss)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + orElse = probe.orElse.const(cacheValue) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + _ <- cache.getOrElse[String](cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (miss)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], beEq(false)) returns true - cache.getOrElse(key)(doElse(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("get or else (failure)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = failure) + orElse = probe.orElse.const(cacheValue) + _ <- cache.getOrElse(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (failure)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns ex - cache.getOrElse(key)(doElse(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("get or else (prefixed,miss)", prefix = Some("the-prefix")) { (cache, connector) => + for { + _ <- connector.expect.get[String](s"the-prefix:$cacheKey", result = None) + _ <- connector.expect.set(s"the-prefix:$cacheKey", cacheValue, result = true) + orElse = probe.orElse.const(cacheValue) + _ <- cache.getOrElse(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (prefixed,miss)" in new MockedSyncRedis with OrElse { - runtime.prefix returns new RedisPrefixImpl("prefix") - connector.get[String](beEq(s"prefix:$key"))(anyClassTag) returns None - connector.set(beEq(s"prefix:$key"), anyString, any[Duration], anyBoolean) returns true - cache.getOrElse(key)(doElse(value)) must beEqualTo(value) - orElse mustEqual 1 - } + test("get or future (hit)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = Some(cacheValue)) + orElse = probe.orElse.async(otherValue) + _ <- cache.getOrFuture(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 0 + } yield Passed + } - "get or future (hit)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns Some(value) - cache.getOrFuture(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 0 - } + test("get or future (miss)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + orElse = probe.orElse.async(cacheValue) + _ <- cache.getOrFuture(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or future (miss)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], beEq(false)) returns true - cache.getOrFuture(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("get or future (failure)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = failure) + orElse = probe.orElse.async(cacheValue) + _ <- cache.getOrFuture(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or future (failure)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns ex - cache.getOrFuture(key)(doFuture(value)) must beEqualTo(value).await - orElse mustEqual 1 - } + test("get or future (failing orElse)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + orElse = probe.orElse.failing(failure) + _ <- cache.getOrFuture[String](cacheKey)(orElse.execute()).assertingFailure[RedisException] + _ = orElse.calls mustEqual 2 + } yield Passed + } - "get or future (failing orElse)" in new MockedCache with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - cache.getOrFuture[String](key)(failedFuture) must throwA[TimeoutException].await - orElse mustEqual 2 - } + test("get or future (rerun)", policy = recoveryPolicy.rerun) { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + orElse = probe.orElse.generic[Future[String]](failure, cacheValue, otherValue) + _ <- cache.getOrFuture(cacheKey)(orElse.execute()).assertingEqual(cacheValue) + _ = orElse.calls mustEqual 2 + } yield Passed + } - "get or future (rerun)" in new MockedCache with OrElse with Attempts { - override protected def policy = new RecoveryPolicy { - def recoverFrom[T](rerun: => Future[T], default: => Future[T], failure: RedisException) = rerun - } - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], beEq(false)) returns true - // run the test - cache.getOrFuture(key) { - attempts match { - case 0 => attempt(failedFuture) - case _ => attempt(doFuture(value)) - } - } must beEqualTo(value).await - // verification - orElse mustEqual 2 - there were two(connector).get[String](anyString)(anyClassTag) - there was one(connector).set(key, value, Duration.Inf, ifNotExists = false) - } + test("remove") { (cache, connector) => + for { + _ <- connector.expect.remove(cacheKey) + _ <- cache.remove(cacheKey).assertingDone + } yield Passed + } - "remove" in new MockedCache { - connector.remove(anyVarArgs) returns unit - cache.remove(key) must beDone.await - } + test("remove recover with default") { (cache, connector) => + for { + _ <- connector.expect.remove(Seq(cacheKey), result = failure) + _ <- cache.remove(cacheKey).assertingDone + } yield Passed + } - "remove recover with default" in new MockedCache { - connector.remove(anyVarArgs) returns ex - cache.remove(key) must beDone.await - } + test("remove multiple") { (cache, connector) => + for { + _ <- connector.expect.remove(cacheKey, cacheKey, cacheKey, cacheKey) + _ <- cache.remove(cacheKey, cacheKey, cacheKey, cacheKey).assertingDone + } yield Passed + } - "remove multiple" in new MockedCache { - connector.remove(anyVarArgs) returns unit - cache.remove(key, key, key, key) must beDone.await - } + test("remove multiple recover with default") { (cache, connector) => + for { + _ <- connector.expect.remove(Seq(cacheKey, cacheKey, cacheKey, cacheKey), result = failure) + _ <- cache.remove(cacheKey, cacheKey, cacheKey, cacheKey).assertingDone + } yield Passed + } - "remove multiple recover with default" in new MockedCache { - connector.remove(anyVarArgs) returns ex - cache.remove(key, key, key, key) must beDone.await - } + test("remove all") { (cache, connector) => + for { + _ <- connector.expect.remove(cacheKey, cacheKey, cacheKey, cacheKey) + _ <- cache.removeAll(Seq(cacheKey, cacheKey, cacheKey, cacheKey): _*).assertingDone + } yield Passed + } - "remove all" in new MockedCache { - connector.remove(anyVarArgs) returns unit - cache.removeAll(Seq(key, key, key, key): _*) must beDone.await - } + test("remove all recover with default") { (cache, connector) => + for { + _ <- connector.expect.remove(Seq(cacheKey, cacheKey, cacheKey, cacheKey), result = failure) + _ <- cache.removeAll(Seq(cacheKey, cacheKey, cacheKey, cacheKey): _*).assertingDone + } yield Passed + } - "remove all recover with default" in new MockedCache { - connector.remove(anyVarArgs) returns ex - cache.removeAll(Seq(key, key, key, key): _*) must beDone.await - } + test("remove matching") { (cache, connector) => + for { + _ <- connector.expect.matching("pattern", result = Seq(cacheKey, cacheKey)) + _ <- connector.expect.remove(cacheKey, cacheKey) + _ <- cache.removeMatching("pattern").assertingDone + } yield Passed + } - "remove matching" in new MockedCache { - connector.matching(beEq("pattern")) returns Seq(key, key) - connector.remove(key, key) returns unit - cache.removeMatching("pattern") must beDone.await - } + test("remove matching recover with default") { (cache, connector) => + for { + _ <- connector.expect.matching("pattern", result = failure) + _ <- cache.removeMatching("pattern").assertingDone + } yield Passed + } - "remove matching recover with default" in new MockedCache { - connector.matching(anyVarArgs) returns ex - cache.removeMatching("pattern") must beDone.await - } + test("invalidate") { (cache, connector) => + for { + _ <- connector.expect.invalidate() + _ <- cache.invalidate().assertingDone + } yield Passed + } - "invalidate" in new MockedCache { - connector.invalidate() returns unit - cache.invalidate() must beDone.await - } + test("invalidate recover with default") { (cache, connector) => + for { + _ <- connector.expect.invalidate(result = failure) + _ <- cache.invalidate().assertingDone + } yield Passed + } - "invalidate recover with default" in new MockedCache { - connector.invalidate() returns ex - cache.invalidate() must beDone.await - } + test("exists") { (cache, connector) => + for { + _ <- connector.expect.exists(cacheKey, result = true) + _ <- cache.exists(cacheKey).assertingEqual(true) + } yield Passed + } - "exists" in new MockedCache { - connector.exists(key) returns true - cache.exists(key) must beTrue.await - } + test("exists recover with default") { (cache, connector) => + for { + _ <- connector.expect.exists(cacheKey, result = failure) + _ <- cache.exists(cacheKey).assertingEqual(false) + } yield Passed + } - "exists recover with default" in new MockedCache { - connector.exists(key) returns ex - cache.exists(key) must beFalse.await - } + test("increment") { (cache, connector) => + for { + _ <- connector.expect.increment(cacheKey, 5L, result = 10L) + _ <- cache.increment(cacheKey, 5L).assertingEqual(10L) + } yield Passed + } - "increment" in new MockedCache { - connector.increment(key, 5L) returns 10L - cache.increment(key, 5L) must beEqualTo(10L).await - } + test("increment recover with default") { (cache, connector) => + for { + _ <- connector.expect.increment(cacheKey, 5L, result = failure) + _ <- cache.increment(cacheKey, 5L).assertingEqual(5L) + } yield Passed + } - "increment recover with default" in new MockedCache { - connector.increment(key, 5L) returns ex - cache.increment(key, 5L) must beEqualTo(5L).await - } + test("decrement") { (cache, connector) => + for { + _ <- connector.expect.increment(cacheKey, -5L, result = 10L) + _ <- cache.decrement(cacheKey, 5L).assertingEqual(10L) + } yield Passed + } - "decrement" in new MockedCache { - connector.increment(key, -5L) returns 10L - cache.decrement(key, 5L) must beEqualTo(10L).await - } + test("decrement recover with default") { (cache, connector) => + for { + _ <- connector.expect.increment(cacheKey, -5L, result = failure) + _ <- cache.decrement(cacheKey, 5L).assertingEqual(-5L) + } yield Passed + } - "decrement recover with default" in new MockedCache { - connector.increment(key, -5L) returns ex - cache.decrement(key, 5L) must beEqualTo(-5L).await + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default, + prefix: Option[String] = None, + )( + f: (RedisCache[AsynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + prefix = prefix.fold[RedisPrefix](RedisEmptyPrefix)(new RedisPrefixImpl(_)), + ) + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val cache: RedisCache[AsynchronousResult] = new RedisCache[AsynchronousResult](connector, Builders.AsynchronousBuilder) + f(cache, connector) } } -} + } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala new file mode 100644 index 00000000..18ae2d44 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala @@ -0,0 +1,458 @@ +package play.api.cache.redis.impl + +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis._ + +import scala.concurrent.Future +import scala.concurrent.duration.Duration +import scala.reflect.ClassTag +import scala.util.{Failure, Try} + +private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => + + protected[impl] trait RedisConnectorMock extends RedisConnector { + + final override def remove(keys: String*): Future[Unit] = + removeValues(keys) + + def removeValues(keys: Seq[String]): Future[Unit] + + override final def mGet[T: ClassTag](keys: String*): Future[Seq[Option[T]]] = + mGetKeys[T](keys) + + def mGetKeys[T: ClassTag](keys: Seq[String]): Future[Seq[Option[T]]] + + final override def mSet(keyValues: (String, Any)*): Future[Unit] = + mSetValues(keyValues) + + def mSetValues(keyValues: Seq[(String, Any)]): Future[Unit] + + final override def mSetIfNotExist(keyValues: (String, Any)*): Future[Boolean] = + mSetIfNotExistValues(keyValues) + + def mSetIfNotExistValues(keyValues: Seq[(String, Any)]): Future[Boolean] + + final override def listPrepend(key: String, value: Any*): Future[Long] = + listPrependValues(key, value) + + def listPrependValues(key: String, values: Seq[Any]): Future[Long] + + final override def listAppend(key: String, value: Any*): Future[Long] = + listAppendValues(key, value) + + def listAppendValues(key: String, values: Seq[Any]): Future[Long] + + final override def setAdd(key: String, value: Any*): Future[Long] = + setAddValues(key, value) + + def setAddValues(key: String, values: Seq[Any]): Future[Long] + + final override def setRemove(key: String, value: Any*): Future[Long] = + setRemoveValues(key, value) + + def setRemoveValues(key: String, values: Seq[Any]): Future[Long] + + final override def sortedSetAdd(key: String, scoreValues: (Double, Any)*): Future[Long] = + sortedSetAddValues(key, scoreValues) + + def sortedSetAddValues(key: String, values: Seq[(Double, Any)]): Future[Long] + + final override def sortedSetRemove(key: String, value: Any*): Future[Long] = + sortedSetRemoveValues(key, value) + + def sortedSetRemoveValues(key: String, values: Seq[Any]): Future[Long] + + final override def hashGet[T: ClassTag](key: String, field: String): Future[Option[T]] = + hashGetField[T](key, field) + + def hashGetField[T: ClassTag](key: String, field: String): Future[Option[T]] + + final override def hashGet[T: ClassTag](key: String, fields: Seq[String]): Future[Seq[Option[T]]] = + hashGetFields[T](key, fields) + + def hashGetFields[T: ClassTag](key: String, fields: Seq[String]): Future[Seq[Option[T]]] + + final override def hashRemove(key: String, field: String*): Future[Long] = + hashRemoveValues(key, field) + + def hashRemoveValues(key: String, fields: Seq[String]): Future[Long] + + final override def hashGetAll[T: ClassTag](key: String): Future[Map[String, T]] = + hashGetAllValues[T](key) + + def hashGetAllValues[T: ClassTag](key: String): Future[Map[String, T]] + } + + final protected implicit class RedisConnectorExpectationOps(connector: RedisConnectorMock) { + def expect: RedisConnectorExpectation = + new RedisConnectorExpectation(connector) + } + + protected final class RedisConnectorExpectation(connector: RedisConnectorMock) { + + def get[T: ClassTag](key: String, result: Try[Option[T]]): Future[Unit] = + Future.successful { + (connector.get(_: String)(_: ClassTag[_])) + .expects(key, implicitly[ClassTag[T]]) + .returning(Future.fromTry(result)) + .once() + } + + def get[T: ClassTag](key: String, result: Option[T]): Future[Unit] = + get(key, Try(result)) + + def get[T: ClassTag](key: String, result: Throwable): Future[Unit] = + get[T](key, Failure(result)) + + def mGet[T: ClassTag](keys: Seq[String], result: Future[Seq[Option[T]]]): Future[Unit] = + Future.successful { + (connector.mGetKeys(_: Seq[String])(_: ClassTag[_])) + .expects(keys, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def set[T](key: String, value: T, duration: Duration = Duration.Inf, setIfNotExists: Boolean = false, result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.set(_: String, _: Any, _: Duration, _: Boolean)) + .expects(key, if (value == null) * else value, duration, setIfNotExists) + .returning(result) + .once() + } + + def mSet(keyValues: Seq[(String, Any)], result: Future[Unit]=Future.unit): Future[Unit] = + Future.successful { + (connector.mSetValues(_: Seq[(String, Any)])) + .expects(keyValues) + .returning(result) + .once() + } + + def mSetIfNotExist(keyValues: Seq[(String, Any)], result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.mSetIfNotExistValues(_: Seq[(String, Any)])) + .expects(keyValues) + .returning(result) + .once() + } + + def expire(key: String, duration: Duration, result: Future[Unit] = Future.unit): Future[Unit] = + Future.successful { + (connector.expire(_: String, _: Duration)) + .expects(key, duration) + .returning(result) + .once() + } + + def expiresIn(key: String, result: Future[Option[Duration]]): Future[Unit] = + Future.successful { + (connector.expiresIn(_: String)) + .expects(key) + .returning(result) + .once() + } + + def remove(keys: String*): Future[Unit] = + remove(keys, Future.unit) + + def remove(keys: Seq[String], result: Future[Unit]): Future[Unit] = + Future.successful { + (connector.removeValues(_: Seq[String])) + .expects(keys) + .returning(result) + .once() + } + + def invalidate(result: Future[Unit] = Future.unit): Future[Unit] = + Future.successful { + (() => connector.invalidate()) + .expects() + .returning(result) + .once() + } + + def exists(key: String, result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.exists(_: String)) + .expects(key) + .returning(result) + .once() + } + + def increment(key: String, by: Long, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.increment(_: String, _: Long)) + .expects(key, by) + .returning(result) + .once() + } + + def append(key: String, value: String, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.append(_: String, _: String)) + .expects(key, value) + .returning(result) + .once() + } + + def matching(pattern: String, result: Future[Seq[String]]): Future[Unit] = + Future.successful { + (connector.matching(_: String)) + .expects(pattern) + .returning(result) + .once() + } + + def listPrepend(key: String, values: Seq[String], result: Future[Long]= Future.successful(5L)): Future[Unit] = + Future.successful { + (connector.listPrependValues(_: String, _: Seq[Any])) + .expects(key, values) + .returning(result) + .once() + } + + def listAppend[T:ClassTag](key: String, values: Seq[T], result: Future[Long] = Future.successful(5L)): Future[Unit] = + Future.successful { + (connector.listAppendValues(_: String, _: Seq[Any])) + .expects(key, values) + .returning(result) + .once() + } + + def listSlice[T: ClassTag](key: String, start: Int, end: Int, result: Future[Seq[T]]): Future[Unit] = + Future.successful { + (connector.listSlice(_: String, _: Int, _: Int)(_: ClassTag[_])) + .expects(key, start, end, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def listHeadPop[T:ClassTag](key: String, result: Future[Option[T]]): Future[Unit] = + Future.successful { + (connector.listHeadPop(_: String)(_: ClassTag[_])) + .expects(key, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def listSize(key: String, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.listSize(_: String)) + .expects(key) + .returning(result) + .once() + } + + def listInsert(key: String, pivot: String, value: String, result: Future[Option[Long]]): Future[Unit] = + Future.successful { + (connector.listInsert(_: String, _: String, _: Any)) + .expects(key, pivot, value) + .returning(result) + .once() + } + + def listSetAt(key: String, index: Int, value: String, result: Future[Unit]): Future[Unit] = + Future.successful { + (connector.listSetAt(_: String, _: Int, _: Any)) + .expects(key, index, value) + .returning(result) + .once() + } + + def listRemove(key: String, value: String, count: Int, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.listRemove(_: String, _:Any, _: Int)) + .expects(key, value, count) + .returning(result) + .once() + } + + def listRemoveAt(key: String, index: Int, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.listSetAt(_: String, _: Int, _: Any)) + .expects(key, index, "play-redis:DELETED") + .returning(Future.unit) + .once() + (connector.listRemove(_: String, _: Any, _: Int)) + .expects(key, "play-redis:DELETED", 0) + .returning(result) + .once() + } + + def listTrim(key: String, start: Int, end: Int, result: Future[Unit] = Future.unit): Future[Unit] = + Future.successful { + (connector.listTrim(_: String, _: Int, _: Int)) + .expects(key, start, end) + .returning(result) + .once() + } + + def setAdd(key: String, values: Seq[String], result: Future[Long] = Future.successful(5L)): Future[Unit] = + Future.successful { + (connector.setAddValues(_: String, _: Seq[Any])) + .expects(key, values) + .returning(result) + .once() + } + + def setIsMember(key: String, value: String, result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.setIsMember(_: String, _: Any)) + .expects(key, value) + .returning(result) + .once() + } + + def setRemove(key: String, values: Seq[String], result: Future[Long] = Future.successful(1L)): Future[Unit] = + Future.successful { + (connector.setRemoveValues(_: String, _: Seq[Any])) + .expects(key, values) + .returning(result) + .once() + } + + def setMembers[T: ClassTag](key: String, result: Future[Set[Any]]): Future[Unit] = + Future.successful { + (connector.setMembers(_: String)(_: ClassTag[_])) + .expects(key, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def setSize(key: String, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.setSize(_: String)) + .expects(key) + .returning(result) + .once() + } + + def sortedSetAdd(key: String, values: Seq[(Double, String)], result: Future[Long] = Future.successful(1L)): Future[Unit] = + Future.successful { + (connector.sortedSetAddValues(_: String, _: Seq[(Double, Any)])) + .expects(key, values) + .returning(result) + .once() + } + + def sortedSetScore(key: String, value: String, result: Future[Option[Double]]): Future[Unit] = + Future.successful { + (connector.sortedSetScore(_: String, _: Any)) + .expects(key, value) + .returning(result) + .once() + } + + def sortedSetRemove(key: String, values: Seq[String], result: Future[Long] = Future.successful(1L)): Future[Unit] = + Future.successful { + (connector.sortedSetRemoveValues(_: String, _: Seq[Any])) + .expects(key, values) + .returning(result) + .once() + } + + def sortedSetRange[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[String]]): Future[Unit] = + Future.successful { + (connector.sortedSetRange(_: String, _: Long, _: Long)(_: ClassTag[_])) + .expects(key, start, end, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def sortedSetReverseRange[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[String]]): Future[Unit] = + Future.successful { + (connector.sortedSetReverseRange(_: String, _: Long, _: Long)(_: ClassTag[_])) + .expects(key, start, end, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def sortedSetSize(key: String, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.sortedSetSize(_: String)) + .expects(key) + .returning(result) + .once() + } + + def hashSet(key: String, field: String, value: String, result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.hashSet(_: String, _: String, _: Any)) + .expects(key, field, value) + .returning(result) + .once() + } + + def hashGet[T: ClassTag](key: String, field: String, result: Future[Option[T]]): Future[Unit] = + Future.successful { + (connector.hashGetField(_: String, _: String)(_: ClassTag[_])) + .expects(key, field, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def hashGet[T: ClassTag](key: String, fields: Seq[String], result: Future[Seq[Option[T]]]): Future[Unit] = + Future.successful { + (connector.hashGetFields(_: String, _: Seq[String])(_: ClassTag[_])) + .expects(key, fields, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def hashExists(key: String, field: String, result: Future[Boolean]): Future[Unit] = + Future.successful { + (connector.hashExists(_: String, _: String)) + .expects(key, field) + .returning(result) + .once() + } + + def hashRemove(key: String, fields: Seq[String], result: Future[Long] = Future.successful(1L)): Future[Unit] = + Future.successful { + (connector.hashRemoveValues(_: String, _: Seq[String])) + .expects(key, fields) + .returning(result) + .once() + } + + def hashIncrement(key: String, field: String, by: Long, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.hashIncrement(_: String, _: String, _: Long)) + .expects(key, field, by) + .returning(result) + .once() + } + + def hashGetAll[T: ClassTag](key: String, result: Future[Map[String, T]]): Future[Unit] = + Future.successful { + (connector.hashGetAllValues(_: String)(_: ClassTag[_])) + .expects(key, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def hashKeys(key: String, result: Future[Set[String]]): Future[Unit] = + Future.successful { + (connector.hashKeys(_: String)) + .expects(key) + .returning(result) + .once() + } + + def hashValues[T: ClassTag](key: String, result: Future[Set[T]]): Future[Unit] = + Future.successful { + (connector.hashValues[T](_: String)(_: ClassTag[T])) + .expects(key, implicitly[ClassTag[T]]) + .returning(result) + .once() + } + + def hashSize(key: String, result: Future[Long]): Future[Unit] = + Future.successful { + (connector.hashSize(_: String)) + .expects(key) + .returning(result) + .once() + } + } +} \ No newline at end of file diff --git a/src/test/scala/play/api/cache/redis/impl/RedisJavaListSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisJavaListSpec.scala index d4b9b776..1ad2cb89 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisJavaListSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisJavaListSpec.scala @@ -1,193 +1,224 @@ package play.api.cache.redis.impl import play.api.cache.redis._ +import play.api.cache.redis.test._ +import play.cache.redis.AsyncRedisList -import org.mockito.ArgumentMatchers -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future +import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters._ -class RedisJavaListSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import JavaCompatibility._ - import RedisCacheImplicits._ +class RedisJavaListSpec extends AsyncUnitSpec with RedisListJavaMock with RedisRuntimeMock { - import ArgumentMatchers._ - - "Redis List" should { - - "prepend" in new MockedJavaList { - internal.prepend(value) returns internal - list.prepend(value).asScala must beEqualTo(list).await - there were one(internal).prepend(value) - } + test("prepend") { (list, internal) => + for { + _ <- internal.expect.prepend(cacheValue) + _ <- list.prepend(cacheValue).assertingEqual(list) + } yield Passed + } - "append" in new MockedJavaList { - internal.append(value) returns internal - list.append(value).asScala must beEqualTo(list).await - there were one(internal).append(value) - } + test("append") { (list, internal) => + for { + _ <- internal.expect.append(cacheValue) + _ <- list.append(cacheValue).assertingEqual(list) + } yield Passed + } - "apply (hit)" in new MockedJavaList { - internal.apply(beEq(5)) returns value - list.apply(5).asScala must beEqualTo(value).await - there were one(internal).apply(5) - } + test("apply (hit)") { (list, internal) => + for { + _ <- internal.expect.apply(5, Some(cacheValue)) + _ <- list.apply(5).assertingEqual(cacheValue) + } yield Passed + } - "apply (miss or fail)" in new MockedJavaList { - internal.apply(beEq(5)) returns NoSuchElementException - list.apply(5).asScala must throwA[NoSuchElementException].await - there were one(internal).apply(5) - } + test("apply (miss or fail)") { (list, internal) => + for { + _ <- internal.expect.apply(5, None) + _ <- list.apply(5).assertingFailure[NoSuchElementException] + } yield Passed + } - "get (miss)" in new MockedJavaList { - internal.get(beEq(5)) returns None - list.get(5).asScala must beEqualTo(None.asJava).await - there were one(internal).get(5) - } + test("get (miss)") { (list, internal) => + for { + _ <- internal.expect.get(5, None) + _ <- list.get(5).assertingEqual(None.toJava) + } yield Passed + } - "get (hit)" in new MockedJavaList { - internal.get(beEq(5)) returns Some(value) - list.get(5).asScala must beEqualTo(Some(value).asJava).await - there were one(internal).get(5) - } + test("get (hit)") { (list, internal) => + for { + _ <- internal.expect.get(5, Some(cacheValue)) + _ <- list.get(5).assertingEqual(Some(cacheValue).toJava) + } yield Passed + } - "head (non-empty)" in new MockedJavaList { - internal.apply(beEq(0)) returns value - list.head.asScala must beEqualTo(value).await - there were one(internal).apply(0) - } + test("head (non-empty)") { (list, internal) => + for { + _ <- internal.expect.apply(0, Some(cacheValue)) + _ <- list.head.assertingEqual(cacheValue) + } yield Passed + } - "head (empty)" in new MockedJavaList { - internal.apply(beEq(0)) returns NoSuchElementException - list.head.asScala must throwA(NoSuchElementException).await - there were one(internal).apply(0) - } + test("head (empty)") { (list, internal) => + for { + _ <- internal.expect.apply(0, None) + _ <- list.head.assertingFailure[NoSuchElementException] + } yield Passed + } - "headOption (non-empty)" in new MockedJavaList { - internal.get(beEq(0)) returns Some(value) - list.headOption.asScala must beEqualTo(Some(value).asJava).await - there were one(internal).get(0) - } + test("headOption (non-empty)") { (list, internal) => + for { + _ <- internal.expect.get(0, Some(cacheValue)) + _ <- list.headOption().assertingEqual(Some(cacheValue).toJava) + } yield Passed + } - "headOption (empty)" in new MockedJavaList { - internal.get(beEq(0)) returns None - list.headOption.asScala must beEqualTo(None.asJava).await - there were one(internal).get(0) - } + test("headOption (empty)") { (list, internal) => + for { + _ <- internal.expect.get(0, None) + _ <- list.headOption().assertingEqual(None.toJava) + } yield Passed + } - "head pop" in new MockedJavaList { - internal.headPop returns None - list.headPop().asScala must beEqualTo(None.asJava).await - there were one(internal).headPop - } + test("head pop") { (list, internal) => + for { + _ <- internal.expect.headPop(None) + _ <- list.headPop().assertingEqual(None.toJava) + } yield Passed + } - "last (non-empty)" in new MockedJavaList { - internal.apply(beEq(-1)) returns value - list.last.asScala must beEqualTo(value).await - there were one(internal).apply(-1) - } + test("last (non-empty)") { (list, internal) => + for { + _ <- internal.expect.apply(-1, Some(cacheValue)) + _ <- list.last().assertingEqual(cacheValue) + } yield Passed + } - "last (empty)" in new MockedJavaList { - internal.apply(beEq(-1)) returns NoSuchElementException - list.last.asScala must throwA(NoSuchElementException).await - there were one(internal).apply(-1) - } + test("last (empty)") { (list, internal) => + for { + _ <- internal.expect.apply(-1, None) + _ <- list.last().assertingFailure[NoSuchElementException] + } yield Passed + } - "lastOption (non-empty)" in new MockedJavaList { - internal.get(beEq(-1)) returns Some(value) - list.lastOption.asScala must beEqualTo(Some(value).asJava).await - there were one(internal).get(-1) - } + test("lastOption (non-empty)") { (list, internal) => + for { + _ <- internal.expect.get(-1, Some(cacheValue)) + _ <- list.lastOption().assertingEqual(Some(cacheValue).toJava) + } yield Passed + } - "lastOption (empty)" in new MockedJavaList { - internal.get(beEq(-1)) returns None - list.lastOption.asScala must beEqualTo(None.asJava).await - there were one(internal).get(-1) - } + test("lastOption (empty)") { (list, internal) => + for { + _ <- internal.expect.get(-1, None) + _ <- list.lastOption().assertingEqual(None.toJava) + } yield Passed + } - "toList" in new MockedJavaList { - view.slice(beEq(0), beEq(-1)) returns List.empty[String] - list.toList.asScala must beEqualTo(List.empty.asJava).await - there were one(internal).view - there were one(view).slice(0, -1) - } + test("toList") { (list, internal) => + for { + _ <- internal.expect.view.slice(0, -1, List.empty) + _ <- list.toList.assertingEqual(List.empty.asJava) + } yield Passed + } - "insert before" in new MockedJavaList { - internal.insertBefore("pivot", value) returns Some(5L) - list.insertBefore("pivot", value).asScala must beEqualTo(Some(5L).asJava).await - there were one(internal).insertBefore("pivot", value) - } + test("insert before") { (list, internal) => + for { + _ <- internal.expect.insertBefore("pivot", cacheValue, Some(5L)) + _ <- list.insertBefore("pivot", cacheValue).assertingEqual(Option(5L).map(long2Long).toJava) + } yield Passed + } - "set at position" in new MockedJavaList { - internal.set(beEq(2), beEq(value)) returns internal - list.set(2, value).asScala must beEqualTo(list).await - there were one(internal).set(2, value) - } + test("set at position") { (list, internal) => + for { + _ <- internal.expect.set(2, cacheValue) + _ <- list.set(2, cacheValue).assertingEqual(list) + } yield Passed + } - "remove element" in new MockedJavaList { - internal.remove(beEq(value), anyInt) returns internal - list.remove(value).asScala must beEqualTo(list).await - there were one(internal).remove(value) - } + test("remove element") { (list, internal) => + for { + _ <- internal.expect.remove(cacheValue) + _ <- list.remove(cacheValue).assertingEqual(list) + } yield Passed + } - "remove with count" in new MockedJavaList { - internal.remove(beEq(value), beEq(2)) returns internal - list.remove(value, 2).asScala must beEqualTo(list).await - there were one(internal).remove(value, 2) - } + test("remove with count") { (list, internal) => + for { + _ <- internal.expect.remove(cacheValue, 2) + _ <- list.remove(cacheValue, 2).assertingEqual(list) + } yield Passed + } - "remove at position" in new MockedJavaList { - internal.removeAt(beEq(2)) returns internal - list.removeAt(2).asScala must beEqualTo(list).await - there were one(internal).removeAt(2) - } + test("remove at position") { (list, internal) => + for { + _ <- internal.expect.removeAt(2) + _ <- list.removeAt(2).assertingEqual(list) + } yield Passed + } - "view all" in new MockedJavaList { - view.slice(beEq(0), beEq(-1)) returns List.empty[String] - list.view().all().asScala must beEqualTo(List.empty.asJava).await - there were one(internal).view - there were one(view).slice(0, -1) - } + test("view all") { (list, internal) => + for { + _ <- internal.expect.view.slice(0, -1, List.empty) + _ <- list.view().all().assertingEqual(List.empty.asJava) + } yield Passed + } - "view take" in new MockedJavaList { - view.slice(beEq(0), beEq(1)) returns List.empty[String] - list.view().take(2).asScala must beEqualTo(List.empty.asJava).await - there were one(internal).view - there were one(view).slice(0, 1) - } + test("view take") { (list, internal) => + for { + _ <- internal.expect.view.slice(0, 1, List.empty) + _ <- list.view().take(2).assertingEqual(List.empty.asJava) + } yield Passed + } - "view drop" in new MockedJavaList { - view.slice(beEq(2), beEq(-1)) returns List.empty[String] - list.view().drop(2).asScala must beEqualTo(List.empty.asJava).await - there were one(internal).view - there were one(view).slice(2, -1) - } + test("view drop") { (list, internal) => + for { + _ <- internal.expect.view.slice(2, -1, List.empty) + _ <- list.view().drop(2).assertingEqual(List.empty.asJava) + } yield Passed + } - "view slice" in new MockedJavaList { - view.slice(beEq(1), beEq(2)) returns List.empty[String] - list.view().slice(1, 2).asScala must beEqualTo(List.empty.asJava).await - there were one(internal).view - there were one(view).slice(1, 2) - } + test("view slice") { (list, internal) => + for { + _ <- internal.expect.view.slice(1, 2, List.empty) + _ <- list.view().slice(1, 2).assertingEqual(List.empty.asJava) + } yield Passed + } - "modify collection" in new MockedJavaList { - list.modify().collection() mustEqual list - } + test("modify clear") { (list, internal) => + for { + _ <- internal.expect.modify.clear() + _ <- list.modify().clear().assertingEqual(list.modify()) + } yield Passed + } - "modify clear" in new MockedJavaList { - private val javaModifier = list.modify() - modifier.clear() returns modifier - javaModifier.clear().asScala must beEqualTo(javaModifier).await - there were one(internal).modify - there were one(modifier).clear() - } + test("modify slice") { (list, internal) => + for { + _ <- internal.expect.modify.slice(1, 2) + _ <- list.modify().slice(1, 2).assertingEqual(list.modify()) + } yield Passed + } - "modify slice" in new MockedJavaList { - private val javaModifier = list.modify() - modifier.slice(beEq(1), beEq(2)) returns modifier - javaModifier.slice(1, 2).asScala must beEqualTo(javaModifier).await - there were one(internal).modify - there were one(modifier).slice(1, 2) + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (AsyncRedisList[String], RedisListMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + val internal: RedisListMock = mock[RedisListMock] + val view: internal.RedisListView = mock[internal.RedisListView] + val modifier: internal.RedisListModification = mock[internal.RedisListModification] + val list: AsyncRedisList[String] = new RedisListJavaImpl(internal) + + (() => internal.view).expects().returns(view).anyNumberOfTimes() + (() => internal.modify).expects().returns(modifier).anyNumberOfTimes() + + f(list, internal) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisJavaMapSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisJavaMapSpec.scala index 77333d9c..c860e31e 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisJavaMapSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisJavaMapSpec.scala @@ -1,71 +1,93 @@ package play.api.cache.redis.impl import play.api.cache.redis._ +import play.api.cache.redis.test._ +import play.cache.redis.AsyncRedisMap -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future +import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters._ -class RedisJavaMapSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import JavaCompatibility._ - import RedisCacheImplicits._ +class RedisJavaMapSpec extends AsyncUnitSpec with RedisMapJavaMock with RedisRuntimeMock { - import org.mockito.ArgumentMatchers._ - - "Redis Map" should { + test("set") { (cache, internal) => + for { + _ <- internal.expect.add(cacheKey, cacheValue) + _ <- cache.add(cacheKey, cacheValue).assertingEqual(cache) + } yield Passed + } - "set" in new MockedJavaMap { - internal.add(beEq(field), beEq(value)) returns internal - map.add(field, value).asScala must beEqualTo(map).await - there were one(internal).add(field, value) - } + test("get") { (cache, internal) => + for { + _ <- internal.expect.get(cacheKey, Some(cacheValue)) + _ <- cache.get(cacheKey).assertingEqual(Option(cacheValue).toJava) + } yield Passed + } - "get" in new MockedJavaMap { - internal.get(beEq(field)) returns Some(value) - map.get(field).asScala must beEqualTo(Some(value).asJava).await - there were one(internal).get(field) - } + test("contains") { (cache, internal) => + for { + _ <- internal.expect.contains(cacheKey, result = true) + _ <- cache.contains(cacheKey).assertingEqual(true) + } yield Passed + } - "contains" in new MockedJavaMap { - internal.contains(beEq(field)) returns true - map.contains(field).asScala.map(Boolean.unbox) must beEqualTo(true).await - there were one(internal).contains(field) - } + test("remove") { (cache, internal) => + for { + _ <- internal.expect.remove(cacheKey, otherKey) + _ <- cache.remove(cacheKey, otherKey).assertingEqual(cache) + } yield Passed + } - "remove" in new MockedJavaMap { - internal.remove(anyVarArgs[String]) returns internal - map.remove(field, other).asScala must beEqualTo(map).await - there were one(internal).remove(field, other) - } + test("increment") { (cache, internal) => + for { + _ <- internal.expect.increment(cacheKey, by = 1L, result = 4L) + _ <- cache.increment(cacheKey).assertingEqual(4L) + } yield Passed + } - "increment" in new MockedJavaMap { - internal.increment(beEq(field), anyLong()) returns 4L - map.increment(field).asScala.map(Long.unbox) must beEqualTo(4L).await - there were one(internal).increment(field) - } + test("increment by") { (cache, internal) => + for { + _ <- internal.expect.increment(cacheKey, by = 2L, result = 6L) + _ <- cache.increment(cacheKey, 2L).assertingEqual(6L) + } yield Passed + } - "increment by" in new MockedJavaMap { - internal.increment(beEq(field), beEq(2L)) returns 6L - map.increment(field, 2L).asScala.map(Long.unbox) must beEqualTo(6L).await - there were one(internal).increment(field, 2L) - } + test("toMap") { (cache, internal) => + for { + _ <- internal.expect.toMap(cacheKey -> cacheValue) + _ <- cache.toMap.assertingEqual(Map(cacheKey -> cacheValue).asJava) + } yield Passed + } - "toMap" in new MockedJavaMap { - internal.toMap returns Map(key -> value) - map.toMap().asScala must beEqualTo(Map(key -> value).asJava).await - there were one(internal).toMap - } + test("keySet") { (cache, internal) => + for { + _ <- internal.expect.keySet(cacheKey, otherKey) + _ <- cache.keySet().assertingEqual(Set(cacheKey, otherKey).asJava) + } yield Passed + } - "keySet" in new MockedJavaMap { - internal.keySet returns Set(key, other) - map.keySet().asScala must beEqualTo(Set(key, other).asJava).await - there were one(internal).keySet - } + test("values") { (cache, internal) => + for { + _ <- internal.expect.values(otherValue, cacheValue) + _ <- cache.values().assertingEqual(Set(otherValue, cacheValue).asJava) + } yield Passed + } - "values" in new MockedJavaMap { - internal.values returns Set(value, other) - map.values().asScala must beEqualTo(Set(value, other).asJava).await - there were one(internal).values + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (AsyncRedisMap[String], RedisMapMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + val internal: RedisMapMock = mock[RedisMapMock] + val map: AsyncRedisMap[String] = new RedisMapJavaImpl(internal) + + f(map, internal) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisJavaSetSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisJavaSetSpec.scala index a2c56f33..bc836b44 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisJavaSetSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisJavaSetSpec.scala @@ -1,39 +1,59 @@ package play.api.cache.redis.impl import play.api.cache.redis._ +import play.api.cache.redis.test._ +import play.cache.redis.{AsyncRedisList, AsyncRedisSet} -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future +import scala.jdk.CollectionConverters._ +import scala.jdk.OptionConverters._ -class RedisJavaSetSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import JavaCompatibility._ - import RedisCacheImplicits._ +class RedisJavaSetSpec extends AsyncUnitSpec with RedisSetJavaMock with RedisRuntimeMock { - "Redis Set" should { + test("add") { (set, internal) => + for { + _ <- internal.expect.add(cacheKey, cacheValue) + _ <- set.add(cacheKey, cacheValue).assertingEqual(set) + } yield Passed + } - "add" in new MockedJavaSet { - internal.add(anyVarArgs[String]) returns internal - set.add(key, value).asScala must beEqualTo(set).await - there were one(internal).add(key, value) - } - "contains" in new MockedJavaSet { - internal.contains(beEq(key)) returns true - set.contains(key).asScala.map(Boolean.unbox) must beTrue.await - there were one(internal).contains(key) - } + test("contains") { (set, internal) => + for { + _ <- internal.expect.contains(cacheKey, result = true) + _ <- set.contains(cacheKey).assertingEqual(true) + } yield Passed + } - "remove" in new MockedJavaSet { - internal.remove(anyVarArgs[String]) returns internal - set.remove(key, value).asScala must beEqualTo(set).await - there were one(internal).remove(key, value) - } + test("remove") { (set, internal) => + for { + _ <- internal.expect.remove(cacheKey, cacheValue) + _ <- set.remove(cacheKey, cacheValue).assertingEqual(set) + } yield Passed + } + + test("toSet") { (set, internal) => + for { + _ <- internal.expect.toSet(cacheKey, cacheValue) + _ <- set.toSet.assertingEqual(Set(cacheKey, cacheValue).asJava) + } yield Passed + } - "toSet" in new MockedJavaSet { - internal.toSet returns Set(key, value) - set.toSet.asScala must beEqualTo(Set(key, value).asJava).await - there were one(internal).toSet + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (AsyncRedisSet[String], RedisSetMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + val internal: RedisSetMock = mock[RedisSetMock] + val set: AsyncRedisSet[String] = new RedisSetJavaImpl(internal) + + f(set, internal) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisListJavaMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisListJavaMock.scala new file mode 100644 index 00000000..55de8dc6 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisListJavaMock.scala @@ -0,0 +1,131 @@ +package play.api.cache.redis.impl + +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis._ + +import scala.concurrent.Future + +private[impl] trait RedisListJavaMock { this: AsyncMockFactoryBase => + + protected[impl] trait RedisListMock extends RedisList[String, Future] + + final protected implicit class RedisListOps(list: RedisListMock) { + def expect: RedisListExpectation = + new RedisListExpectation(list) + } + + protected final class RedisListExpectation(list: RedisListMock) { + + def apply(index: Int, value: Option[String]): Future[Unit] = + Future.successful { + (list.apply(_: Int)) + .expects(index) + .returning( + value.fold[Future[String]]( + Future.failed(new NoSuchElementException()) + )( + Future.successful + ) + ) + .once() + } + + def get(index: Int, value: Option[String]): Future[Unit] = + Future.successful { + (list.get(_: Int)) + .expects(index) + .returning(Future.successful(value)) + .once() + } + + def prepend(value: String): Future[Unit] = + Future.successful { + (list.prepend(_: String)) + .expects(value) + .returning(Future.successful(list)) + .once() + } + + def append(value: String): Future[Unit] = + Future.successful { + (list.append(_: String)) + .expects(value) + .returning(Future.successful(list)) + .once() + } + + def headPop(value: Option[String]): Future[Unit] = + Future.successful { + (() => list.headPop) + .expects() + .returning(Future.successful(value)) + .once() + } + + def insertBefore(pivot: String, value: String, newSize: Option[Long]): Future[Unit] = + Future.successful { + (list.insertBefore(_: String, _: String)) + .expects(pivot, value) + .returning(Future.successful(newSize)) + .once() + } + + def set(index: Int, value: String): Future[Unit] = + Future.successful { + (list.set(_: Int, _: String)) + .expects(index, value) + .returning(Future.successful(list)) + .once() + } + + def remove(value: String, count: Int = 1): Future[Unit] = + Future.successful { + (list.remove(_: String, _: Int)) + .expects(value, count) + .returning(Future.successful(list)) + .once() + } + + def removeAt(index: Int): Future[Unit] = + Future.successful { + (list.removeAt(_: Int)) + .expects(index) + .returning(Future.successful(list)) + .once() + } + + def view: RedisListViewExpectation = new RedisListViewExpectation(list) + + def modify: RedisListModificationExpectation = new RedisListModificationExpectation(list) + } + + protected final class RedisListViewExpectation(list: RedisListMock) { + + def slice(from: Int, to: Int, value: List[String]): Future[Unit] = + Future.successful { + (list.view.slice(_: Int, _: Int)) + .expects(from, to) + .returning(Future.successful(value)) + .once() + } + } + + protected final class RedisListModificationExpectation(list: RedisListMock) { + + def clear(): Future[Unit] = + Future.successful { + (() => list.modify.clear()) + .expects() + .returning(Future.successful(list.modify)) + .once() + } + + def slice(from: Int, to: Int): Future[Unit] = + Future.successful { + (list.modify.slice(_: Int, _: Int)) + .expects(from, to) + .returning(Future.successful(list.modify)) + .once() + } + } +} \ No newline at end of file diff --git a/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala index 5f02a3ae..8e6b4182 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala @@ -1,311 +1,354 @@ package play.api.cache.redis.impl -import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.impl.Builders.AsynchronousBuilder +import play.api.cache.redis.test._ + +import scala.concurrent.Future + +class RedisListSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { + + test("prepend (all variants)") { (list, connector) => + for { + _ <- connector.expect.listPrepend(otherKey, Seq(cacheValue)) + _ <- connector.expect.listPrepend(otherKey, Seq(cacheValue)) + _ <- connector.expect.listPrepend(otherKey, Seq(cacheValue, otherValue)) + _ <- list.prepend(cacheValue).assertingEqual(list) + _ <- (List(cacheValue, otherValue) ++: list).assertingEqual(list) + _ <- (cacheValue +: list).assertingEqual(list) + } yield Passed + } -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification - -class RedisListSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import RedisCacheImplicits._ - - import org.mockito.ArgumentMatchers._ - import org.mockito._ - - val expiration = 1.second - - "Redis List" should { - - "prepend (all variants)" in new MockedList { - connector.listPrepend(key, value) returns 5L - connector.listPrepend(key, value, value) returns 10L - // verify - list.prepend(value) must beEqualTo(list).await - List(value, value) ++: list must beEqualTo(list).await - value +: list must beEqualTo(list).await - - there were two(connector).listPrepend(key, value) - there were one(connector).listPrepend(key, value, value) - } - - "prepend (failing)" in new MockedList { - connector.listPrepend(key, value) returns ex - // verify - list.prepend(value) must beEqualTo(list).await - - there were one(connector).listPrepend(key, value) - } - - "append (all variants)" in new MockedList { - connector.listAppend(key, value) returns 5L - connector.listAppend(key, value, value) returns 10L - // verify - list.append(value) must beEqualTo(list).await - list :+ value must beEqualTo(list).await - list :++ Seq(value, value) must beEqualTo(list).await + test("prepend (failing)") { (list, connector) => + for { + _ <- connector.expect.listPrepend(otherKey, Seq(cacheValue), result = failure) + _ <- list.prepend(cacheValue).assertingEqual(list) + } yield Passed + } - there were two(connector).listAppend(key, value) - there were one(connector).listAppend(key, value, value) - } + test("append (all variants)") { (list, connector) => + for { + _ <- connector.expect.listAppend(otherKey,Seq( cacheValue)) + _ <- connector.expect.listAppend(otherKey,Seq( cacheValue)) + _ <- connector.expect.listAppend(otherKey,Seq( cacheValue, cacheValue)) + _ <- list.append(cacheValue).assertingEqual(list) + _ <- (list :+ cacheValue).assertingEqual(list) + _ <- (list :++ Seq(cacheValue, cacheValue)).assertingEqual(list) + } yield Passed + } - "append (failing)" in new MockedList { - connector.listAppend(key, value) returns ex - // verify - list.append(value) must beEqualTo(list).await + test("append (failing)") { (list, connector) => + for { + _ <- connector.expect.listAppend(otherKey, Seq(cacheValue), result = failure) + _ <- list.append(cacheValue).assertingEqual(list) + } yield Passed + } - there were one(connector).listAppend(key, value) + test("get (miss)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 5, 5, Seq(cacheValue)) + _ <- list.get(5).assertingEqual(Some(cacheValue)) + } yield Passed } - "get (miss)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns Seq(value) - list.get(5) must beSome(value).await - } + test("get (hit)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 5, 5, Seq.empty[String]) + _ <- list.get(5).assertingEqual(None) + } yield Passed + } - "get (hit)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns Seq.empty[String] - list.get(5) must beNone.await - } + test("get (failure)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 5, 5, result = failure) + _ <- list.get(5).assertingEqual(None) + } yield Passed + } - "get (failure)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns ex - list.get(5) must beNone.await - } + test("apply (hit)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 5, 5, Seq(cacheValue)) + _ <- list(5).assertingEqual(cacheValue) + } yield Passed + } - "apply (hit)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns Seq(value) - list(5) must beEqualTo(value).await - } + test("apply (miss)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 5, 5, Seq.empty[String]) + _ <- list(5).assertingFailure[NoSuchElementException] + } yield Passed + } - "apply (miss)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns Seq.empty[String] - list(5) must throwA[NoSuchElementException].await - } + test("apply (failing)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 5, 5, result = failure) + _ <- list(5).assertingFailure[NoSuchElementException] + } yield Passed + } - "apply (failing)" in new MockedList { - connector.listSlice[String](beEq(key), anyInt, anyInt)(anyClassTag) returns ex - list(5) must throwA[NoSuchElementException].await - } + test("head pop") { (list, connector) => + for { + _ <- connector.expect.listHeadPop[String](otherKey, result = None) + _ <- list.headPop.assertingEqual(None) + } yield Passed + } - "head pop" in new MockedList { - connector.listHeadPop[String](beEq(key))(anyClassTag) returns None - list.headPop must beNone.await - } + test("head pop (failing)") { (list, connector) => + for { + _ <- connector.expect.listHeadPop[String](otherKey, result = failure) + _ <- list.headPop.assertingEqual(None) + } yield Passed + } - "head pop (failing)" in new MockedList { - connector.listHeadPop[String](beEq(key))(anyClassTag) returns ex - list.headPop must beNone.await - } + test("size") { (list, connector) => + for { + _ <- connector.expect.listSize(otherKey, 2L) + _ <- list.size.assertingEqual(2L) + } yield Passed + } - "size" in new MockedList { - connector.listSize(key) returns 2L - list.size must beEqualTo(2L).await - } + test("size (failing)") { (list, connector) => + for { + _ <- connector.expect.listSize(otherKey, result = failure) + _ <- list.size.assertingEqual(0L) + } yield Passed + } - "size (failing)" in new MockedList { - connector.listSize(key) returns ex - list.size must beEqualTo(0L).await - } + test("insert before") { (list, connector) => + for { + _ <- connector.expect.listInsert(otherKey, "pivot", cacheValue, Some(5L)) + _ <- list.insertBefore("pivot", cacheValue).assertingEqual(Some(5L)) + } yield Passed + } - "insert before" in new MockedList { - connector.listInsert(key, "pivot", value) returns Some(5L) - list.insertBefore("pivot", value) must beSome(5L).await - there were one(connector).listInsert(key, "pivot", value) - } + test("insert before (failing)") { (list, connector) => + for { + _ <- connector.expect.listInsert(otherKey, "pivot", cacheValue, result = failure) + _ <- list.insertBefore("pivot", cacheValue).assertingEqual(None) + } yield Passed + } - "insert before (failing)" in new MockedList { - connector.listInsert(key, "pivot", value) returns ex - list.insertBefore("pivot", value) must beNone.await - there were one(connector).listInsert(key, "pivot", value) - } + test("set at position") { (list, connector) => + for { + _ <- connector.expect.listSetAt(otherKey, 5, cacheValue, result = ()) + _ <- list.set(5, cacheValue).assertingEqual(list) + } yield Passed + } - "set at position" in new MockedList { - connector.listSetAt(beEq(key), anyInt, beEq(value)) returns unit - list.set(5, value) must beEqualTo(list).await - there were one(connector).listSetAt(key, 5, value) - } + test("set at position (failing)") { (list, connector) => + for { + _ <- connector.expect.listSetAt(otherKey, 5, cacheValue, result = failure) + _ <- list.set(5, cacheValue).assertingEqual(list) + } yield Passed + } - "set at position (failing)" in new MockedList { - connector.listSetAt(beEq(key), anyInt, beEq(value)) returns ex - list.set(5, value) must beEqualTo(list).await - there were one(connector).listSetAt(key, 5, value) - } + test("empty list") { (list, connector) => + for { + _ <- connector.expect.listSize(otherKey, result = 0L) + _ <- connector.expect.listSize(otherKey, result = 0L) + _ <- list.isEmpty.assertingEqual(true) + _ <- list.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty list" in new MockedList { - connector.listSize(beEq(key)) returns 0L - list.isEmpty must beTrue.await - list.nonEmpty must beFalse.await - } + test("non-empty list") { (list, connector) => + for { + _ <- connector.expect.listSize(otherKey, 1L) + _ <- connector.expect.listSize(otherKey, 1L) + _ <- list.isEmpty.assertingEqual(false) + _ <- list.nonEmpty.assertingEqual(true) + } yield Passed + } - "non-empty list" in new MockedList { - connector.listSize(beEq(key)) returns 1L - list.isEmpty must beFalse.await - list.nonEmpty must beTrue.await - } + test("empty/non-empty list (failing)") { (list, connector) => + for { + _ <- connector.expect.listSize(otherKey, result = failure) + _ <- connector.expect.listSize(otherKey, result = failure) + _ <- list.isEmpty.assertingEqual(true) + _ <- list.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty/non-empty list (failing)" in new MockedList { - connector.listSize(beEq(key)) returns ex - list.isEmpty must beTrue.await - list.nonEmpty must beFalse.await - } + test("remove element") { (list, connector) => + for { + _ <- connector.expect.listRemove(otherKey, cacheValue, 1, result = 1L) + _ <- list.remove(cacheValue).assertingEqual(list) + } yield Passed + } - "remove element" in new MockedList { - connector.listRemove(anyString, anyString, anyInt) returns 1L - list.remove(value) must beEqualTo(list).await - there were one(connector).listRemove(key, value, 1) - } + test("remove element (failing)") { (list, connector) => + for { + _ <- connector.expect.listRemove(otherKey, cacheValue, 1, result = failure) + _ <- list.remove(cacheValue).assertingEqual(list) + } yield Passed + } - "remove element (failing)" in new MockedList { - connector.listRemove(anyString, anyString, anyInt) returns ex - list.remove(value) must beEqualTo(list).await - there were one(connector).listRemove(key, value, 1) - } + test("remove at position") { (list, connector) => + for { + _ <- connector.expect.listRemoveAt(otherKey, 1, result = 1L) + _ <- list.removeAt(1).assertingEqual(list) + } yield Passed + } - "remove at position" in new MockedList { - Mockito.when(connector.listSetAt(anyString, anyInt, anyString)).thenAnswer { - AdditionalAnswers.answer { - new stubbing.Answer3[Future[Unit], String, Int, String] { - def answer(key: String, position: Int, value: String) = { - data(position) = value - unit - } - } - } - } - - Mockito.when(connector.listRemove(anyString, anyString, anyInt)).thenAnswer { - AdditionalAnswers.answer { - new stubbing.Answer3[Future[Long], String, String, Int] { - def answer(key: String, value: String, count: Int) = { - val index = data.indexOf(value) - if (index > -1) data.remove(index, 1) - if (index > -1) 1L else 0L - } - } - } - } - - list.removeAt(0) must beEqualTo(list).await - data mustEqual Seq(value, value) - - list.removeAt(1) must beEqualTo(list).await - data mustEqual Seq(value) - } + test("remove at position (failing)") { (list, connector) => + for { + _ <- connector.expect.listRemoveAt(otherKey, 1, result = failure) + _ <- list.removeAt(1).assertingEqual(list) + } yield Passed + } - "remove at position (failing)" in new MockedList { - connector.listSetAt(anyString, anyInt, anyString) returns ex - list.removeAt(0) must beEqualTo(list).await - there were one(connector).listSetAt(key, 0, "play-redis:DELETED") - } + test("view all") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 0, -1, result = Seq(cacheValue)) + _ <- list.view.all.assertingEqual(Seq(cacheValue)) + } yield Passed + } - "view all" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns (data: Seq[String]) - list.view.all must beEqualTo(data).await - there were one(connector).listSlice[String](key, 0, -1) - } + test("view take") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 0, 1, result = Seq(cacheValue)) + _ <- list.view.take(2).assertingEqual(Seq(cacheValue)) + } yield Passed + } - "view take" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns (data: Seq[String]) - list.view.take(2) must beEqualTo(data).await - there were one(connector).listSlice[String](key, 0, 1) - } + test("view drop") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 2, -1, result = Seq(cacheValue)) + _ <- list.view.drop(2).assertingEqual(Seq(cacheValue)) + } yield Passed + } - "view drop" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns (data: Seq[String]) - list.view.drop(2) must beEqualTo(data).await - there were one(connector).listSlice[String](key, 2, -1) - } + test("view slice") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 1, 2, result = Seq(cacheValue)) + _ <- list.view.slice(1, 2).assertingEqual(Seq(cacheValue)) + } yield Passed + } - "view slice" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns (data: Seq[String]) - list.view.slice(1, 2) must beEqualTo(data).await - there were one(connector).listSlice[String](key, 1, 2) - } + test("view slice (failing)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 1, 2, result = failure) + _ <- list.view.slice(1, 2).assertingEqual(Seq.empty) + } yield Passed + } - "view slice (failing)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns ex - list.view.slice(1, 2) must beEqualTo(Seq.empty).await - } + test("head (non-empty)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 0, 0, result = Seq(cacheValue)) + _ <- connector.expect.listSlice(otherKey, 0, 0, result = Seq(cacheValue)) + _ <- list.head.assertingEqual(cacheValue) + _ <- list.headOption.assertingEqual(Some(cacheValue)) + } yield Passed + } - "head (non-empty)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns data.headOption.toSeq - list.head must beEqualTo(data.head).await - list.headOption must beSome(data.head).await - there were two(connector).listSlice[String](key, 0, 0) - } + test("head (empty)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 0, 0, result = Seq.empty[String]) + _ <- connector.expect.listSlice(otherKey, 0, 0, result = Seq.empty[String]) + _ <- list.head.assertingFailure[NoSuchElementException] + _ <- list.headOption.assertingEqual(None) + } yield Passed + } - "head (empty)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns Seq.empty[String] - list.head must throwA[NoSuchElementException].await - list.headOption must beNone.await - there were two(connector).listSlice[String](key, 0, 0) - } + test("head (failing)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 0, 0, result = failure) + _ <- connector.expect.listSlice[String](otherKey, 0, 0, result = failure) + _ <- list.head.assertingFailure[NoSuchElementException] + _ <- list.headOption.assertingEqual(None) + } yield Passed + } - "head (failing)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns ex - list.head must throwA[NoSuchElementException].await - list.headOption must beNone.await - } + test("last (non-empty)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, -1, -1, result = Seq(cacheValue)) + _ <- connector.expect.listSlice[String](otherKey, -1, -1, result = Seq(cacheValue)) + _ <- list.last.assertingEqual(cacheValue) + _ <- list.lastOption.assertingEqual(Some(cacheValue)) + } yield Passed + } - "last (non-empty)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns data.headOption.toSeq - list.last must beEqualTo(data.head).await - list.lastOption must beSome(data.head).await - there were two(connector).listSlice[String](key, -1, -1) - } + test("last (empty)") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, -1, -1, result = Seq.empty[String]) + _ <- connector.expect.listSlice(otherKey, -1, -1, result = Seq.empty[String]) + _ <- list.last.assertingFailure[NoSuchElementException] + _ <- list.lastOption.assertingEqual(None) + } yield Passed + } - "last (empty)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns Seq.empty[String] - list.last must throwA[NoSuchElementException].await - list.lastOption must beNone.await - there were two(connector).listSlice[String](key, -1, -1) - } + test("last (failing)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 0, 0, result = failure) + _ <- connector.expect.listSlice[String](otherKey, 0, 0, result = failure) + _ <- list.head.assertingFailure[NoSuchElementException] + _ <- list.headOption.assertingEqual(None) + } yield Passed + } - "last (failing)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns ex - list.head must throwA[NoSuchElementException].await - list.headOption must beNone.await - } + test("toList") { (list, connector) => + for { + _ <- connector.expect.listSlice(otherKey, 0, -1, result = Seq(cacheValue, otherValue)) + _ <- list.toList.assertingEqual(Seq(cacheValue, otherValue)) + } yield Passed + } - "toList" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns (data: Seq[String]) - list.toList must beEqualTo(data).await - there were one(connector).listSlice[String](key, 0, -1) - } + test("toList (failing)") { (list, connector) => + for { + _ <- connector.expect.listSlice[String](otherKey, 0, -1, result = failure) + _ <- list.toList.assertingEqual(Seq.empty) + } yield Passed + } - "toList (failing)" in new MockedList { - connector.listSlice[String](anyString, anyInt, anyInt)(anyClassTag) returns ex - list.toList must beEqualTo(Seq.empty).await - there were one(connector).listSlice[String](key, 0, -1) - } + test("modify collection") { (list, _) => + for { + _ <- Future.successful(list.modify.collection).assertingEqual(list) + } yield Passed + } - "modify collection" in new MockedList { - list.modify.collection mustEqual list - } + test("modify take") { (list, connector) => + for { + _ <- connector.expect.listTrim(otherKey, 0, 1) + _ <- list.modify.take(2).assertingSuccess + } yield Passed + } - "modify take" in new MockedList { - connector.listTrim(anyString, anyInt, anyInt) returns unit - list.modify.take(2) must not(throwA[Throwable]).await - there were one(connector).listTrim(key, 0, 1) - } + test("modify drop") { (list, connector) => + for { + _ <- connector.expect.listTrim(otherKey, 2, -1) + _ <- list.modify.drop(2).assertingSuccess + } yield Passed + } - "modify drop" in new MockedList { - connector.listTrim(anyString, anyInt, anyInt) returns unit - list.modify.drop(2) must not(throwA[Throwable]).await - there were one(connector).listTrim(key, 2, -1) - } + test("modify clear") { (list, connector) => + for { + _ <- connector.expect.remove(otherKey) + _ <- list.modify.clear().assertingSuccess + } yield Passed + } - "modify clear" in new MockedList { - connector.remove(anyVarArgs) returns unit - list.modify.clear() must not(throwA[Throwable]).await - there were one(connector).remove(key) - } + test("modify slice") { (list, connector) => + for { + _ <- connector.expect.listTrim(otherKey, 1, 2) + _ <- list.modify.slice(1, 2).assertingSuccess + } yield Passed + } - "modify slice" in new MockedList { - connector.listTrim(anyString, anyInt, anyInt) returns unit - list.modify.slice(1, 2) must not(throwA[Throwable]).await - there were one(connector).listTrim(key, 1, 2) + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (RedisList[String, AsynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + implicit val builder: Builders.AsynchronousBuilder.type = AsynchronousBuilder + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val list: RedisList[String, AsynchronousResult] = new RedisListImpl[String, AsynchronousResult](otherKey, connector) + f(list, connector) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisMapJavaMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisMapJavaMock.scala new file mode 100644 index 00000000..4e186387 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisMapJavaMock.scala @@ -0,0 +1,89 @@ +package play.api.cache.redis.impl + +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis._ + +import scala.concurrent.Future + +private[impl] trait RedisMapJavaMock { this: AsyncMockFactoryBase => + + protected[impl] trait RedisMapMock extends RedisMap[String, Future] { + + override final def remove(field: String*): Future[RedisMap[String, Future]] = + removeValues(field) + + def removeValues(field: Seq[String]): Future[RedisMap[String, Future]] + } + + final protected implicit class RedisMapOps(map: RedisMapMock) { + def expect: RedisMapExpectation = + new RedisMapExpectation(map) + } + + protected final class RedisMapExpectation(map: RedisMapMock) { + + def add(key: String, value: String): Future[Unit] = + Future.successful { + (map.add(_: String, _: String)) + .expects(key, value) + .returning(Future.successful(map)) + .once() + } + + def get(key: String, value: Option[String]): Future[Unit] = + Future.successful { + (map.get(_: String)) + .expects(key) + .returning(Future.successful(value)) + .once() + } + + def contains(key: String, result: Boolean): Future[Unit] = + Future.successful { + (map.contains(_: String)) + .expects(key) + .returning(Future.successful(result)) + .once() + } + + def remove(key: String*): Future[Unit] = + Future.successful { + (map.removeValues(_: Seq[String])) + .expects(key) + .returning(Future.successful(map)) + .once() + } + + def increment(key: String, by: Long, result: Long): Future[Unit] = + Future.successful { + (map.increment(_: String, _: Long)) + .expects(key, by) + .returning(Future.successful(result)) + .once() + } + + def toMap(values: (String, String)*): Future[Unit] = + Future.successful { + (() => map.toMap) + .expects() + .returning(Future.successful(values.toMap)) + .once() + } + + def keySet(keys: String*): Future[Unit] = + Future.successful { + (() => map.keySet) + .expects() + .returning(Future.successful(keys.toSet)) + .once() + } + + def values(values: String*): Future[Unit] = + Future.successful { + (() => map.values) + .expects() + .returning(Future.successful(values.toSet)) + .once() + } + } +} \ No newline at end of file diff --git a/src/test/scala/play/api/cache/redis/impl/RedisMapSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisMapSpec.scala index 7de61087..fdbbc50c 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisMapSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisMapSpec.scala @@ -1,155 +1,205 @@ package play.api.cache.redis.impl import play.api.cache.redis._ +import play.api.cache.redis.impl.Builders.AsynchronousBuilder +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class RedisMapSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { +class RedisMapSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { - import Implicits._ - import RedisCacheImplicits._ - - import org.mockito.ArgumentMatchers._ - - "Redis Map" should { + test("set") { (map, connector) => + for { + _ <- connector.expect.hashSet(cacheKey, field, cacheValue, result = true) + _ <- map.add(field, cacheValue).assertingEqual(map) + } yield Passed + } - "set" in new MockedMap { - connector.hashSet(anyString, anyString, anyString) returns true - map.add(field, value) must beEqualTo(map).await - there were one(connector).hashSet(key, field, value) - } + test("set (failing)") { (map, connector) => + for { + _ <- connector.expect.hashSet(cacheKey, field, cacheValue, result = failure) + _ <- map.add(field, cacheValue).assertingEqual(map) + } yield Passed + } - "set (failing)" in new MockedMap { - connector.hashSet(anyString, anyString, anyString) returns ex - map.add(field, value) must beEqualTo(map).await - there were one(connector).hashSet(key, field, value) - } + test("get") { (map, connector) => + for { + _ <- connector.expect.hashGet[String](cacheKey, field, result = Some(cacheValue)) + _ <- connector.expect.hashGet[String](cacheKey, otherValue, result = None) + _ <- map.get(field).assertingEqual(Some(cacheValue)) + _ <- map.get(otherValue).assertingEqual(None) + } yield Passed + } - "get" in new MockedMap { - connector.hashGet[String](anyString, beEq(field))(anyClassTag) returns Some(value) - connector.hashGet[String](anyString, beEq(other))(anyClassTag) returns None - map.get(field) must beSome(value).await - map.get(other) must beNone.await - there were one(connector).hashGet[String](key, field) - there were one(connector).hashGet[String](key, other) - } + test("get (failing)") { (map, connector) => + for { + _ <- connector.expect.hashGet[String](cacheKey, field, result = failure) + _ <- map.get(field).assertingEqual(None) + } yield Passed + } - "get (failing)" in new MockedMap { - connector.hashGet[String](anyString, beEq(field))(anyClassTag) returns ex - map.get(field) must beNone.await - there were one(connector).hashGet[String](key, field) - } + test("get fields") { (map, connector) => + for { + _ <- connector.expect.hashGet[String](cacheKey, Seq(field, otherValue), result = Seq(Some(cacheValue), None)) + _ <- connector.expect.hashGet[String](cacheKey, Seq(field, otherValue), result = Seq(Some(cacheValue), None)) + _ <- map.getFields(field, otherValue).assertingEqual(Seq(Some(cacheValue), None)) + _ <- map.getFields(Seq(field, otherValue)).assertingEqual(Seq(Some(cacheValue), None)) + } yield Passed + } - "get fields" in new MockedMap { - connector.hashGet[String](anyString, beEq(Seq(field, other)))(anyClassTag) returns Seq(Some(value), None) - map.getFields(field, other) must beEqualTo(Seq(Some(value), None)).await - map.getFields(Seq(field, other)) must beEqualTo(Seq(Some(value), None)).await - there were two(connector).hashGet[String](key, Seq(field, other)) - } + test("get fields (failing)") { (map, connector) => + for { + _ <- connector.expect.hashGet[String](cacheKey, Seq(field, otherValue), result = failure) + _ <- connector.expect.hashGet[String](cacheKey, Seq(field, otherValue), result = failure) + _ <- map.getFields(field, otherValue).assertingEqual(Seq(None, None)) + _ <- map.getFields(Seq(field, otherValue)).assertingEqual(Seq(None, None)) + } yield Passed + } - "get fields (failing)" in new MockedMap { - connector.hashGet[String](anyString, beEq(Seq(field, other)))(anyClassTag) returns ex - map.getFields(field, other) must beEqualTo(Seq(None, None)).await - map.getFields(Seq(field, other)) must beEqualTo(Seq(None, None)).await - there were two(connector).hashGet[String](key, Seq(field, other)) - } + test("contains") { (map, connector) => + for { + _ <- connector.expect.hashExists(cacheKey, field, result = true) + _ <- connector.expect.hashExists(cacheKey, otherValue, result = false) + _ <- map.contains(field).assertingEqual(true) + _ <- map.contains(otherValue).assertingEqual(false) + } yield Passed + } - "contains" in new MockedMap { - connector.hashExists(anyString, beEq(field)) returns true - connector.hashExists(anyString, beEq(other)) returns false - map.contains(field) must beTrue.await - map.contains(other) must beFalse.await - } + test("contains (failing)") { (map, connector) => + for { + _ <- connector.expect.hashExists(cacheKey, field, result = failure) + _ <- map.contains(field).assertingEqual(false) + } yield Passed + } - "contains (failing)" in new MockedMap { - connector.hashExists(anyString, anyString) returns ex - map.contains(field) must beFalse.await - there were one(connector).hashExists(key, field) - } + test("remove") { (map, connector) => + for { + _ <- connector.expect.hashRemove(cacheKey, Seq(field)) + _ <- connector.expect.hashRemove(cacheKey, Seq(field, otherValue)) + _ <- map.remove(field).assertingEqual(map) + _ <- map.remove(field, otherValue).assertingEqual(map) + } yield Passed + } - "remove" in new MockedMap { - connector.hashRemove(anyString, anyVarArgs) returns 1L - map.remove(field) must beEqualTo(map).await - map.remove(field, other) must beEqualTo(map).await - there were one(connector).hashRemove(key, field) - there were one(connector).hashRemove(key, field, other) - } + test("remove (failing)") { (map, connector) => + for { + _ <- connector.expect.hashRemove(cacheKey, Seq(field), result = failure) + _ <- map.remove(field).assertingEqual(map) + } yield Passed + } - "remove (failing)" in new MockedMap { - connector.hashRemove(anyString, anyVarArgs) returns ex - map.remove(field) must beEqualTo(map).await - there were one(connector).hashRemove(key, field) - } + test("increment") { (map, connector) => + for { + _ <- connector.expect.hashIncrement(cacheKey, field, 2L, result = 5L) + _ <- map.increment(field, 2L).assertingEqual(5L) + } yield Passed + } - "increment" in new MockedMap { - connector.hashIncrement(anyString, beEq(field), anyLong) returns 5L - map.increment(field, 2L) must beEqualTo(5L).await - there were one(connector).hashIncrement(key, field, 2L) - } + test("increment (failing)") { (map, connector) => + for { + _ <- connector.expect.hashIncrement(cacheKey, field, 2L, result = failure) + _ <- map.increment(field, 2L).assertingEqual(2L) + } yield Passed + } - "increment (failing)" in new MockedMap { - connector.hashIncrement(anyString, beEq(field), anyLong) returns ex - map.increment(field, 2L) must beEqualTo(2L).await - there were one(connector).hashIncrement(key, field, 2L) - } + test("toMap") { (map, connector) => + for { + _ <- connector.expect.hashGetAll[String](cacheKey, result = Map(field -> cacheValue)) + _ <- map.toMap.assertingEqual(Map(field -> cacheValue)) + } yield Passed + } - "toMap" in new MockedMap { - connector.hashGetAll[String](anyString)(anyClassTag) returns Map(field -> value) - map.toMap must beEqualTo(Map(field -> value)).await - } + test("toMap (failing)") { (map, connector) => + for { + _ <- connector.expect.hashGetAll[String](cacheKey, result = failure) + _ <- map.toMap.assertingEqual(Map.empty) + } yield Passed + } - "toMap (failing)" in new MockedMap { - connector.hashGetAll[String](anyString)(anyClassTag) returns ex - map.toMap must beEqualTo(Map.empty).await - } + test("keySet") { (map, connector) => + for { + _ <- connector.expect.hashKeys(cacheKey, result = Set(field)) + _ <- map.keySet.assertingEqual(Set(field)) + } yield Passed + } - "keySet" in new MockedMap { - connector.hashKeys(anyString) returns Set(field) - map.keySet must beEqualTo(Set(field)).await - } + test("keySet (failing)") { (map, connector) => + for { + _ <- connector.expect.hashKeys(cacheKey, result = failure) + _ <- map.keySet.assertingEqual(Set.empty) + } yield Passed + } - "keySet (failing)" in new MockedMap { - connector.hashKeys(anyString) returns ex - map.keySet must beEqualTo(Set.empty).await - } + test("values") { (map, connector) => + for { + _ <- connector.expect.hashValues[String](cacheKey, result = Set(cacheValue)) + _ <- map.values.assertingEqual(Set(cacheValue)) + } yield Passed + } - "values" in new MockedMap { - connector.hashValues[String](anyString)(anyClassTag) returns Set(value) - map.values must beEqualTo(Set(value)).await - } + test("values (failing)") { (map, connector) => + for { + _ <- connector.expect.hashValues[String](cacheKey, result = failure) + _ <- map.values.assertingEqual(Set.empty) + } yield Passed + } - "values (failing)" in new MockedMap { - connector.hashValues[String](anyString)(anyClassTag) returns ex - map.values must beEqualTo(Set.empty).await - } + test("size") { (map, connector) => + for { + _ <- connector.expect.hashSize(cacheKey, result = 2L) + _ <- map.size.assertingEqual(2L) + } yield Passed + } - "size" in new MockedMap { - connector.hashSize(key) returns 2L - map.size must beEqualTo(2L).await - } + test("size (failing)") { (map, connector) => + for { + _ <- connector.expect.hashSize(cacheKey, result = failure) + _ <- map.size.assertingEqual(0L) + } yield Passed + } - "size (failing)" in new MockedMap { - connector.hashSize(key) returns ex - map.size must beEqualTo(0L).await - } + test("empty map") { (map, connector) => + for { + _ <- connector.expect.hashSize(cacheKey, result = 0L) + _ <- connector.expect.hashSize(cacheKey, result = 0L) + _ <- map.isEmpty.assertingEqual(true) + _ <- map.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty map" in new MockedMap { - connector.hashSize(beEq(key)) returns 0L - map.isEmpty must beTrue.await - map.nonEmpty must beFalse.await - } + test("non-empty map") { (map, connector) => + for { + _ <- connector.expect.hashSize(cacheKey, result = 1L) + _ <- connector.expect.hashSize(cacheKey, result = 1L) + _ <- map.isEmpty.assertingEqual(false) + _ <- map.nonEmpty.assertingEqual(true) + } yield Passed + } - "non-empty map" in new MockedMap { - connector.hashSize(beEq(key)) returns 1L - map.isEmpty must beFalse.await - map.nonEmpty must beTrue.await - } + test("empty/non-empty map (failing)") { (map, connector) => + for { + _ <- connector.expect.hashSize(cacheKey, result = failure) + _ <- connector.expect.hashSize(cacheKey, result = failure) + _ <- map.isEmpty.assertingEqual(true) + _ <- map.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty/non-empty map (failing)" in new MockedMap { - connector.hashSize(beEq(key)) returns ex - map.isEmpty must beTrue.await - map.nonEmpty must beFalse.await + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (RedisMap[String, AsynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + implicit val builder: Builders.AsynchronousBuilder.type = AsynchronousBuilder + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val map: RedisMap[String, AsynchronousResult] = new RedisMapImpl[String, AsynchronousResult](cacheKey, connector) + f(map, connector) } } -} + } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala index 31481927..e0ecc5cb 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala @@ -1,36 +1,44 @@ package play.api.cache.redis.impl -import play.api.cache.redis._ +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class RedisPrefixSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { +class RedisPrefixSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { - import Implicits._ - import RedisCacheImplicits._ - - import org.mockito.ArgumentMatchers._ - - "RedisPrefix" should { + test("apply when defined", prefix = new RedisPrefixImpl("prefix")) { (connector, cache) => + for { + // get one + _ <- connector.expect.get[String](s"prefix:$cacheKey", None) + _ <- cache.get[String](cacheKey).assertingEqual(None) + // get multiple + _ <- connector.expect.mGet[String](Seq(s"prefix:$cacheKey", s"prefix:$otherKey"), Seq(None, Some(cacheValue))) + _ <- cache.getAll[String](cacheKey, otherKey).assertingEqual(Seq(None, Some(cacheValue))) + } yield Passed + } - "apply when defined" in new MockedCache { - runtime.prefix returns new RedisPrefixImpl("prefix") - connector.get[String](anyString)(anyClassTag) returns None - connector.mGet[String](anyVarArgs)(anyClassTag) returns Seq(None, Some(value)) - // run the test - cache.get[String](key) must beNone.await - cache.getAll[String](key, other) must beEqualTo(Seq(None, Some(value))).await - there were one(connector).get[String](s"prefix:$key") - there were one(connector).mGet[String](s"prefix:$key", s"prefix:$other") - } + test("not apply when is empty", prefix = RedisEmptyPrefix) { (connector, cache) => + for { + // get one + _ <- connector.expect.get[String](cacheKey, None) + _ <- cache.get[String](cacheKey).assertingEqual(None) + // get multiple + _ <- connector.expect.mGet[String](Seq(cacheKey, otherKey), Seq(None, Some(cacheValue))) + _ <- cache.getAll[String](cacheKey, otherKey).assertingEqual(Seq(None, Some(cacheValue))) + } yield Passed + } - "not apply when is empty" in new MockedCache { - runtime.prefix returns RedisEmptyPrefix - connector.get[String](anyString)(anyClassTag) returns None - // run the test - cache.get[String](key) must beNone.await - there were one(connector).get[String](key) + private def test( + name: String, + prefix: RedisPrefix + )( + f: (RedisConnectorMock, AsyncRedis) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime(prefix = prefix) + val connector = mock[RedisConnectorMock] + val cache: AsyncRedis = new AsyncRedisImpl(connector) + f(connector, cache) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeMock.scala new file mode 100644 index 00000000..0bc693e9 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeMock.scala @@ -0,0 +1,41 @@ +package play.api.cache.redis.impl + +import akka.util.Timeout +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis.{FailThrough, RecoverWithDefault, RecoveryPolicy, RedisException} + +import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.duration._ + +private[impl] trait RedisRuntimeMock { outer: AsyncMockFactoryBase => + + protected object recoveryPolicy { + + private class RerunPolicy extends RecoveryPolicy { + override def recoverFrom[T]( + rerun: => Future[T], + default: => Future[T], + failure: RedisException + ): Future[T] = rerun + } + + val failThrough: RecoveryPolicy = new FailThrough {} + val default: RecoveryPolicy = new RecoverWithDefault {} + val rerun: RecoveryPolicy = new RerunPolicy + } + + protected def redisRuntime( + invocationPolicy: InvocationPolicy = EagerInvocation, + recoveryPolicy: RecoveryPolicy = outer.recoveryPolicy.failThrough, + timeout: FiniteDuration = 200.millis, + prefix: RedisPrefix = RedisEmptyPrefix, + ): RedisRuntime = { + val runtime = mock[RedisRuntime] + (() => runtime.context).expects().returns(ExecutionContext.global).anyNumberOfTimes() + (() => runtime.invocation).expects().returns(invocationPolicy).anyNumberOfTimes() + (() => runtime.prefix).expects().returns(prefix).anyNumberOfTimes() + (() => runtime.policy).expects().returns(recoveryPolicy).anyNumberOfTimes() + (() => runtime.timeout).expects().returns(Timeout(timeout)).anyNumberOfTimes() + runtime + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala index 96390493..bd2aef9c 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala @@ -1,52 +1,67 @@ package play.api.cache.redis.impl +import akka.actor.ActorSystem +import akka.util.Timeout import play.api.cache.redis._ +import play.api.cache.redis.configuration.{RedisHost, RedisStandalone} +import play.api.cache.redis.test.UnitSpec -import org.specs2.mutable.Specification +class RedisRuntimeSpec extends UnitSpec { + import RedisRuntime._ -class RedisRuntimeSpec extends Specification with WithApplication { + private implicit val recoveryResolver: RecoveryPolicyResolver = + new RecoveryPolicyResolverImpl - import Implicits._ + private implicit val system: ActorSystem = ActorSystem("test") - implicit val recoveryResolver: RecoveryPolicyResolver = new RecoveryPolicyResolverImpl - - "RedisRuntime" should { - import RedisRuntime._ - - "be build from config (A)" in { - val runtime = RedisRuntime( - instance = defaultInstance, - recovery = "log-and-fail", - invocation = "eager", - prefix = None - ) - - runtime.timeout mustEqual akka.util.Timeout(defaultInstance.timeout.sync) - runtime.policy must beAnInstanceOf[LogAndFailPolicy] - runtime.invocation mustEqual EagerInvocation - runtime.prefix mustEqual RedisEmptyPrefix - } + "be build from config (A)" in { + val instance = RedisStandalone( + name = "standalone", + host = RedisHost(localhost, defaultPort), + settings = defaultsSettings + ) + val runtime = RedisRuntime( + instance = instance, + recovery = "log-and-fail", + invocation = "eager", + prefix = None + ) + runtime.timeout mustEqual Timeout(instance.timeout.sync) + runtime.policy mustBe a[LogAndFailPolicy] + runtime.invocation mustEqual EagerInvocation + runtime.prefix mustEqual RedisEmptyPrefix + } "be build from config (B)" in { + val instance = RedisStandalone( + name = "standalone", + host = RedisHost(localhost, defaultPort), + settings = defaultsSettings + ) val runtime = RedisRuntime( - instance = defaultInstance, + instance = instance, recovery = "log-and-default", invocation = "lazy", prefix = Some("prefix") ) - - runtime.policy must beAnInstanceOf[LogAndDefaultPolicy] + runtime.policy mustBe a[LogAndDefaultPolicy] runtime.invocation mustEqual LazyInvocation runtime.prefix mustEqual new RedisPrefixImpl("prefix") } "be build from config (C)" in { - RedisRuntime( - instance = defaultInstance, - recovery = "log-and-default", - invocation = "other", - prefix = Some("prefix") - ) must throwA[IllegalArgumentException] + val instance = RedisStandalone( + name = "standalone", + host = RedisHost(localhost, defaultPort), + settings = defaultsSettings + ) + assertThrows[IllegalArgumentException] { + RedisRuntime( + instance = instance, + recovery = "log-and-default", + invocation = "other", + prefix = Some("prefix") + ) + } } - } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisSetJavaMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisSetJavaMock.scala new file mode 100644 index 00000000..728e69aa --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisSetJavaMock.scala @@ -0,0 +1,62 @@ +package play.api.cache.redis.impl + +import org.scalamock.scalatest.AsyncMockFactoryBase +import play.api.cache.redis._ + +import scala.concurrent.Future + +private[impl] trait RedisSetJavaMock { this: AsyncMockFactoryBase => + + protected[impl] trait RedisSetMock extends RedisSet[String, Future] { + + override final def add(values: String*): Future[RedisSet[String, Future]] = + addValues(values) + + def addValues(value: Seq[String]): Future[RedisSet[String, Future]] + + override final def remove(values: String*): Future[RedisSet[String, Future]] = + removeValues(values) + + def removeValues(value: Seq[String]): Future[RedisSet[String, Future]] + } + + final protected implicit class RedisSetOps(set: RedisSetMock) { + def expect: RedisSetExpectation = + new RedisSetExpectation(set) + } + + protected final class RedisSetExpectation(set: RedisSetMock) { + + def add(value: String*): Future[Unit] = + Future.successful { + (set.addValues(_: Seq[String])) + .expects(value) + .returning(Future.successful(set)) + .once() + } + + def contains(value: String, result: Boolean): Future[Unit] = + Future.successful { + (set.contains(_: String)) + .expects(value) + .returning(Future.successful(result)) + .once() + } + + def remove(value: String*): Future[Unit] = + Future.successful { + (set.removeValues(_: Seq[String])) + .expects(value) + .returning(Future.successful(set)) + .once() + } + + def toSet(values: String*): Future[Unit] = + Future.successful { + (() => set.toSet) + .expects() + .returning(Future.successful(values.toSet)) + .once() + } + } +} \ No newline at end of file diff --git a/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala index c216f08a..add20ac6 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala @@ -1,95 +1,131 @@ package play.api.cache.redis.impl import play.api.cache.redis._ +import play.api.cache.redis.impl.Builders.AsynchronousBuilder +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class RedisSetSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import RedisCacheImplicits._ +class RedisSetSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { - import org.mockito.ArgumentMatchers._ - - "Redis Set" should { + test("add") { (set, connector) => + for { + _ <- connector.expect.setAdd(cacheKey, Seq(cacheValue)) + _ <- connector.expect.setAdd(cacheKey, Seq(cacheValue, otherValue)) + _ <- set.add(cacheValue).assertingEqual(set) + _ <- set.add(cacheValue, otherValue).assertingEqual(set) + } yield Passed + } - "add" in new MockedSet { - connector.setAdd(anyString, anyVarArgs) returns 5L - set.add(value) must beEqualTo(set).await - set.add(value, other) must beEqualTo(set).await - there were one(connector).setAdd(key, value) - there were one(connector).setAdd(key, value, other) - } + test("add (failing)") { (set, connector) => + for { + _ <- connector.expect.setAdd(cacheKey, Seq(cacheValue), result = failure) + _ <- set.add(cacheValue).assertingEqual(set) + } yield Passed + } - "add (failing)" in new MockedSet { - connector.setAdd(anyString, anyVarArgs) returns ex - set.add(value) must beEqualTo(set).await - there were one(connector).setAdd(key, value) - } + test("contains") { (set, connector) => + for { + _ <- connector.expect.setIsMember(cacheKey, cacheValue, result = true) + _ <- connector.expect.setIsMember(cacheKey, otherValue, result = false) + _ <- set.contains(cacheValue).assertingEqual(true) + _ <- set.contains(otherValue).assertingEqual(false) + } yield Passed + } - "contains" in new MockedSet { - connector.setIsMember(anyString, beEq(value)) returns true - connector.setIsMember(anyString, beEq(other)) returns false - set.contains(value) must beTrue.await - set.contains(other) must beFalse.await - } + test("contains (failing)") { (set, connector) => + for { + _ <- connector.expect.setIsMember(cacheKey, cacheValue, result = failure) + _ <- set.contains(cacheValue).assertingEqual(false) + } yield Passed + } - "contains (failing)" in new MockedSet { - connector.setIsMember(anyString, anyString) returns ex - set.contains(value) must beFalse.await - there were one(connector).setIsMember(key, value) - } + test("remove") { (set, connector) => + for { + _ <- connector.expect.setRemove(cacheKey, Seq(cacheValue)) + _ <- connector.expect.setRemove(cacheKey, Seq(otherValue, cacheValue)) + _ <- set.remove(cacheValue).assertingEqual(set) + _ <- set.remove(otherValue, cacheValue).assertingEqual(set) + } yield Passed + } - "remove" in new MockedSet { - connector.setRemove(anyString, anyVarArgs) returns 1L - set.remove(value) must beEqualTo(set).await - set.remove(other, value) must beEqualTo(set).await - there were one(connector).setRemove(key, value) - there were one(connector).setRemove(key, other, value) - } + test("remove (failing)") { (set, connector) => + for { + _ <- connector.expect.setRemove(cacheKey, Seq(cacheValue), result = failure) + _ <- set.remove(cacheValue).assertingEqual(set) + } yield Passed + } - "remove (failing)" in new MockedSet { - connector.setRemove(anyString, anyVarArgs) returns ex - set.remove(value) must beEqualTo(set).await - there were one(connector).setRemove(key, value) - } + test("toSet") { (set, connector) => + for { + _ <- connector.expect.setMembers[String](cacheKey, result = Set[Any](cacheValue, otherValue)) + _ <- set.toSet.assertingEqual(Set(cacheValue, otherValue)) + } yield Passed + } - "toSet" in new MockedSet { - connector.setMembers[String](anyString)(anyClassTag) returns (data.toSet: Set[String]) - set.toSet must beEqualTo(data).await - } + test("toSet (failing)") { (set, connector) => + for { + _ <- connector.expect.setMembers[String](cacheKey, result = failure) + _ <- set.toSet.assertingEqual(Set.empty) + } yield Passed + } - "toSet (failing)" in new MockedSet { - connector.setMembers[String](anyString)(anyClassTag) returns ex - set.toSet must beEqualTo(Set.empty).await - } + test("size") { (set, connector) => + for { + _ <- connector.expect.setSize(cacheKey, result = 2L) + _ <- set.size.assertingEqual(2L) + } yield Passed + } - "size" in new MockedSet { - connector.setSize(key) returns 2L - set.size must beEqualTo(2L).await - } + test("size (failing)") { (set, connector) => + for { + _ <- connector.expect.setSize(cacheKey, result = failure) + _ <- set.size.assertingEqual(0L) + } yield Passed + } - "size (failing)" in new MockedSet { - connector.setSize(key) returns ex - set.size must beEqualTo(0L).await - } + test("empty set") { (set, connector) => + for { + _ <- connector.expect.setSize(cacheKey, result = 0L) + _ <- connector.expect.setSize(cacheKey, result = 0L) + _ <- set.isEmpty.assertingEqual(true) + _ <- set.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty set" in new MockedSet { - connector.setSize(beEq(key)) returns 0L - set.isEmpty must beTrue.await - set.nonEmpty must beFalse.await - } + test("non-empty set") { (set, connector) => + for { + _ <- connector.expect.setSize(cacheKey, result = 1L) + _ <- connector.expect.setSize(cacheKey, result = 1L) + _ <- set.isEmpty.assertingEqual(false) + _ <- set.nonEmpty.assertingEqual(true) + } yield Passed + } - "non-empty set" in new MockedSet { - connector.setSize(beEq(key)) returns 1L - set.isEmpty must beFalse.await - set.nonEmpty must beTrue.await - } + test("empty/non-empty set (failing)") { (set, connector) => + for { + _ <- connector.expect.setSize(cacheKey, result = failure) + _ <- connector.expect.setSize(cacheKey, result = failure) + _ <- set.isEmpty.assertingEqual(true) + _ <- set.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty/non-empty set (failing)" in new MockedSet { - connector.setSize(beEq(key)) returns ex - set.isEmpty must beTrue.await - set.nonEmpty must beFalse.await + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (RedisSet[String, AsynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + implicit val builder: Builders.AsynchronousBuilder.type = AsynchronousBuilder + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val set: RedisSet[String, AsynchronousResult] = new RedisSetImpl[String, AsynchronousResult](cacheKey, connector) + f(set, connector) } } } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisSortedSetSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisSortedSetSpec.scala index d0a867af..22cdbdb7 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisSortedSetSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisSortedSetSpec.scala @@ -1,98 +1,134 @@ package play.api.cache.redis.impl -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification import play.api.cache.redis._ +import play.api.cache.redis.impl.Builders.AsynchronousBuilder +import play.api.cache.redis.test._ -import scala.reflect.ClassTag +import scala.concurrent.Future -class RedisSortedSetSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { - import Implicits._ - import RedisCacheImplicits._ - import org.mockito.ArgumentMatchers._ +class RedisSortedSetSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { - "Redis Set" should { + private val scoreValue: (Double, String) = (1.0, "cacheValue") + private val otherScoreValue: (Double, String) = (2.0, "other") - "add" in new MockedSortedSet { - connector.sortedSetAdd(anyString, anyVarArgs) returns 5L - set.add(scoreValue) must beEqualTo(set).await - set.add(scoreValue, otherScoreValue) must beEqualTo(set).await - there were one(connector).sortedSetAdd(key, scoreValue) - there were one(connector).sortedSetAdd(key, scoreValue, otherScoreValue) - } + test("add") { (set, connector) => + for { + _ <- connector.expect.sortedSetAdd(cacheKey, Seq(scoreValue)) + _ <- connector.expect.sortedSetAdd(cacheKey, Seq(scoreValue, otherScoreValue)) + _ <- set.add(scoreValue).assertingEqual(set) + _ <- set.add(scoreValue, otherScoreValue).assertingEqual(set) + } yield Passed + } - "add (failing)" in new MockedSortedSet { - connector.sortedSetAdd(anyString, anyVarArgs) returns ex - set.add(scoreValue) must beEqualTo(set).await - there were one(connector).sortedSetAdd(key, scoreValue) - } + test("add (failing)") { (set, connector) => + for { + _ <- connector.expect.sortedSetAdd(cacheKey, Seq(scoreValue), result = failure) + _ <- set.add(scoreValue).assertingEqual(set) + } yield Passed + } - "contains (hit)" in new MockedSortedSet { - connector.sortedSetScore(beEq(key), beEq(other)) returns Some(1D) - set.contains(other) must beTrue.await - there was one(connector).sortedSetScore(key, other) - } + test("contains (hit)") { (set, connector) => + for { + _ <- connector.expect.sortedSetScore(cacheKey, otherValue, result = Some(1D)) + _ <- set.contains(otherValue).assertingEqual(true) + } yield Passed + } - "contains (miss)" in new MockedSortedSet { - connector.sortedSetScore(beEq(key), beEq(other)) returns None - set.contains(other) must beFalse.await - there was one(connector).sortedSetScore(key, other) - } + test("contains (miss)") { (set, connector) => + for { + _ <- connector.expect.sortedSetScore(cacheKey, otherValue, result = None) + _ <- set.contains(otherValue).assertingEqual(false) + } yield Passed + } - "remove" in new MockedSortedSet { - connector.sortedSetRemove(anyString, anyVarArgs) returns 1L - set.remove(value) must beEqualTo(set).await - set.remove(other, value) must beEqualTo(set).await - there were one(connector).sortedSetRemove(key, value) - there were one(connector).sortedSetRemove(key, other, value) - } + test("remove") { (set, connector) => + for { + _ <- connector.expect.sortedSetRemove(cacheKey, Seq(cacheValue)) + _ <- connector.expect.sortedSetRemove(cacheKey, Seq(otherValue, cacheValue)) + _ <- set.remove(cacheValue).assertingEqual(set) + _ <- set.remove(otherValue, cacheValue).assertingEqual(set) + } yield Passed + } - "remove (failing)" in new MockedSortedSet { - connector.sortedSetRemove(anyString, anyVarArgs) returns ex - set.remove(value) must beEqualTo(set).await - there was one(connector).sortedSetRemove(key, value) - } + test("remove (failing)") { (set, connector) => + for { + _ <- connector.expect.sortedSetRemove(cacheKey, Seq(cacheValue), result = failure) + _ <- set.remove(cacheValue).assertingEqual(set) + } yield Passed + } - "range" in new MockedSortedSet { - val data = Seq(value, other) - connector.sortedSetRange[String](anyString, anyLong, anyLong)(anyClassTag) returns data - set.range(1, 5) must beEqualTo(data).await - there was one(connector).sortedSetRange(key, 1, 5)(implicitly[ClassTag[String]]) - } + test("range") { (set, connector) => + val data = Seq(cacheValue, otherValue) + for { + _ <- connector.expect.sortedSetRange[String](cacheKey, 1, 5, result = data) + _ <- set.range(1, 5).assertingEqual(data) + } yield Passed + } - "range (reversed)" in new MockedSortedSet { - val data = Seq(value, other) - connector.sortedSetReverseRange[String](anyString, anyLong, anyLong)(anyClassTag) returns data - set.range(1, 5, isReverse = true) must beEqualTo(data).await - there was one(connector).sortedSetReverseRange(key, 1, 5)(implicitly[ClassTag[String]]) - } + test("range (reversed)") { (set, connector) => + val data = Seq(cacheValue, otherValue) + for { + _ <- connector.expect.sortedSetReverseRange[String](cacheKey, 1, 5, result = data) + _ <- set.range(1, 5, isReverse = true).assertingEqual(data) + } yield Passed + } - "size" in new MockedSortedSet { - connector.sortedSetSize(key) returns 2L - set.size must beEqualTo(2L).await - } + test("size") { (set, connector) => + for { + _ <- connector.expect.sortedSetSize(cacheKey, result = 2L) + _ <- set.size.assertingEqual(2L) + } yield Passed + } - "size (failing)" in new MockedSortedSet { - connector.sortedSetSize(key) returns ex - set.size must beEqualTo(0L).await - } + test("size (failing)") { (set, connector) => + for { + _ <- connector.expect.sortedSetSize(cacheKey, result = failure) + _ <- set.size.assertingEqual(0L) + } yield Passed + } - "empty set" in new MockedSortedSet { - connector.sortedSetSize(beEq(key)) returns 0L - set.isEmpty must beTrue.await - set.nonEmpty must beFalse.await - } + test("empty set") { (set, connector) => + for { + _ <- connector.expect.sortedSetSize(cacheKey, result = 0L) + _ <- connector.expect.sortedSetSize(cacheKey, result = 0L) + _ <- set.isEmpty.assertingEqual(true) + _ <- set.nonEmpty.assertingEqual(false) + } yield Passed + } - "non-empty set" in new MockedSortedSet { - connector.sortedSetSize(beEq(key)) returns 1L - set.isEmpty must beFalse.await - set.nonEmpty must beTrue.await - } + test("non-empty set") { (set, connector) => + for { + _ <- connector.expect.sortedSetSize(cacheKey, result = 1L) + _ <- connector.expect.sortedSetSize(cacheKey, result = 1L) + _ <- set.isEmpty.assertingEqual(false) + _ <- set.nonEmpty.assertingEqual(true) + } yield Passed + } + + test("empty/non-empty set (failing)") { (set, connector) => + for { + _ <- connector.expect.sortedSetSize(cacheKey, result = failure) + _ <- connector.expect.sortedSetSize(cacheKey, result = failure) + _ <- set.isEmpty.assertingEqual(true) + _ <- set.nonEmpty.assertingEqual(false) + } yield Passed + } - "empty/non-empty set (failing)" in new MockedSortedSet { - connector.sortedSetSize(beEq(key)) returns ex - set.isEmpty must beTrue.await - set.nonEmpty must beFalse.await + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default + )( + f: (RedisSortedSet[String, AsynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + ) + implicit val builder: Builders.AsynchronousBuilder.type = AsynchronousBuilder + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val set: RedisSortedSet[String, AsynchronousResult] = new RedisSortedSetImpl[String, AsynchronousResult](cacheKey, connector) + f(set, connector) } } -} + } diff --git a/src/test/scala/play/api/cache/redis/impl/SyncRedisSpec.scala b/src/test/scala/play/api/cache/redis/impl/SyncRedisSpec.scala index 62b9fcab..abc7109b 100644 --- a/src/test/scala/play/api/cache/redis/impl/SyncRedisSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/SyncRedisSpec.scala @@ -1,47 +1,88 @@ package play.api.cache.redis.impl -import scala.concurrent.duration._ - import play.api.cache.redis._ +import play.api.cache.redis.test._ -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification +import scala.concurrent.Future -class SyncRedisSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { +class SyncRedisSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnectorMock with ImplicitFutureMaterialization { + import Helpers._ - import Implicits._ - import RedisCacheImplicits._ + test("get or else (hit)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = Some(cacheValue)) + orElse = probe.orElse.const(otherValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 0 + } yield Passed + } - import org.mockito.ArgumentMatchers._ + test("get or else (miss)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + orElse = probe.orElse.const(cacheValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 1 + } yield Passed + } - "SyncRedis" should { + test("get or else (failure in get)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = failure) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + orElse = probe.orElse.const(cacheValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (hit)" in new MockedSyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns Some(value) - cache.getOrElse(key)(doElse(value)) must beEqualTo(value) - orElse mustEqual 0 - } + test("get or else (failure in set)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = None) + _ <- connector.expect.set(cacheKey, cacheValue, result = failure) + orElse = probe.orElse.const(cacheValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (miss)" in new MockedSyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns None - connector.set(anyString, anyString, any[Duration], anyBoolean) returns true - cache.getOrElse(key)(doElse(value)) must beEqualTo(value) - orElse mustEqual 1 - } + test("get or else (failure in orElse)") { (cache, connector) => + for { + _ <- connector.expect.get[String](cacheKey, result = failure) + _ <- connector.expect.set(cacheKey, cacheValue, result = true) + orElse = probe.orElse.const(cacheValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (failure)" in new MockedSyncRedis with OrElse { - connector.get[String](anyString)(anyClassTag) returns ex - connector.set(anyString, anyString, any[Duration], anyBoolean) returns true - cache.getOrElse(key)(doElse(value)) must beEqualTo(value) - orElse mustEqual 1 - } + test("get or else (prefixed,miss)", prefix = Some("the-prefix")) { (cache, connector) => + for { + _ <- connector.expect.get[String](s"the-prefix:$cacheKey", result = None) + _ <- connector.expect.set(s"the-prefix:$cacheKey", cacheValue, result = true) + orElse = probe.orElse.const(cacheValue) + _ = cache.getOrElse(cacheKey)(orElse.execute()) mustEqual cacheValue + _ = orElse.calls mustEqual 1 + } yield Passed + } - "get or else (prefixed,miss)" in new MockedSyncRedis with OrElse { - runtime.prefix returns new RedisPrefixImpl("prefix") - connector.get[String](beEq(s"prefix:$key"))(anyClassTag) returns None - connector.set(beEq(s"prefix:$key"), anyString, any[Duration], anyBoolean) returns true - cache.getOrElse(key)(doElse(value)) must beEqualTo(value) - orElse mustEqual 1 + private def test( + name: String, + policy: RecoveryPolicy = recoveryPolicy.default, + prefix: Option[String] = None, + )( + f: (RedisCache[SynchronousResult], RedisConnectorMock) => Future[Assertion] + ): Unit = { + name in { + implicit val runtime: RedisRuntime = redisRuntime( + invocationPolicy = LazyInvocation, + recoveryPolicy = policy, + prefix = prefix.fold[RedisPrefix](RedisEmptyPrefix)(new RedisPrefixImpl(_)), + ) + val connector: RedisConnectorMock = mock[RedisConnectorMock] + val cache: RedisCache[SynchronousResult] = new SyncRedis(connector) + f(cache, connector) } } -} + } diff --git a/src/test/scala/play/api/cache/redis/test/BaseSpec.scala b/src/test/scala/play/api/cache/redis/test/BaseSpec.scala new file mode 100644 index 00000000..2f173560 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/BaseSpec.scala @@ -0,0 +1,165 @@ +package play.api.cache.redis.test + +import akka.Done +import org.scalactic.source.Position +import org.scalamock.scalatest.AsyncMockFactory +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.{AnyWordSpecLike, AsyncWordSpecLike} +import org.scalatest._ +import play.api.cache.redis.RedisException +import play.api.cache.redis.configuration._ + +import java.util.concurrent.{CompletionStage, TimeoutException} +import scala.concurrent.duration._ +import scala.concurrent.{Await, ExecutionContext, Future} +import scala.language.implicitConversions +import scala.reflect.ClassTag +import scala.util.{Failure, Success, Try} + +trait DefaultValues { + + protected final val defaultCacheName: String = "play" + protected final val localhost = "localhost" + protected final val defaultPort: Int = 6379 + + val defaultsSettings: RedisSettingsTest = + RedisSettingsTest( + invocationContext = "akka.actor.default-dispatcher", + invocationPolicy = "lazy", + timeout = RedisTimeouts(1.second, None, Some(500.millis)), + recovery = "log-and-default", + source = "standalone" + ) + + protected final val cacheKey: String = "cache-key" + protected final val cacheValue: String = "cache-value" + protected final val otherKey: String = "other-key" + protected final val otherValue: String = "other-value" + protected final val field: String = "field" + + protected final val cacheExpiration: FiniteDuration = 1.minute + + protected final val failure: RedisException = SimulatedException.asRedis + +} + +trait ImplicitOptionMaterialization { + protected implicit def implicitlyAny2Some[T](value: T): Option[T] = Some(value) +} + +trait ImplicitFutureMaterialization { + protected implicit def implicitlyThrowable2Future[T](cause: Throwable): Future[T] = Future.failed(cause) + protected implicit def implicitlyAny2Future[T](value: T): Future[T] = Future.successful(value) +} + +trait TimeLimitedSpec extends AsyncTestSuiteMixin with AsyncUtilities { + this: AsyncTestSuite => + + protected def testTimeout: FiniteDuration = 1.second + + private class TimeLimitedTest(inner: NoArgAsyncTest) extends NoArgAsyncTest { + override def apply(): FutureOutcome = restrict(inner()) + + override val configMap: ConfigMap = inner.configMap + override val name: String = inner.name + override val scopes: IndexedSeq[String] = inner.scopes + override val text: String = inner.text + override val tags: Set[String] = inner.tags + override val pos: Option[Position] = inner.pos + } + + private def restrict(test: FutureOutcome): FutureOutcome = { + val result: Future[Outcome] = Future.firstCompletedOf( + Seq( + Future.after(testTimeout, ()).map(_ => fail(s"Test didn't finish within $testTimeout.")), + test.toFuture + ) + ) + new FutureOutcome(result) + } + + abstract override def withFixture(test: NoArgAsyncTest): FutureOutcome = { + super.withFixture(new TimeLimitedTest(test)) + } +} + +trait AsyncUtilities { this: AsyncTestSuite => + + implicit class FutureAsyncUtilities(future: Future.type) { + def after[T](duration: FiniteDuration, value: T): Future[T] = + Future(Await.result(Future.never, duration)).recover(_ => value) + + def waitFor(duration: FiniteDuration): Future[Unit] = + after(duration, ()) + } + +} + +trait FutureAssertions { this: BaseSpec => + import scala.jdk.FutureConverters._ + + implicit def completionStageToFutureOps[T](future: CompletionStage[T]): FutureAssertionOps[T] = + new FutureAssertionOps(future.asScala) + + implicit def completionStageToFutureDoneOps(future: CompletionStage[Done]): FutureAssertionDoneOps = + new FutureAssertionDoneOps(future.asScala) + + implicit class FutureAssertionOps[T](future: Future[T]) { + + def asserting(f: T => Assertion): Future[Assertion] = + future.map(f) + + def assertingCondition(f: T => Boolean): Future[Assertion] = + future.map(v => assert(f(v))) + + def assertingEqual(expected: => T): Future[Assertion] = + asserting(_ mustEqual expected) + + def assertingTry(f: Try[T] => Assertion): Future[Assertion] = + future.map(Success.apply).recover { case ex => Failure(ex) }.map(f) + + def assertingFailure[Cause <: Throwable : ClassTag]: Future[Assertion] = + future.map(value => fail(s"Expected exception but got $value")).recover { case ex => ex mustBe a[Cause] } + + def assertingFailure[Cause <: Throwable : ClassTag, InnerCause <: Throwable : ClassTag]: Future[Assertion] = + future.map(value => fail(s"Expected exception but got $value")).recover { case ex => + ex mustBe a[Cause] + ex.getCause mustBe a[InnerCause] + } + + def assertingFailure(cause: Throwable): Future[Assertion] = + future.map(value => fail(s"Expected exception but got $value")).recover { case ex => ex mustEqual cause } + + def assertingSuccess: Future[Assertion] = + future.recover(cause => fail(s"Got unexpected exception", cause)).map(_ => Passed) + + def assertTimeout(timeout: FiniteDuration): Future[Assertion] = { + Future.firstCompletedOf( + Seq( + Future(Thread.sleep(timeout.toMillis)).map(_ => throw new TimeoutException(s"Expected timeout after $timeout")), + future.map(value => fail(s"Expected timeout but got $value")) + ) + ).assertingFailure[TimeoutException] + } + } + + implicit class FutureAssertionDoneOps(future: Future[Done]) { + def assertingDone: Future[Assertion] = future.assertingEqual(Done) + } +} + +trait BaseSpec extends Matchers with AsyncMockFactory { + + protected type Assertion = org.scalatest.Assertion + protected val Passed: Assertion = org.scalatest.Succeeded + + override implicit def executionContext: ExecutionContext = ExecutionContext.global +} + +trait AsyncBaseSpec extends BaseSpec with AsyncWordSpecLike with FutureAssertions with AsyncUtilities with TimeLimitedSpec + +trait UnitSpec extends BaseSpec with AnyWordSpecLike with DefaultValues + +trait AsyncUnitSpec extends AsyncBaseSpec with DefaultValues + +trait IntegrationSpec extends AsyncBaseSpec diff --git a/src/test/scala/play/api/cache/redis/test/FakeApplication.scala b/src/test/scala/play/api/cache/redis/test/FakeApplication.scala new file mode 100644 index 00000000..f496134c --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/FakeApplication.scala @@ -0,0 +1,19 @@ +package play.api.cache.redis.test + +import akka.actor.ActorSystem +import play.api.inject.Injector + +trait FakeApplication extends StoppableApplication { + import play.api.Application + import play.api.inject.guice.GuiceApplicationBuilder + + private lazy val theBuilder: GuiceApplicationBuilder = builder + + protected lazy val injector: Injector = theBuilder.injector() + + protected lazy val application: Application = injector.instanceOf[Application] + + implicit protected lazy val system: ActorSystem = injector.instanceOf[ActorSystem] + + protected def builder: GuiceApplicationBuilder = new GuiceApplicationBuilder() +} diff --git a/src/test/scala/play/api/cache/redis/ForAllTestContainer.scala b/src/test/scala/play/api/cache/redis/test/ForAllTestContainer.scala similarity index 54% rename from src/test/scala/play/api/cache/redis/ForAllTestContainer.scala rename to src/test/scala/play/api/cache/redis/test/ForAllTestContainer.scala index 224f0a7c..eddb85e9 100644 --- a/src/test/scala/play/api/cache/redis/ForAllTestContainer.scala +++ b/src/test/scala/play/api/cache/redis/test/ForAllTestContainer.scala @@ -1,11 +1,11 @@ -package play.api.cache.redis +package play.api.cache.redis.test import com.dimafeng.testcontainers.SingleContainer -import org.specs2.specification.BeforeAfterAll +import org.scalatest.{BeforeAndAfterAll, Suite} -trait ForAllTestContainer extends BeforeAfterAll { +trait ForAllTestContainer extends BeforeAndAfterAll {this: Suite => - def newContainer: SingleContainer[_] + protected def newContainer: SingleContainer[_] final protected lazy val container = newContainer diff --git a/src/test/scala/play/api/cache/redis/test/Helpers.scala b/src/test/scala/play/api/cache/redis/test/Helpers.scala new file mode 100644 index 00000000..6d78fb82 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/Helpers.scala @@ -0,0 +1,24 @@ +package play.api.cache.redis.test + +object Helpers { + + object configuration { + import com.typesafe.config.ConfigFactory + import play.api.Configuration + + def default: Configuration = { + Configuration(ConfigFactory.load()) + } + + def fromHocon(hocon: String): Configuration = { + val reference = ConfigFactory.load() + val local = ConfigFactory.parseString(hocon.stripMargin) + Configuration(local.withFallback(reference)) + } + } + + object probe { + + val orElse: OrElseProbe.type = OrElseProbe + } +} diff --git a/src/test/scala/play/api/cache/redis/test/OrElseProbe.scala b/src/test/scala/play/api/cache/redis/test/OrElseProbe.scala new file mode 100644 index 00000000..cfe9c490 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/OrElseProbe.scala @@ -0,0 +1,39 @@ +package play.api.cache.redis.test + +import java.util.concurrent.CompletionStage +import scala.concurrent.Future +import scala.jdk.FutureConverters.FutureOps + + +final class OrElseProbe[T](queue: LazyList[T]) { + + private var called: Int = 0 + private var next = queue + + def calls: Int = called + + def execute(): T = { + called += 1 + val result = next.head + next = next.tail + result + } +} + +object OrElseProbe { + + def const[T](value: T): OrElseProbe[T] = + new OrElseProbe(LazyList.continually(value)) + + def async[T](value: T): OrElseProbe[Future[T]] = + new OrElseProbe(LazyList.continually(Future.successful(value))) + + def asyncJava[T](value: T): OrElseProbe[CompletionStage[T]] = + new OrElseProbe(LazyList.continually(Future.successful(value).asJava)) + + def failing[T](exception: Throwable): OrElseProbe[Future[T]] = + new OrElseProbe(LazyList.continually(Future.failed(exception))) + + def generic[T](values: T*): OrElseProbe[T] = + new OrElseProbe(LazyList(values: _*)) +} diff --git a/src/test/scala/play/api/cache/redis/test/RedisClusterContainer.scala b/src/test/scala/play/api/cache/redis/test/RedisClusterContainer.scala new file mode 100644 index 00000000..28648902 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/RedisClusterContainer.scala @@ -0,0 +1,39 @@ +package play.api.cache.redis.test + +import org.scalatest.Suite +import play.api.Logger + +import scala.concurrent.duration._ + +trait RedisClusterContainer extends RedisContainer { this: Suite => + + private val log = Logger("play.api.cache.redis.test") + + protected def redisMaster = 4 + + protected def redisSlaves = 1 + + protected final def initialPort = 7000 + + private val waitForStart = 4.seconds + + override protected lazy val redisConfig: RedisContainerConfig = + RedisContainerConfig( + redisDockerImage = "grokzen/redis-cluster:7.0.10", + redisMappedPorts = Seq.empty, + redisFixedPorts = 0.until(redisMaster * (redisSlaves + 1)).map(initialPort + _), + redisEnvironment = Map( + "IP" -> "0.0.0.0", + "INITIAL_PORT" -> initialPort.toString, + "MASTERS" -> redisMaster.toString, + "SLAVES_PER_MASTER" -> redisSlaves.toString, + ), + ) + + override def beforeAll(): Unit = { + super.beforeAll() + log.info(s"Waiting for Redis Cluster to start on ${container.containerIpAddress}, will wait for $waitForStart") + Thread.sleep(waitForStart.toMillis) + log.info(s"Finished waiting for Redis Cluster to start on ${container.containerIpAddress}") + } +} diff --git a/src/test/scala/play/api/cache/redis/test/RedisContainer.scala b/src/test/scala/play/api/cache/redis/test/RedisContainer.scala new file mode 100644 index 00000000..448918f7 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/RedisContainer.scala @@ -0,0 +1,24 @@ +package play.api.cache.redis.test + +import com.dimafeng.testcontainers.GenericContainer +import org.scalatest.Suite +import org.testcontainers.containers.FixedHostPortGenericContainer +import org.testcontainers.containers.wait.strategy.Wait + +trait RedisContainer extends ForAllTestContainer { this: Suite => + + protected def redisConfig: RedisContainerConfig + + private lazy val config = redisConfig + + //noinspection ScalaDeprecation + protected override final val newContainer: GenericContainer = { + val container: FixedHostPortGenericContainer[_] = new FixedHostPortGenericContainer(config.redisDockerImage) + container.withExposedPorts(config.redisMappedPorts.map(int2Integer): _*) + config.redisEnvironment.foreach { case (k, v) => container.withEnv(k, v) } + container.waitingFor(Wait.forListeningPorts(config.redisMappedPorts ++ config.redisFixedPorts: _*)) + config.redisFixedPorts.foreach { port => container.withFixedExposedPort(port, port) } + new GenericContainer(container) + } +} + diff --git a/src/test/scala/play/api/cache/redis/RedisContainerConfig.scala b/src/test/scala/play/api/cache/redis/test/RedisContainerConfig.scala similarity index 54% rename from src/test/scala/play/api/cache/redis/RedisContainerConfig.scala rename to src/test/scala/play/api/cache/redis/test/RedisContainerConfig.scala index f5cdb544..6c498eb4 100644 --- a/src/test/scala/play/api/cache/redis/RedisContainerConfig.scala +++ b/src/test/scala/play/api/cache/redis/test/RedisContainerConfig.scala @@ -1,7 +1,8 @@ -package play.api.cache.redis +package play.api.cache.redis.test final case class RedisContainerConfig( redisDockerImage: String, - redisPorts: Seq[Int], + redisMappedPorts: Seq[Int], + redisFixedPorts: Seq[Int], redisEnvironment: Map[String, String], ) diff --git a/src/test/scala/play/api/cache/redis/logging/RedisLogger.scala b/src/test/scala/play/api/cache/redis/test/RedisLogger.scala similarity index 73% rename from src/test/scala/play/api/cache/redis/logging/RedisLogger.scala rename to src/test/scala/play/api/cache/redis/test/RedisLogger.scala index ef54badc..536b794f 100644 --- a/src/test/scala/play/api/cache/redis/logging/RedisLogger.scala +++ b/src/test/scala/play/api/cache/redis/test/RedisLogger.scala @@ -1,4 +1,4 @@ -package play.api.cache.redis.logging +package play.api.cache.redis.test import akka.event.Logging.{InitializeLogger, LoggerInitialized} import akka.event.slf4j.Slf4jLogger @@ -9,11 +9,11 @@ import akka.event.slf4j.Slf4jLogger */ class RedisLogger extends Slf4jLogger { - private def doReceive: PartialFunction[Any, Unit] = { + private val doReceive: PartialFunction[Any, Unit] = { case InitializeLogger(_) => sender() ! LoggerInitialized } - override def receive = { + override def receive: PartialFunction[Any, Unit] = { doReceive.orElse(super.receive) } } diff --git a/src/test/scala/play/api/cache/redis/test/RedisSentinelContainer.scala b/src/test/scala/play/api/cache/redis/test/RedisSentinelContainer.scala new file mode 100644 index 00000000..00a9d087 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/RedisSentinelContainer.scala @@ -0,0 +1,41 @@ +package play.api.cache.redis.test + +import org.scalatest.Suite +import play.api.Logger + +import scala.concurrent.duration.DurationInt + +trait RedisSentinelContainer extends RedisContainer { + this: Suite => + + private val log = Logger("play.api.cache.redis.test") + + protected def nodes: Int = 3 + + protected final def initialPort: Int = 7000 + + protected final def sentinelPort: Int = initialPort - 2000 + + protected def master: String = s"sentinel$initialPort" + + private val waitForStart = 7.seconds + + override protected lazy val redisConfig: RedisContainerConfig = + RedisContainerConfig( + redisDockerImage = "grokzen/redis-cluster:7.0.10", + redisMappedPorts = Seq.empty, + redisFixedPorts = 0.until(nodes).flatMap(i => Seq(initialPort + i, sentinelPort + i)), + redisEnvironment = Map( + "IP" -> "0.0.0.0", + "INITIAL_PORT" -> initialPort.toString, + "SENTINEL" -> "true" + ), + ) + + override def beforeAll(): Unit = { + super.beforeAll() + log.info(s"Waiting for Redis Sentinel to start on ${container.containerIpAddress}, will wait for $waitForStart") + Thread.sleep(waitForStart.toMillis) + log.info(s"Finished waiting for Redis Sentinel to start on ${container.containerIpAddress}") + } +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala b/src/test/scala/play/api/cache/redis/test/RedisSettingsTest.scala similarity index 60% rename from src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala rename to src/test/scala/play/api/cache/redis/test/RedisSettingsTest.scala index fb0424ce..7e46d76a 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala +++ b/src/test/scala/play/api/cache/redis/test/RedisSettingsTest.scala @@ -1,6 +1,8 @@ -package play.api.cache.redis.configuration +package play.api.cache.redis.test -case class RedisSettingsTest( +import play.api.cache.redis.configuration._ + +final case class RedisSettingsTest( invocationContext: String, invocationPolicy: String, timeout: RedisTimeouts, diff --git a/src/test/scala/play/api/cache/redis/test/RedisStandaloneContainer.scala b/src/test/scala/play/api/cache/redis/test/RedisStandaloneContainer.scala new file mode 100644 index 00000000..e09ea24f --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/RedisStandaloneContainer.scala @@ -0,0 +1,16 @@ +package play.api.cache.redis.test + +import org.scalatest.Suite + +trait RedisStandaloneContainer extends RedisContainer { this: Suite => + + protected lazy val defaultPort: Int = 6379 + + override protected lazy val redisConfig: RedisContainerConfig = + RedisContainerConfig( + redisDockerImage = "redis:latest", + redisMappedPorts = Seq(defaultPort), + redisFixedPorts = Seq.empty, + redisEnvironment = Map.empty + ) +} diff --git a/src/test/scala/play/api/cache/redis/test/SimulatedException.scala b/src/test/scala/play/api/cache/redis/test/SimulatedException.scala new file mode 100644 index 00000000..84153c7f --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/SimulatedException.scala @@ -0,0 +1,11 @@ +package play.api.cache.redis.test + +import play.api.cache.redis.TimeoutException + +import scala.util.control.NoStackTrace + +sealed class SimulatedException extends RuntimeException("Simulated failure.") with NoStackTrace { + def asRedis: TimeoutException = TimeoutException(this) +} + +object SimulatedException extends SimulatedException diff --git a/src/test/scala/play/api/cache/redis/test/StoppableApplication.scala b/src/test/scala/play/api/cache/redis/test/StoppableApplication.scala new file mode 100644 index 00000000..6c2c392b --- /dev/null +++ b/src/test/scala/play/api/cache/redis/test/StoppableApplication.scala @@ -0,0 +1,45 @@ +package play.api.cache.redis.test + +import akka.Done +import akka.actor.{ActorSystem, CoordinatedShutdown} +import org.scalatest.Assertion +import play.api.inject.ApplicationLifecycle + +import scala.concurrent.{ExecutionContext, Future} +import scala.util.{Failure, Success} + +trait StoppableApplication extends ApplicationLifecycle { + + private var hooks: List[() => Future[_]] = Nil + + protected def system: ActorSystem + + def shutdownAsync(): Future[Done] = + CoordinatedShutdown(system).run(CoordinatedShutdown.UnknownReason) + + def runAsyncInApplication(block: => Future[Assertion])(implicit ec: ExecutionContext): Future[Assertion] = { + block.map(Success(_)).recover(Failure(_)) + .flatMap(result => Future.sequence(hooks.map(_.apply())).map(_ => result)) + .flatMap(result => shutdownAsync().map(_ => result)) + .flatMap(result => system.terminate().map(_ => result)) + .flatMap(Future.fromTry) + } + + final def runInApplication(block: => Assertion)(implicit ec: ExecutionContext): Future[Assertion] = { + runAsyncInApplication(Future(block)) + } + + final override def addStopHook(hook: () => Future[_]): Unit = + hooks = hook :: hooks + + final override def stop(): Future[_] = Future.unit + +} + +object StoppableApplication { + + def apply(actorSystem: ActorSystem): StoppableApplication = + new StoppableApplication { + override protected def system: ActorSystem = actorSystem + } +}