diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 215b0d6..eac2e3f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 15 env: - SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC + SBT_OPTS: "-Dfile.encoding=UTF-8 -Duser.timezone=UTC -Xmx2g" steps: - name: Checkout code uses: actions/checkout@v4 @@ -31,8 +31,8 @@ jobs: jvm: adoptium:1.11 - name: Build - timeout-minutes: 10 + timeout-minutes: 15 env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - sbt --client "+clean; +compile; +Test/compile; lint; +coverage; +test; +coverageReport; +coveralls;" + sbt -v "+clean; +compile; +Test/compile; lint; +coverage; +test; +coverageReport; +coveralls;" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9807381..01c2dc1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,7 +13,7 @@ jobs: timeout-minutes: 15 environment: "Generally Available" env: - SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC + SBT_OPTS: "-Dfile.encoding=UTF-8 -Duser.timezone=UTC -Xmx2g" steps: - name: Checkout code uses: actions/checkout@v4 @@ -36,10 +36,10 @@ jobs: echo $PGP_SECRET | base64 --decode | gpg --batch --import - name: Test and Publish JARs - timeout-minutes: 10 + timeout-minutes: 15 env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} run: | - sbt --client "+test; +publishSigned; sonatypeBundleRelease;" + sbt -v "+test; +publishSigned; sonatypeBundleRelease;" diff --git a/CHANGELOG.md b/CHANGELOG.md index 529fe8c..83c251f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Migration to Pekko and Play 3.0, thanks to @TomJKing for help in [#272](https://github.com/KarelCemus/play-redis/pull/272), finished in [#278](https://github.com/KarelCemus/play-redis/pull/278) +Added support to Scala 3 in [#264](https://github.com/KarelCemus/play-redis/issues/264) + ### [:link: 3.0.0](https://github.com/KarelCemus/play-redis/tree/3.0.0-M1) Updated to Play `2.9.0` and dropped `Scala 2.12` since it was discontinued from the Play framework. diff --git a/README.md b/README.md index 55c38e8..77d9ee8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Redis Cache module for Play framework - **This version supports Play framework 3.0.x with JDK 11 and Scala 2.13. Scala 3 on the roadmap.**
+ **This version supports Play framework 3.0.x with JDK 11 and Scala 2.13 and Scala 3.**
**For previous versions see older releases.** [![Travis CI: Status](https://travis-ci.org/KarelCemus/play-redis.svg?branch=master)](https://travis-ci.org/KarelCemus/play-redis) diff --git a/build.sbt b/build.sbt index da353ca..d8e4e14 100644 --- a/build.sbt +++ b/build.sbt @@ -11,11 +11,11 @@ description := "Redis cache plugin for the Play framework 2" organization := "com.github.karelcemus" -crossScalaVersions := Seq("2.13.12") //, "3.3.0" +crossScalaVersions := Seq("2.13.12", "3.3.1") scalaVersion := crossScalaVersions.value.head -playVersion := "3.0.0" +playVersion := "3.0.1" libraryDependencies ++= Seq( // play framework cache API @@ -23,8 +23,8 @@ libraryDependencies ++= Seq( // redis connector "io.github.rediscala" %% "rediscala" % "1.14.0-pekko", // test framework with mockito extension - "org.scalatest" %% "scalatest" % "3.2.17" % Test, - "org.scalamock" %% "scalamock" % "5.2.0" % Test, + "org.scalatest" %% "scalatest" % "3.2.18" % Test, + "org.scalamock" %% "scalamock" % "6.0.0-M1" % Test, // test module for play framework "org.playframework" %% "play-test" % playVersion.value % Test, // to run integration tests @@ -37,7 +37,11 @@ resolvers ++= Seq( javacOptions ++= Seq("-Xlint:unchecked", "-encoding", "UTF-8") -scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked", "-Ywarn-unused") +scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked") + +scalacOptions ++= { + if (scalaVersion.value.startsWith("2.")) Seq("-Ywarn-unused") else Seq.empty +} enablePlugins(CustomReleasePlugin) @@ -47,7 +51,6 @@ coverageExcludedFiles := ".*exceptions.*" Test / test := (Test / testOnly).toTask(" * -- -l \"org.scalatest.Ignore\"").value semanticdbEnabled := true -semanticdbOptions += "-P:semanticdb:synthetics:on" semanticdbVersion := scalafixSemanticdb.revision ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value) @@ -57,6 +60,7 @@ wartremoverWarnings ++= Warts.allBut( Wart.AsInstanceOf, Wart.AutoUnboxing, Wart.DefaultArguments, + Wart.FinalVal, Wart.GlobalExecutionContext, Wart.ImplicitConversion, Wart.ImplicitParameter, diff --git a/src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala b/src/main/scala-2.13/RedisSetJavaImpl.scala similarity index 100% rename from src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala rename to src/main/scala-2.13/RedisSetJavaImpl.scala diff --git a/src/main/scala-3/play/api/cache/redis/impl/RedisSetJavaImpl.scala b/src/main/scala-3/play/api/cache/redis/impl/RedisSetJavaImpl.scala new file mode 100644 index 0000000..2730576 --- /dev/null +++ b/src/main/scala-3/play/api/cache/redis/impl/RedisSetJavaImpl.scala @@ -0,0 +1,31 @@ +package play.api.cache.redis.impl + +import play.api.cache.redis.RedisSet +import play.cache.redis.AsyncRedisSet + +import scala.concurrent.Future + +class RedisSetJavaImpl[Elem](internal: RedisSet[Elem, Future])(implicit runtime: RedisRuntime) extends AsyncRedisSet[Elem] { + import JavaCompatibility.* + + override def add(elements: Array[? <: Elem]): CompletionStage[AsyncRedisSet[Elem]] = + async { implicit context => + internal.add(elements.toSeq: _*).map(_ => this) + } + + override def contains(element: Elem): CompletionStage[java.lang.Boolean] = + async { implicit context => + internal.contains(element).map(Boolean.box) + } + + override def remove(elements: Array[? <: Elem]): CompletionStage[AsyncRedisSet[Elem]] = + async { implicit context => + internal.remove(elements.toSeq: _*).map(_ => this) + } + + override def toSet: CompletionStage[JavaSet[Elem]] = + async { implicit context => + internal.toSet.map(_.asJava) + } + +} diff --git a/src/main/scala/play/api/cache/redis/connector/PekkoSerializer.scala b/src/main/scala/play/api/cache/redis/connector/PekkoSerializer.scala index 54c7c70..37ddc94 100644 --- a/src/main/scala/play/api/cache/redis/connector/PekkoSerializer.scala +++ b/src/main/scala/play/api/cache/redis/connector/PekkoSerializer.scala @@ -129,6 +129,7 @@ private[connector] class PekkoDecoder(serializer: Serialization) { Base64.getDecoder.decode(base64) /** deserializes the binary stream into the object */ + @SuppressWarnings(Array("org.wartremover.warts.RedundantAsInstanceOf")) private def binaryToAnyRef[T](binary: Array[Byte])(implicit classTag: ClassTag[T]): AnyRef = serializer.deserialize(binary, classTag.runtimeClass.asInstanceOf[Class[? <: AnyRef]]).get diff --git a/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala b/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala index 85e6dd9..0444d8d 100644 --- a/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala +++ b/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala @@ -256,7 +256,7 @@ private[connector] class RedisConnectorImpl(serializer: PekkoSerializer, redis: case length => log.debug(s"Inserted $value into the list at '$key'. New size is $length.") Some(length) - } recover [Option[Long]] { + } recover { case ExecutionFailedException(_, _, _, ex) if ex.getMessage startsWith "WRONGTYPE" => log.warn(s"Value at '$key' is not a list.") throw new IllegalArgumentException(s"Value at '$key' is not a list.") 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 6bd6837..0c36538 100644 --- a/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala +++ b/src/main/scala/play/api/cache/redis/impl/InvocationPolicy.scala @@ -19,7 +19,7 @@ sealed trait InvocationPolicy { object EagerInvocation extends InvocationPolicy { override def invoke[T](f: => Future[Any], thenReturn: T)(implicit context: ExecutionContext): Future[T] = { - f: Unit + val _ = f Future successful thenReturn } diff --git a/src/main/scala/play/api/cache/redis/impl/JavaCompatibility.scala b/src/main/scala/play/api/cache/redis/impl/JavaCompatibility.scala index 5a3c9dc..7b64c93 100644 --- a/src/main/scala/play/api/cache/redis/impl/JavaCompatibility.scala +++ b/src/main/scala/play/api/cache/redis/impl/JavaCompatibility.scala @@ -21,7 +21,7 @@ private[impl] object JavaCompatibility extends JavaCompatibilityBase { def apply[T](values: T*): JavaList[T] = { val list = new java.util.ArrayList[T]() - list.addAll(values.asJava): Unit + val _ = list.addAll(values.asJava) list } diff --git a/src/main/scala/play/api/cache/redis/package.scala b/src/main/scala/play/api/cache/redis/package.scala index ec73bbe..c9b2e40 100644 --- a/src/main/scala/play/api/cache/redis/package.scala +++ b/src/main/scala/play/api/cache/redis/package.scala @@ -21,8 +21,8 @@ package object redis extends AnyRef with ExpirationImplicits with ExceptionImpli } @SuppressWarnings(Array("org.wartremover.warts.Equals")) - implicit final class HigherKindedAnyOps[F[_]](private val self: F[?]) extends AnyVal { - def =~=(other: F[?]): Boolean = self == other + implicit final class HigherKindedAnyOps[F[_], A](private val self: F[A]) extends AnyVal { + def =~=[T](other: F[T]): Boolean = self == other } } diff --git a/src/test/scala-2.13/play/api/cache/redis/connector/RedisCommandsMock.scala b/src/test/scala-2.13/play/api/cache/redis/connector/RedisCommandsMock.scala new file mode 100644 index 0000000..d83e201 --- /dev/null +++ b/src/test/scala-2.13/play/api/cache/redis/connector/RedisCommandsMock.scala @@ -0,0 +1,28 @@ +package play.api.cache.redis.connector + +import org.scalamock.scalatest.AsyncMockFactory +import redis.{ByteStringSerializer, RedisCommands} + +import scala.concurrent.Future + +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] + + final override def zrem[V: ByteStringSerializer](key: String, members: V*): Future[Long] = + zremMock(key, members) + + def zremMock[V: ByteStringSerializer](key: String, members: Seq[V]): Future[Long] +} + +private object RedisCommandsMock { + + def mock(factory: AsyncMockFactory): (RedisCommands, RedisCommandsMock) = { + val mock = factory.mock[RedisCommandsMock](factory) + (mock, mock) + } + +} diff --git a/src/test/scala-2.13/play/api/cache/redis/impl/AsyncRedisMock.scala b/src/test/scala-2.13/play/api/cache/redis/impl/AsyncRedisMock.scala new file mode 100644 index 0000000..2c3dfbc --- /dev/null +++ b/src/test/scala-2.13/play/api/cache/redis/impl/AsyncRedisMock.scala @@ -0,0 +1,28 @@ +package play.api.cache.redis.impl + +import org.scalamock.scalatest.AsyncMockFactory +import play.api.cache.redis.{AsynchronousResult, Done} + +import scala.reflect.ClassTag + +private 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](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]] +} + +private object AsyncRedisMock { + + def mock(factory: AsyncMockFactory): (AsyncRedis, AsyncRedisMock) = { + val mock = factory.mock[AsyncRedisMock](factory) + (mock, mock) + } + +} diff --git a/src/test/scala-3/play/api/cache/redis/connector/RedisCommandsMock.scala b/src/test/scala-3/play/api/cache/redis/connector/RedisCommandsMock.scala new file mode 100644 index 0000000..acd8439 --- /dev/null +++ b/src/test/scala-3/play/api/cache/redis/connector/RedisCommandsMock.scala @@ -0,0 +1,118 @@ +package play.api.cache.redis.connector + +import org.scalamock.scalatest.AsyncMockFactory +import redis.* +import redis.api.ListPivot +import redis.protocol.RedisReply + +import scala.concurrent.{ExecutionContext, Future} + +// this is implemented due to a bug in ScalaMock 6.0.0-M1 and reported as https://github.com/paulbutcher/ScalaMock/issues/503 +private trait RedisCommandsMock { + + def get[R: ByteStringDeserializer](key: String): Future[Option[R]] + + def set[V: ByteStringSerializer](key: String, value: V, exSeconds: Option[Long], pxMilliseconds: Option[Long], NX: Boolean, XX: Boolean): Future[Boolean] + + def expire(key: String, seconds: Long): Future[Boolean] + + def mset[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] + + def msetnx[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] + + def incrby(key: String, increment: Long): Future[Long] + + def lrange[R: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[R]] + + def lrem[V: ByteStringSerializer](key: String, count: Long, value: V): Future[Long] + + def ltrim(key: String, start: Long, stop: Long): Future[Boolean] + + def linsert[V: ByteStringSerializer](key: String, beforeAfter: ListPivot, pivot: String, value: V): Future[Long] + + def hincrby(key: String, field: String, increment: Long): Future[Long] + + def hset[V: ByteStringSerializer](key: String, field: String, value: V): Future[Boolean] + + def zcard(key: String): Future[Long] + + def zscore[V: ByteStringSerializer](key: String, member: V): Future[Option[Double]] + + def zrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] + + def zrevrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] + + def zaddMock[V: ByteStringSerializer](key: String, scoreMembers: Seq[(Double, V)]): Future[Long] + + def zremMock[V: ByteStringSerializer](key: String, members: Seq[V]): Future[Long] +} + +private object RedisCommandsMock { + + def mock(factory: AsyncMockFactory)(implicit ec: ExecutionContext): (RedisCommands, RedisCommandsMock) = { + val mock = factory.mock[RedisCommandsMock](factory) + (new RedisCommandsAdapter(mock), mock) + } + +} + +private class RedisCommandsAdapter(inner: RedisCommandsMock)(implicit override val executionContext: ExecutionContext) extends RedisCommands { + + override def send[T](redisCommand: RedisCommand[? <: RedisReply, T]): Future[T] = + throw new IllegalStateException(s"Uncaught call to mock: $redisCommand") + + final override def get[R: ByteStringDeserializer](key: String): Future[Option[R]] = + inner.get(key) + + final override def set[V: ByteStringSerializer](key: String, value: V, exSeconds: Option[Long], pxMilliseconds: Option[Long], NX: Boolean, XX: Boolean): Future[Boolean] = + inner.set(key, value, exSeconds, pxMilliseconds, NX, XX) + + final override def expire(key: String, seconds: Long): Future[Boolean] = + inner.expire(key, seconds) + + final override def mset[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] = + inner.mset(keysValues) + + final override def msetnx[V: ByteStringSerializer](keysValues: Map[String, V]): Future[Boolean] = + inner.msetnx(keysValues) + + final override def incrby(key: String, increment: Long): Future[Long] = + inner.incrby(key, increment) + + final override def lrange[R: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[R]] = + inner.lrange(key, start, stop) + + final override def lrem[V: ByteStringSerializer](key: String, count: Long, value: V): Future[Long] = + inner.lrem(key, count, value) + + final override def ltrim(key: String, start: Long, stop: Long): Future[Boolean] = + inner.ltrim(key, start, stop) + + final override def linsert[V: ByteStringSerializer](key: String, beforeAfter: ListPivot, pivot: String, value: V): Future[Long] = + inner.linsert(key, beforeAfter, pivot, value) + + final override def hincrby(key: String, field: String, increment: Long): Future[Long] = + inner.hincrby(key, field, increment) + + final override def hset[V: ByteStringSerializer](key: String, field: String, value: V): Future[Boolean] = + inner.hset(key, field, value) + + final override def zcard(key: String): Future[Long] = + inner.zcard(key) + + final override def zscore[V: ByteStringSerializer](key: String, member: V): Future[Option[Double]] = + inner.zscore(key, member) + + final override def zrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] = + inner.zrange(key, start, stop) + + final override def zrevrange[V: ByteStringDeserializer](key: String, start: Long, stop: Long): Future[Seq[V]] = + inner.zrevrange(key, start, stop) + + final override def zadd[V: ByteStringSerializer](key: String, scoreMembers: (Double, V)*): Future[Long] = + inner.zaddMock(key, scoreMembers) + + final override def zrem[V: ByteStringSerializer](key: String, members: V*): Future[Long] = + inner.zremMock(key, members) + +} diff --git a/src/test/scala-3/play/api/cache/redis/impl/AsyncRedisMock.scala b/src/test/scala-3/play/api/cache/redis/impl/AsyncRedisMock.scala new file mode 100644 index 0000000..0f4233b --- /dev/null +++ b/src/test/scala-3/play/api/cache/redis/impl/AsyncRedisMock.scala @@ -0,0 +1,156 @@ +package play.api.cache.redis.impl + +import org.apache.pekko.Done +import org.scalamock.scalatest.AsyncMockFactory +import play.api.cache.redis.* + +import scala.concurrent.duration.Duration +import scala.concurrent.{ExecutionContext, Future} +import scala.reflect.ClassTag + +// this is implemented due to a bug in ScalaMock 6.0.0-M1 and reported as https://github.com/paulbutcher/ScalaMock/issues/503 +private trait AsyncRedisMock { + + def get[A: ClassTag](key: String): Future[Option[A]] + + def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] + + def removeAll(): Future[Done] + + def getAllKeys[T](keys: Iterable[String]): Future[Seq[Option[T]]] + + def getOrElse[T: ClassTag](key: String, expiration: Duration)(orElse: => T): Future[T] + + def getOrFuture[T: ClassTag](key: String, expiration: Duration)(orElse: => Future[T]): Future[T] + + def exists(key: String): Future[Boolean] + + def matching(pattern: String): Future[Seq[String]] + + def set(key: String, value: Any, expiration: Duration): Future[Done] + + def setIfNotExists(key: String, value: Any, expiration: Duration): Future[Boolean] + + def setAll(keyValues: (String, Any)*): Future[Done] + + def setAllIfNotExist(keyValues: (String, Any)*): Future[Boolean] + + def append(key: String, value: String, expiration: Duration): Future[Done] + + def expire(key: String, expiration: Duration): Future[Done] + + def expiresIn(key: String): Future[Option[Duration]] + + def remove(key: String): Future[Done] + + def remove(key1: String, key2: String, keys: String*): Future[Done] + + def removeAllKeys(keys: Seq[String]): Future[Done] + + def removeMatching(pattern: String): Future[Done] + + def invalidate(): Future[Done] + + def increment(key: String, by: Long = 1): Future[Long] + + def decrement(key: String, by: Long = 1): Future[Long] + + def list[T: ClassTag](key: String): RedisList[T, Future] + + def set[T: ClassTag](key: String): RedisSet[T, Future] + + def map[T: ClassTag](key: String): RedisMap[T, Future] + + def zset[T: ClassTag](key: String): RedisSortedSet[T, Future] +} + +private object AsyncRedisMock { + + def mock(factory: AsyncMockFactory)(implicit ec: ExecutionContext): (AsyncRedis, AsyncRedisMock) = { + val mock = factory.mock[AsyncRedisMock](factory) + (new AsyncRedisAdapter(mock), mock) + } + +} + +private class AsyncRedisAdapter(inner: AsyncRedisMock) extends AsyncRedis { + + override def get[A: ClassTag](key: String): Future[Option[A]] = + inner.get(key) + + override def getOrElseUpdate[A: ClassTag](key: String, expiration: Duration)(orElse: => Future[A]): Future[A] = + inner.getOrElseUpdate(key, expiration)(orElse) + + override def removeAll(): Future[Done] = + inner.removeAll() + + override def getAll[T: ClassTag](keys: Iterable[String]): Future[Seq[Option[T]]] = + inner.getAllKeys(keys) + + override def getOrElse[T: ClassTag](key: String, expiration: Duration)(orElse: => T): Future[T] = + inner.getOrElse(key, expiration)(orElse) + + override def getOrFuture[T: ClassTag](key: String, expiration: Duration)(orElse: => Future[T]): Future[T] = + inner.getOrFuture(key, expiration)(orElse) + + override def exists(key: String): Future[Boolean] = + inner.exists(key) + + override def matching(pattern: String): Future[Seq[String]] = + inner.matching(pattern) + + override def set(key: String, value: Any, expiration: Duration): Future[Done] = + inner.set(key, value, expiration) + + override def setIfNotExists(key: String, value: Any, expiration: Duration): Future[Boolean] = + inner.setIfNotExists(key, value, expiration) + + override def setAll(keyValues: (String, Any)*): Future[Done] = + inner.setAll(keyValues: _*) + + override def setAllIfNotExist(keyValues: (String, Any)*): Future[Boolean] = + inner.setAllIfNotExist(keyValues: _*) + + override def append(key: String, value: String, expiration: Duration): Future[Done] = + inner.append(key, value, expiration) + + override def expire(key: String, expiration: Duration): Future[Done] = + inner.expire(key, expiration) + + override def expiresIn(key: String): Future[Option[Duration]] = + inner.expiresIn(key) + + override def remove(key: String): Future[Done] = + inner.remove(key) + + override def remove(key1: String, key2: String, keys: String*): Future[Done] = + inner.remove(key1, key2, keys: _*) + + override def removeAll(keys: String*): Future[Done] = + inner.removeAllKeys(keys) + + override def removeMatching(pattern: String): Future[Done] = + inner.removeMatching(pattern) + + override def invalidate(): Future[Done] = + inner.invalidate() + + override def increment(key: String, by: Long = 1): Future[Long] = + inner.increment(key, by) + + override def decrement(key: String, by: Long = 1): Future[Long] = + inner.decrement(key, by) + + override def list[T: ClassTag](key: String): RedisList[T, Future] = + inner.list(key) + + override def set[T: ClassTag](key: String): RedisSet[T, Future] = + inner.set(key) + + override def map[T: ClassTag](key: String): RedisMap[T, Future] = + inner.map(key) + + override def zset[T: ClassTag](key: String): RedisSortedSet[T, Future] = + inner.zset(key) + +} diff --git a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala index f662aeb..96b4b37 100644 --- a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala +++ b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala @@ -5,8 +5,12 @@ import play.api._ import play.api.cache.redis.test._ import play.api.inject.ApplicationLifecycle +import scala.concurrent.duration._ + class RedisCacheComponentsSpec extends IntegrationSpec with RedisStandaloneContainer { + override protected def testTimeout: FiniteDuration = 3.seconds + private val prefix = "components-sync" test("miss on get") { cache => 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 466d95e..d752db9 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala @@ -244,13 +244,13 @@ class RedisConnectorFailureSpec extends AsyncUnitSpec with ImplicitFutureMateria private def test(name: String)(f: (SerializerAssertions, RedisCommandsMock, RedisConnector) => Future[Assertion]): Unit = name in { implicit val runtime: RedisRuntime = mock[RedisRuntime] - val serializer = mock[PekkoSerializer] - val commands = mock[RedisCommandsMock] + val serializer: PekkoSerializer = mock[PekkoSerializer] + val (commands: RedisCommands, mockedCommands: RedisCommandsMock) = RedisCommandsMock.mock(this) val connector: RedisConnector = new RedisConnectorImpl(serializer, commands) (() => runtime.context).expects().returns(ExecutionContext.global).anyNumberOfTimes() - f(new SerializerAssertions(serializer), commands, connector) + f(new SerializerAssertions(serializer), mockedCommands, connector) } private class SerializerAssertions(mock: PekkoSerializer) { @@ -272,17 +272,4 @@ class RedisConnectorFailureSpec extends AsyncUnitSpec with ImplicitFutureMateria } - 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] - - final override 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/RedisRequestTimeoutSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala index b8053a9..3e94fac 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpec.scala @@ -13,7 +13,7 @@ class RedisRequestTimeoutSpec extends AsyncUnitSpec { override protected def testTimeout: FiniteDuration = 3.seconds "fail long running requests when connected but timeout defined" in { - implicit val system: ActorSystem = ActorSystem("test") + implicit val system: ActorSystem = ActorSystem("test", classLoader = Some(getClass.getClassLoader)) val application = StoppableApplication(system) application.runAsyncInApplication { 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 58ae80e..f91a3b7 100644 --- a/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala @@ -196,5 +196,7 @@ object SerializerSpec { } /** Plain test object to be cached */ + @SerialVersionUID(3363306882840417725L) final private case class SimpleObject(key: String, value: Int) + } 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 15739b0..dc57abf 100644 --- a/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/AsyncJavaRedisSpec.scala @@ -10,7 +10,7 @@ import scala.concurrent.Future import scala.concurrent.duration._ import scala.jdk.CollectionConverters.IterableHasAsScala -class AsyncJavaRedisSpec extends AsyncUnitSpec with AsyncRedisMock with RedisRuntimeMock { +class AsyncJavaRedisSpec extends AsyncUnitSpec with MockedAsyncRedis with RedisRuntimeMock { import Helpers._ private val expiration = 5.seconds @@ -359,10 +359,10 @@ class AsyncJavaRedisSpec extends AsyncUnitSpec with AsyncRedisMock with RedisRun classLoader = getClass.getClassLoader, mode = Mode.Test, ) - val async = mock[AsyncRedisMock] + val (async: AsyncRedis, asyncMock: AsyncRedisMock) = AsyncRedisMock.mock(this) val cache: play.cache.redis.AsyncCacheApi = new AsyncJavaRedis(async) - f(async, cache) + f(asyncMock, cache) } } diff --git a/src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala b/src/test/scala/play/api/cache/redis/impl/MockedAsyncRedis.scala similarity index 93% rename from src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala rename to src/test/scala/play/api/cache/redis/impl/MockedAsyncRedis.scala index ae66047..e09ccba 100644 --- a/src/test/scala/play/api/cache/redis/impl/AsyncRedisMock.scala +++ b/src/test/scala/play/api/cache/redis/impl/MockedAsyncRedis.scala @@ -2,25 +2,13 @@ package play.api.cache.redis.impl import org.scalamock.scalatest.AsyncMockFactoryBase import play.api.cache.redis._ +import play.api.cache.redis.test.ImplicitOptionMaterialization 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](keys: Iterable[String]): AsynchronousResult[Seq[Option[T]]] - } +private[impl] trait MockedAsyncRedis { this: AsyncMockFactoryBase => implicit final protected class AsyncRedisOps(async: AsyncRedisMock) { @@ -29,7 +17,7 @@ private[impl] trait AsyncRedisMock { this: AsyncMockFactoryBase => } - final protected class AsyncRedisExpectation(async: AsyncRedisMock) { + final protected class AsyncRedisExpectation(async: AsyncRedisMock) extends ImplicitOptionMaterialization { private def classTagKey(key: String): String = s"classTag::$key" @@ -45,7 +33,7 @@ private[impl] trait AsyncRedisMock { this: AsyncMockFactoryBase => def get[T: ClassTag](key: String, value: Option[T]): Future[Unit] = Future.successful { (async - .get(_: String)(_: ClassTag[?])) + .get[T](_: String)(_: ClassTag[T])) .expects(key, implicitly[ClassTag[T]]) .returning(Future.successful(value)) .once() diff --git a/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala b/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala index e0061bc..c0dd485 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisConnectorMock.scala @@ -2,6 +2,7 @@ package play.api.cache.redis.impl import org.scalamock.scalatest.AsyncMockFactoryBase import play.api.cache.redis._ +import play.api.cache.redis.test.ImplicitOptionMaterialization import scala.concurrent.Future import scala.concurrent.duration.Duration @@ -90,12 +91,12 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => } - final protected class RedisConnectorExpectation(connector: RedisConnectorMock) { + final protected class RedisConnectorExpectation(connector: RedisConnectorMock) extends ImplicitOptionMaterialization { def get[T: ClassTag](key: String, result: Try[Option[T]]): Future[Unit] = Future.successful { (connector - .get(_: String)(_: ClassTag[?])) + .get(_: String)(_: ClassTag[T])) .expects(key, implicitly[ClassTag[T]]) .returning(Future.fromTry(result)) .once() @@ -110,7 +111,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def mGet[T: ClassTag](keys: Seq[String], result: Future[Seq[Option[T]]]): Future[Unit] = Future.successful { (connector - .mGetKeys(_: Seq[String])(_: ClassTag[?])) + .mGetKeys(_: Seq[String])(_: ClassTag[T])) .expects(keys, implicitly[ClassTag[T]]) .returning(result) .once() @@ -238,7 +239,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def listSlice[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[T]]): Future[Unit] = Future.successful { (connector - .listSlice(_: String, _: Long, _: Long)(_: ClassTag[?])) + .listSlice(_: String, _: Long, _: Long)(_: ClassTag[T])) .expects(key, start, end, implicitly[ClassTag[T]]) .returning(result) .once() @@ -247,7 +248,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def listHeadPop[T: ClassTag](key: String, result: Future[Option[T]]): Future[Unit] = Future.successful { (connector - .listHeadPop(_: String)(_: ClassTag[?])) + .listHeadPop(_: String)(_: ClassTag[T])) .expects(key, implicitly[ClassTag[T]]) .returning(result) .once() @@ -339,10 +340,10 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => .once() } - def setMembers[T: ClassTag](key: String, result: Future[Set[Any]]): Future[Unit] = + def setMembers[T: ClassTag](key: String, result: Future[Set[T]]): Future[Unit] = Future.successful { (connector - .setMembers(_: String)(_: ClassTag[?])) + .setMembers(_: String)(_: ClassTag[T])) .expects(key, implicitly[ClassTag[T]]) .returning(result) .once() @@ -384,19 +385,19 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => .once() } - def sortedSetRange[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[String]]): Future[Unit] = + def sortedSetRange[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[T]]): Future[Unit] = Future.successful { (connector - .sortedSetRange(_: String, _: Long, _: Long)(_: ClassTag[?])) + .sortedSetRange(_: String, _: Long, _: Long)(_: ClassTag[T])) .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] = + def sortedSetReverseRange[T: ClassTag](key: String, start: Long, end: Long, result: Future[Seq[T]]): Future[Unit] = Future.successful { (connector - .sortedSetReverseRange(_: String, _: Long, _: Long)(_: ClassTag[?])) + .sortedSetReverseRange(_: String, _: Long, _: Long)(_: ClassTag[T])) .expects(key, start, end, implicitly[ClassTag[T]]) .returning(result) .once() @@ -423,7 +424,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def hashGet[T: ClassTag](key: String, field: String, result: Future[Option[T]]): Future[Unit] = Future.successful { (connector - .hashGetField(_: String, _: String)(_: ClassTag[?])) + .hashGetField(_: String, _: String)(_: ClassTag[T])) .expects(key, field, implicitly[ClassTag[T]]) .returning(result) .once() @@ -432,7 +433,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def hashGet[T: ClassTag](key: String, fields: Seq[String], result: Future[Seq[Option[T]]]): Future[Unit] = Future.successful { (connector - .hashGetFields(_: String, _: Seq[String])(_: ClassTag[?])) + .hashGetFields(_: String, _: Seq[String])(_: ClassTag[T])) .expects(key, fields, implicitly[ClassTag[T]]) .returning(result) .once() @@ -468,7 +469,7 @@ private[impl] trait RedisConnectorMock { this: AsyncMockFactoryBase => def hashGetAll[T: ClassTag](key: String, result: Future[Map[String, T]]): Future[Unit] = Future.successful { (connector - .hashGetAllValues(_: String)(_: ClassTag[?])) + .hashGetAllValues(_: String)(_: ClassTag[T])) .expects(key, implicitly[ClassTag[T]]) .returning(result) .once() 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 c732141..5d4a348 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisSetSpec.scala @@ -58,7 +58,7 @@ class RedisSetSpec extends AsyncUnitSpec with RedisRuntimeMock with RedisConnect test("toSet") { (set, connector) => for { - _ <- connector.expect.setMembers[String](cacheKey, result = Set[Any](cacheValue, otherValue)) + _ <- connector.expect.setMembers[String](cacheKey, result = Set(cacheValue, otherValue)) _ <- set.toSet.assertingEqual(Set(cacheValue, otherValue)) } yield Passed } diff --git a/src/test/scala/play/api/cache/redis/test/BaseSpec.scala b/src/test/scala/play/api/cache/redis/test/BaseSpec.scala index cac81cb..3dd549f 100644 --- a/src/test/scala/play/api/cache/redis/test/BaseSpec.scala +++ b/src/test/scala/play/api/cache/redis/test/BaseSpec.scala @@ -96,7 +96,7 @@ trait AsyncUtilities { this: AsyncTestSuite => } -trait FutureAssertions extends AsyncUtilities { this: BaseSpec => +trait FutureAssertions extends AsyncUtilities { this: AsyncBaseSpec => import scala.jdk.FutureConverters._ implicit def completionStageToFutureOps[T](future: CompletionStage[T]): FutureAssertionOps[T] = @@ -152,16 +152,17 @@ trait FutureAssertions extends AsyncUtilities { this: BaseSpec => } -trait BaseSpec extends Matchers with AsyncMockFactory { +trait BaseSpec extends Matchers { protected type Assertion = org.scalatest.Assertion protected val Passed: Assertion = org.scalatest.Succeeded +} + +trait AsyncBaseSpec extends BaseSpec with AsyncWordSpecLike with AsyncMockFactory with FutureAssertions with AsyncUtilities with TimeLimitedSpec { implicit override 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