diff --git a/CHANGELOG.md b/CHANGELOG.md
index 491cb50c..6e974396 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@ Standalone client now fails eagerly when the connection to redis is not
established. This is to avoid long timeout while the rediscala is trying
to reconnect. [#147](https://github.com/KarelCemus/play-redis/issues/147)
-Deprecated`timeout` property and replaced by `sync-timeout` with the identical
+Deprecated `timeout` property and replaced by `sync-timeout` with the identical
meaning and use. Will be removed by 2.2.0. [#154](https://github.com/KarelCemus/play-redis/issues/154)
Introduced **optional** `redis-timeout` property indicating timeout on redis queries.
@@ -22,6 +22,8 @@ See [the configuration]() for more details. [#154](https://github.com/KarelCemus
Rediscala bumped to 1.8.3 and subsequently Akka bumped to 2.5.6 [#150](https://github.com/KarelCemus/play-redis/issues/150).
+Revamped tests, reduced their number but increased value and code coverage [#108](https://github.com/KarelCemus/play-redis/issues/108)
+
#### Removal of `@Named` and introduction of `@NamedCache`
Named caches now uses `@NamedCache` instead of `@Named` to be consistent with Play's EhCache and
diff --git a/build.sbt b/build.sbt
index 8b7592ba..37a7ffa5 100644
--- a/build.sbt
+++ b/build.sbt
@@ -15,14 +15,12 @@ scalaVersion := "2.12.5"
crossScalaVersions := Seq( "2.11.12", scalaVersion.value )
-val playVersion = "2.6.12"
+val playVersion = "2.6.13"
val connectorVersion = "1.8.3"
val specs2Version = "4.0.3"
-parallelExecution in Test := false
-
libraryDependencies ++= Seq(
// play framework cache API
"com.typesafe.play" %% "play-cache" % playVersion % Provided,
@@ -31,7 +29,9 @@ libraryDependencies ++= Seq(
// test framework
"org.specs2" %% "specs2-core" % specs2Version % Test,
// test module for play framework
- "com.typesafe.play" %% "play-specs2" % playVersion % Test
+ "com.typesafe.play" %% "play-specs2" % playVersion % Test,
+ // mockito framework
+ "org.mockito" % "mockito-core" % "2.18.0"
)
resolvers ++= Seq(
@@ -71,3 +71,6 @@ publishTo := {
if (isSnapshot.value) Some(Opts.resolver.sonatypeSnapshots)
else Some( Opts.resolver.sonatypeStaging )
}
+
+// exclude from tests coverage
+coverageExcludedFiles := ".*exceptions.*"
diff --git a/project/build.properties b/project/build.properties
index 31334bbd..05313438 100644
--- a/project/build.properties
+++ b/project/build.properties
@@ -1 +1 @@
-sbt.version=1.1.1
+sbt.version=1.1.2
diff --git a/src/main/scala/play/api/cache/redis/RecoveryPolicy.scala b/src/main/scala/play/api/cache/redis/RecoveryPolicy.scala
index 7332fe76..9d18507d 100644
--- a/src/main/scala/play/api/cache/redis/RecoveryPolicy.scala
+++ b/src/main/scala/play/api/cache/redis/RecoveryPolicy.scala
@@ -16,9 +16,6 @@ import play.api.inject._
*/
trait RecoveryPolicy {
- /** name of the policy used for internal purposes */
- def name: String = this.getClass.getSimpleName
-
/** When a failure occurs, this method handles it. It may re-run it, return default value,
* log it or propagate the exception.
*
@@ -30,7 +27,12 @@ trait RecoveryPolicy {
*/
def recoverFrom[ T ]( rerun: => Future[ T ], default: => Future[ T ], failure: RedisException ): Future[ T ]
+ // $COVERAGE-OFF$
+ /** name of the policy used for internal purposes */
+ def name: String = this.getClass.getSimpleName
+
override def toString = s"RecoveryPolicy($name)"
+ // $COVERAGE-ON$
}
/** Abstract recovery policy provides a general helpers for
@@ -47,8 +49,8 @@ trait Reports extends RecoveryPolicy {
protected def message( failure: RedisException ): String = failure match {
case TimeoutException( cause ) => s"Command execution timed out."
case SerializationException( key, message, cause ) => s"$message for key '$key'."
- case ExecutionFailedException( Some( key ), command, cause ) => s"Command $command for key '$key' failed."
- case ExecutionFailedException( None, command, cause ) => s"Command $command failed."
+ case ExecutionFailedException( Some( key ), command, statement, cause ) => s"Command $command for key '$key' failed."
+ case ExecutionFailedException( None, command, statement, cause ) => s"Command $command failed."
case UnexpectedResponseException( Some( key ), command ) => s"Command $command for key '$key' returned unexpected response."
case UnexpectedResponseException( None, command ) => s"Command $command returned unexpected response."
}
@@ -98,7 +100,7 @@ trait FailThrough extends RecoveryPolicy {
override def recoverFrom[ T ]( rerun: => Future[ T ], default: => Future[ T ], failure: RedisException ): Future[ T ] = {
// fail through
- throw failure
+ Future.failed( failure )
}
}
@@ -159,6 +161,8 @@ trait RecoveryPolicyResolver {
def resolve: PartialFunction[ String, RecoveryPolicy ]
}
+// $COVERAGE-OFF$
+
class RecoveryPolicyResolverImpl extends RecoveryPolicyResolver {
val resolve: PartialFunction[ String, RecoveryPolicy ] = {
case "log-and-fail" => new LogAndFailPolicy
@@ -187,3 +191,5 @@ class RecoveryPolicyResolverGuice @Inject( )( injector: Injector ) extends Recov
case name => injector instanceOf bind[ RecoveryPolicy ].qualifiedWith( name )
}
}
+
+// $COVERAGE-ON$
diff --git a/src/main/scala/play/api/cache/redis/RedisCacheComponents.scala b/src/main/scala/play/api/cache/redis/RedisCacheComponents.scala
index f13a2cd7..4289dd34 100644
--- a/src/main/scala/play/api/cache/redis/RedisCacheComponents.scala
+++ b/src/main/scala/play/api/cache/redis/RedisCacheComponents.scala
@@ -34,10 +34,6 @@ trait RedisCacheComponents
private lazy val akkaSerializer: connector.AkkaSerializer = new connector.AkkaSerializerProvider().get
- private def hasInstances = configuration.underlying.hasPath( "play.cache.redis.instances" )
-
- private def defaultCache = configuration.underlying.getString( "play.cache.redis.default-cache" )
-
private lazy val manager = configuration.get( "play.cache.redis" )( play.api.cache.redis.configuration.RedisInstanceManager )
/** translates the cache name into the configuration */
diff --git a/src/main/scala/play/api/cache/redis/configuration/Equals.scala b/src/main/scala/play/api/cache/redis/configuration/Equals.scala
new file mode 100644
index 00000000..811881a9
--- /dev/null
+++ b/src/main/scala/play/api/cache/redis/configuration/Equals.scala
@@ -0,0 +1,14 @@
+package play.api.cache.redis.configuration
+
+/**
+ * @author Karel Cemus
+ */
+private[ configuration ] object Equals {
+
+ // $COVERAGE-OFF$
+ @inline
+ def check[ T ]( a: T, b: T )( property: ( T => Any )* ): Boolean = {
+ property.forall( property => property( a ) == property( b ) )
+ }
+ // $COVERAGE-ON$
+}
diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisConfigLoader.scala b/src/main/scala/play/api/cache/redis/configuration/RedisConfigLoader.scala
index 1e1e0133..ea6e1723 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisConfigLoader.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisConfigLoader.scala
@@ -22,10 +22,6 @@ private[ configuration ] object RedisConfigLoader {
def /( suffix: String ): String = if ( path == "" ) suffix else s"$path.$suffix"
}
- implicit class FallbackValue[ T ]( val value: T ) extends AnyVal {
- def asFallback = ( _: String ) => value
- }
-
def required( path: String ) = throw new IllegalStateException( s"Configuration key '$path' is missing." )
}
@@ -39,10 +35,6 @@ private[ configuration ] object RedisConfigLoader {
*/
private[ configuration ] trait RedisConfigLoader[ T ] { outer =>
- implicit final def loader( implicit defaults: RedisSettings ) = new ConfigLoader[ T ] {
- def load( config: Config, path: String ) = outer.load( config, path )
- }
-
def load( config: Config, path: String )( implicit defaults: RedisSettings ): T
}
@@ -56,9 +48,5 @@ private[ configuration ] trait RedisConfigLoader[ T ] { outer =>
*/
private[ configuration ] trait RedisConfigInstanceLoader[ T ] { outer =>
- final def loader( name: String )( implicit defaults: RedisSettings ) = new ConfigLoader[ T ] {
- def load( config: Config, path: String ) = outer.load( config, path = path, name = name )
- }
-
def load( config: Config, path: String, name: String )( implicit defaults: RedisSettings ): T
}
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 34c5beb0..d7a17dd7 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala
@@ -18,11 +18,12 @@ trait RedisHost {
def database: Option[ Int ]
/** when enabled security, this returns password for the AUTH command */
def password: Option[ String ]
+ // $COVERAGE-OFF$
/** trait-specific equals */
override def equals( obj: scala.Any ) = equalsAsHost( obj )
/** trait-specific equals, invokable from children */
protected def equalsAsHost( obj: scala.Any ) = obj match {
- case that: RedisHost => this.host == that.host && this.port == that.port && this.database == that.database && this.password == that.password
+ case that: RedisHost => Equals.check( this, that )( _.host, _.port, _.database, _.password )
case _ => false
}
/** to string */
@@ -32,6 +33,7 @@ trait RedisHost {
case ( None, Some( database ) ) => s"redis://$host:$port?db=$database"
case ( None, None ) => s"redis://$host:$port"
}
+ // $COVERAGE-ON$
}
@@ -72,8 +74,11 @@ object RedisHost extends ConfigLoader[ RedisHost ] {
val password = _password
}
- def unapply( host: RedisHost ): Option[ (String, Int, Option[ Int ], Option[ String ]) ] =
+ // $COVERAGE-OFF$
+ def unapply( host: RedisHost ): Option[ (String, Int, Option[ Int ], Option[ String ]) ] = {
Some( (host.host, host.port, host.database, host.password) )
+ }
+ // $COVERAGE-ON$
}
/**
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 b32d707f..d6b0bf59 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala
@@ -9,6 +9,7 @@ package play.api.cache.redis.configuration
sealed trait RedisInstance extends RedisSettings {
/** name of the redis instance */
def name: String
+ // $COVERAGE-OFF$
/** trait-specific equals */
override def equals( obj: scala.Any ) = equalsAsInstance( obj )
/** trait-specific equals, invokable from children */
@@ -16,6 +17,7 @@ sealed trait RedisInstance extends RedisSettings {
case that: RedisInstance => this.name == that.name && equalsAsSettings( that )
case _ => false
}
+ // $COVERAGE-ON$
}
/**
@@ -27,6 +29,7 @@ sealed trait RedisInstance extends RedisSettings {
trait RedisCluster extends RedisInstance {
/** nodes definition when cluster is defined */
def nodes: List[ RedisHost ]
+ // $COVERAGE-OFF$
/** trait-specific equals */
override def equals( obj: scala.Any ) = obj match {
case that: RedisCluster => equalsAsInstance( that ) && this.nodes == that.nodes
@@ -34,6 +37,7 @@ trait RedisCluster extends RedisInstance {
}
/** to string */
override def toString = s"Cluster[${ nodes mkString "," }]"
+ // $COVERAGE-ON$
}
object RedisCluster {
@@ -57,6 +61,7 @@ object RedisCluster {
* @author Karel Cemus
*/
trait RedisStandalone extends RedisInstance with RedisHost {
+ // $COVERAGE-OFF$
/** trait-specific equals */
override def equals( obj: scala.Any ) = obj match {
case that: RedisStandalone => equalsAsInstance( that ) && equalsAsHost( that )
@@ -67,6 +72,7 @@ trait RedisStandalone extends RedisInstance with RedisHost {
case Some( database ) => s"Standalone($name@$host:$port?db=$database)"
case None => s"Standalone($name@$host:$port)"
}
+ // $COVERAGE-ON$
}
object RedisStandalone {
diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisInstanceManager.scala b/src/main/scala/play/api/cache/redis/configuration/RedisInstanceManager.scala
index 591b1d5b..67239c4d 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisInstanceManager.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisInstanceManager.scala
@@ -36,6 +36,13 @@ trait RedisInstanceManager extends Traversable[ RedisInstanceProvider ] {
/** traverse all binders */
def foreach[ U ]( f: RedisInstanceProvider => U ) = caches.view.flatMap( instanceOfOption ).foreach( f )
+
+ // $COVERAGE-OFF$
+ override def equals( obj: scala.Any ) = obj match {
+ case that: RedisInstanceManager => Equals.check( this, that )( _.caches, _.defaultInstance, _.toSet )
+ case _ => false
+ }
+ // $COVERAGE-ON$
}
private[ redis ] object RedisInstanceManager extends ConfigLoader[ RedisInstanceManager ] {
diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisInstanceProvider.scala b/src/main/scala/play/api/cache/redis/configuration/RedisInstanceProvider.scala
index e970f1da..a865c478 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisInstanceProvider.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisInstanceProvider.scala
@@ -16,13 +16,35 @@ sealed trait RedisInstanceProvider extends Any {
def resolved( implicit resolver: RedisInstanceResolver ): RedisInstance
}
-class ResolvedRedisInstance( val instance: RedisInstance ) extends AnyVal with RedisInstanceProvider {
+class ResolvedRedisInstance( val instance: RedisInstance ) extends RedisInstanceProvider {
def name: String = instance.name
def resolved( implicit resolver: RedisInstanceResolver ) = instance
+
+ // $COVERAGE-OFF$
+ override def equals( obj: scala.Any ) = obj match {
+ case that: ResolvedRedisInstance => this.name == that.name && this.instance == that.instance
+ case _ => false
+ }
+
+ override def hashCode( ) = name.hashCode
+
+ override def toString = s"ResolvedRedisInstance($name@$instance)"
+ // $COVERAGE-ON$
}
-class UnresolvedRedisInstance( val name: String ) extends AnyVal with RedisInstanceProvider {
+class UnresolvedRedisInstance( val name: String ) extends RedisInstanceProvider {
def resolved( implicit resolver: RedisInstanceResolver ) = resolver resolve name
+
+ // $COVERAGE-OFF$
+ override def equals( obj: scala.Any ) = obj match {
+ case that: UnresolvedRedisInstance => this.name == that.name
+ case _ => false
+ }
+
+ override def hashCode( ) = name.hashCode
+
+ override def toString = s"UnresolvedRedisInstance($name)"
+ // $COVERAGE-ON$
}
private[ configuration ] object RedisInstanceProvider extends RedisConfigInstanceLoader[ RedisInstanceProvider ] {
@@ -39,7 +61,7 @@ private[ configuration ] object RedisInstanceProvider extends RedisConfigInstanc
// supplied custom configuration
case "custom" => RedisInstanceCustom
// found but unrecognized
- case other => invalidConfiguration( config.getConfig( path / "source" ).origin(), other )
+ case other => invalidConfiguration( config.getValue( path / "source" ).origin(), other )
}
}.load( config, path, name )
diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisSettings.scala b/src/main/scala/play/api/cache/redis/configuration/RedisSettings.scala
index d1d57d6c..3dae9cf3 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisSettings.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisSettings.scala
@@ -23,13 +23,15 @@ trait RedisSettings {
def source: String
/** instance prefix */
def prefix: Option[ String ]
+ // $COVERAGE-OFF$
/** trait-specific equals */
override def equals( obj: scala.Any ) = equalsAsSettings( obj )
/** trait-specific equals, invokable from children */
protected def equalsAsSettings( obj: scala.Any ) = obj match {
- case that: RedisSettings => this.invocationContext == that.invocationContext && this.timeout == that.timeout && this.recovery == that.recovery && this.source == that.source
+ case that: RedisSettings => Equals.check( this, that )( _.invocationContext, _.invocationPolicy, _.timeout, _.recovery, _.source, _.prefix )
case _ => false
}
+ // $COVERAGE-ON$
}
@@ -37,22 +39,22 @@ object RedisSettings extends ConfigLoader[ RedisSettings ] {
import RedisConfigLoader._
def load( config: Config, path: String ) = apply(
- dispatcher = loadInvocationContext( config, path )( required ),
- invocationPolicy = loadInvocationPolicy( config, path )( required ),
- recovery = loadRecovery( config, path )( required ),
- timeout = loadTimeouts( config, path )( RedisTimeouts.requiredDefault( required ).asFallback ),
- source = loadSource( config, path )( "standalone".asFallback ),
- prefix = loadPrefix( config, path )( None.asFallback )
+ dispatcher = loadInvocationContext( config, path ).get,
+ invocationPolicy = loadInvocationPolicy( config, path ).get,
+ recovery = loadRecovery( config, path ).get,
+ timeout = loadTimeouts( config, path )( RedisTimeouts.requiredDefault ),
+ source = loadSource( config, path ).get,
+ prefix = loadPrefix( config, path )
)
def withFallback( fallback: RedisSettings ) = new ConfigLoader[ RedisSettings ] {
def load( config: Config, path: String ) = apply(
- dispatcher = loadInvocationContext( config, path )( fallback.invocationContext.asFallback ),
- invocationPolicy = loadInvocationPolicy( config, path )( fallback.invocationPolicy.asFallback ),
- recovery = loadRecovery( config, path )( fallback.recovery.asFallback ),
- timeout = loadTimeouts( config, path )( fallback.timeout.asFallback ),
- source = loadSource( config, path )( fallback.source.asFallback ),
- prefix = loadPrefix( config, path )( fallback.prefix.asFallback )
+ dispatcher = loadInvocationContext( config, path ) getOrElse fallback.invocationContext,
+ invocationPolicy = loadInvocationPolicy( config, path ) getOrElse fallback.invocationPolicy,
+ recovery = loadRecovery( config, path ) getOrElse fallback.recovery,
+ timeout = loadTimeouts( config, path )( fallback.timeout ),
+ source = loadSource( config, path ) getOrElse fallback.source,
+ prefix = loadPrefix( config, path ) orElse fallback.prefix
)
}
@@ -69,23 +71,23 @@ object RedisSettings extends ConfigLoader[ RedisSettings ] {
val source = _source
}
- private def loadInvocationContext( config: Config, path: String )( default: String => String ): String =
- config.getOption( path / "dispatcher", _.getString ) getOrElse default( path / "dispatcher" )
+ private def loadInvocationContext( config: Config, path: String ): Option[ String ] =
+ config.getOption( path / "dispatcher", _.getString )
- private def loadInvocationPolicy( config: Config, path: String )( default: String => String ): String =
- config.getOption( path / "invocation", _.getString ) getOrElse default( path / "invocation" )
+ private def loadInvocationPolicy( config: Config, path: String ): Option[ String ] =
+ config.getOption( path / "invocation", _.getString )
- private def loadRecovery( config: Config, path: String )( default: String => String ): String =
- config.getOption( path / "recovery", _.getString ) getOrElse default( path / "recovery" )
+ private def loadRecovery( config: Config, path: String ): Option[ String ] =
+ config.getOption( path / "recovery", _.getString )
- private def loadSource( config: Config, path: String )( default: String => String ): String =
- config.getOption( path / "source", _.getString ) getOrElse default( path / "source" )
+ private def loadSource( config: Config, path: String ): Option[ String ] =
+ config.getOption( path / "source", _.getString )
- private def loadPrefix( config: Config, path: String )( default: String => Option[ String ] ): Option[ String ] =
- config.getOption( path / "prefix", _.getString ) orElse default( path / "prefix" )
+ private def loadPrefix( config: Config, path: String ): Option[ String ] =
+ config.getOption( path / "prefix", _.getString )
- private def loadTimeouts( config: Config, path: String )( default: String => RedisTimeouts ): RedisTimeouts =
- RedisTimeouts.load( config, path )( default )
+ private def loadTimeouts( config: Config, path: String )( defaults: RedisTimeouts ): RedisTimeouts =
+ RedisTimeouts.load( config, path )( defaults )
}
diff --git a/src/main/scala/play/api/cache/redis/configuration/RedisTimeouts.scala b/src/main/scala/play/api/cache/redis/configuration/RedisTimeouts.scala
index c765ad93..6f719e76 100644
--- a/src/main/scala/play/api/cache/redis/configuration/RedisTimeouts.scala
+++ b/src/main/scala/play/api/cache/redis/configuration/RedisTimeouts.scala
@@ -32,10 +32,12 @@ case class RedisTimeoutsImpl
) extends RedisTimeouts {
+ // $COVERAGE-OFF$
override def equals( obj: scala.Any ) = obj match {
case that: RedisTimeouts => this.sync == that.sync && this.redis == that.redis
case _ => false
}
+ // $COVERAGE-ON$
}
@@ -44,7 +46,7 @@ object RedisTimeouts {
private def log = Logger( "play.api.cache.redis" )
- def requiredDefault( required: String => Nothing ) = new RedisTimeouts {
+ def requiredDefault: RedisTimeouts = new RedisTimeouts {
def sync = required( "sync-timeout" )
def redis = None
}
@@ -53,9 +55,9 @@ object RedisTimeouts {
def apply( sync: FiniteDuration, redis: Option[ FiniteDuration ] = None ): RedisTimeouts =
RedisTimeoutsImpl( sync, redis )
- def load( config: Config, path: String )( default: String => RedisTimeouts ) = RedisTimeouts(
- sync = loadSyncTimeout( config, path )( default( _ ).sync ),
- redis = loadRedisTimeout( config, path )( default( _ ).redis )
+ def load( config: Config, path: String )( default: RedisTimeouts ) = RedisTimeouts(
+ sync = loadSyncTimeout( config, path ) getOrElse default.sync,
+ redis = loadRedisTimeout( config, path ) orElse default.redis
)
@scala.deprecated( "Property 'timeout' was deprecated in 2.1.0 and was replaced by the 'sync-timeout' with the identical use and meaning.", since = "2.1.0" )
@@ -71,15 +73,13 @@ object RedisTimeouts {
FiniteDuration( duration.getSeconds, TimeUnit.SECONDS )
}
- private def loadSyncTimeout( config: Config, path: String )( default: String => FiniteDuration ): FiniteDuration =
- config.getOption( path / "sync-timeout", _.getDuration ).map {
- duration => FiniteDuration( duration.getSeconds, TimeUnit.SECONDS )
- } orElse {
- loadTimeout( config, path )
- } getOrElse default( path / "sync-timeout" )
+ private def loadSyncTimeout( config: Config, path: String ): Option[ FiniteDuration ] =
+ loadTimeout( config, path ) orElse {
+ config.getOption( path / "sync-timeout", _.getDuration ).map( duration => FiniteDuration( duration.getSeconds, TimeUnit.SECONDS ) )
+ }
- private def loadRedisTimeout( config: Config, path: String )( default: String => Option[ FiniteDuration ] ): Option[ FiniteDuration ] =
+ private def loadRedisTimeout( config: Config, path: String ): Option[ FiniteDuration ] =
config.getOption( path / "redis-timeout", _.getDuration ).map {
duration => FiniteDuration( duration.getSeconds, TimeUnit.SECONDS )
- } orElse default( path / "redis-timeout" )
+ }
}
diff --git a/src/main/scala/play/api/cache/redis/connector/AkkaSerializer.scala b/src/main/scala/play/api/cache/redis/connector/AkkaSerializer.scala
index 9a5d550c..442e4142 100644
--- a/src/main/scala/play/api/cache/redis/connector/AkkaSerializer.scala
+++ b/src/main/scala/play/api/cache/redis/connector/AkkaSerializer.scala
@@ -56,8 +56,10 @@ private[ connector ] class AkkaEncoder( serializer: Serialization ) {
case primitive if isPrimitive( primitive ) => primitive.toString
// AnyRef is supported by Akka serializers, but it does not consider classTag, thus it is done manually
case anyRef: AnyRef => anyRefToString( anyRef )
+ // $COVERAGE-OFF$
// if no of the cases above matches, throw an exception
case _ => unsupported( s"Type ${ value.getClass } is not supported by redis cache connector." )
+ // $COVERAGE-ON$
}
/** determines whether the given value is a primitive */
diff --git a/src/main/scala/play/api/cache/redis/connector/ExpectedFuture.scala b/src/main/scala/play/api/cache/redis/connector/ExpectedFuture.scala
index add60d56..18143a6c 100644
--- a/src/main/scala/play/api/cache/redis/connector/ExpectedFuture.scala
+++ b/src/main/scala/play/api/cache/redis/connector/ExpectedFuture.scala
@@ -40,29 +40,33 @@ private[ connector ] class ExpectedFutureWithoutKey[ T ]( protected val future:
}
protected def onFailed( ex: Throwable ): Nothing =
- failed( None, cmd, ex )
+ failed( None, cmd, cmd, ex )
def withKey( key: String ): ExpectedFutureWithKey[ T ] = new ExpectedFutureWithKey[ T ]( future, cmd, key, s"$cmd $key" )
def withKeys( keys: Traversable[ String ] ): ExpectedFutureWithKey[ T ] = withKey( keys mkString " " )
+
+ override def toString = s"ExpectedFuture($cmd)"
}
-private[ connector ] class ExpectedFutureWithKey[ T ]( protected val future: Future[ T ], protected val cmd: String, key: String, fullCommand: => String ) extends ExpectedFuture[ T ] {
+private[ connector ] class ExpectedFutureWithKey[ T ]( protected val future: Future[ T ], protected val cmd: String, key: String, statement: => String ) extends ExpectedFuture[ T ] {
protected def onUnexpected: PartialFunction[ Any, Nothing ] = {
case _ => unexpected( Some( key ), cmd )
}
protected def onFailed( ex: Throwable ): Nothing =
- failed( Some( key ), cmd, ex )
+ failed( Some( key ), cmd, statement, ex )
def andParameter( param: => Any ): ExpectedFutureWithKey[ T ] = andParameters( param.toString )
def andParameters( params: Traversable[ Any ] ): ExpectedFutureWithKey[ T ] = andParameters( params mkString " " )
- def andParameters( params: => String ): ExpectedFutureWithKey[ T ] = new ExpectedFutureWithKey( future, cmd, key, s"$fullCommand $params" )
+ def andParameters( params: => String ): ExpectedFutureWithKey[ T ] = new ExpectedFutureWithKey( future, cmd, key, s"$statement $params" )
def asCommand( commandOverride: => String ) = new ExpectedFutureWithKey( future, cmd, key, s"$cmd $commandOverride" )
+
+ override def toString = s"ExpectedFuture($statement)"
}
/**
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 163ee283..2e61a032 100644
--- a/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala
+++ b/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala
@@ -76,6 +76,7 @@ private[ connector ] class RedisCommandsStandalone( configuration: RedisStandalo
override def onConnectStatus = ( status: Boolean ) => connected = status
}
+ // $COVERAGE-OFF$
def start( ) = database.fold {
log.info( s"Redis cache actor started. It is connected to $host:$port" )
} {
@@ -87,6 +88,7 @@ private[ connector ] class RedisCommandsStandalone( configuration: RedisStandalo
client.stop()
log.info( "Redis cache stopped." )
}
+ // $COVERAGE-ON$
}
@@ -111,6 +113,7 @@ private[ connector ] class RedisCommandsCluster( configuration: RedisCluster )(
protected implicit val scheduler = system.scheduler
}
+ // $COVERAGE-OFF$
def start( ) = {
def servers = nodes.map {
case RedisHost( host, port, Some( database ), _ ) => s" $host:$port?database=$database"
@@ -125,4 +128,5 @@ private[ connector ] class RedisCommandsCluster( configuration: RedisCluster )(
client.stop()
log.info( "Redis cluster cache stopped." )
}
+ // $COVERAGE-ON$
}
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 9815425e..8724ad0b 100644
--- a/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala
+++ b/src/main/scala/play/api/cache/redis/connector/RedisConnectorImpl.scala
@@ -128,10 +128,15 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
keys
}
+ // coverage is disabled as testing it would require
+ // either a mock or would clear a redis while
+ // the tests are in progress
+ // $COVERAGE-OFF$
def invalidate( ): Future[ Unit ] =
redis.flushdb( ) executing "FLUSHDB" expects {
case _ => log.info( "Invalidated." ) // cache was invalidated
}
+ // $COVERAGE-ON$
def exists( key: String ): Future[ Boolean ] =
redis.exists( key ) executing "EXISTS" withKey key expects {
@@ -168,7 +173,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
Future.sequence( values.map( encode( key, _ ) ) ).flatMap( redis.lpush( key, _: _* ) ) executing "LPUSH" withKey key andParameters values expects {
case length => log.debug( s"The $length values was prepended to key '$key'." ); length
} recover {
- case ExecutionFailedException( _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
+ 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." )
}
@@ -176,6 +181,10 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
def listAppend( key: String, values: Any* ) =
Future.sequence( values.map( encode( key, _ ) ) ).flatMap( redis.rpush( key, _: _* ) ) executing "RPUSH" withKey key andParameters values expects {
case length => log.debug( s"The $length values was appended to key '$key'." ); length
+ } 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." )
}
def listSize( key: String ) =
@@ -187,7 +196,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
encode( key, value ).flatMap( redis.lset( key, position, _ ) ) executing "LSET" withKey key andParameter value expects {
case _ => log.debug( s"Updated value at $position in '$key' to $value." )
} recover {
- case ExecutionFailedException( _, _, actors.ReplyErrorException( "ERR index out of range" ) ) =>
+ case ExecutionFailedException( _, _, _, actors.ReplyErrorException( "ERR index out of range" ) ) =>
log.debug( s"Update of the value at $position in '$key' failed due to index out of range." )
throw new IndexOutOfBoundsException( "Index out of range" )
}
@@ -226,7 +235,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
case -1L | 0L => log.debug( s"Insert into the list at '$key' failed. Pivot not found." ); None
case length => log.debug( s"Inserted $value into the list at '$key'. New size is $length." ); Some( length )
} recover {
- case ExecutionFailedException( _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
+ 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." )
}
@@ -238,7 +247,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
Future.sequence( values map toEncoded ).flatMap( redis.sadd( key, _: _* ) ) executing "SADD" withKey key andParameters values expects {
case inserted => log.debug( s"Inserted $inserted elements into the set at '$key'." ); inserted
} recover {
- case ExecutionFailedException( _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
+ case ExecutionFailedException( _, _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
log.warn( s"Value at '$key' is not a set." )
throw new IllegalArgumentException( s"Value at '$key' is not a set." )
}
@@ -314,7 +323,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
case true => log.debug( s"Item $field in the collection at '$key' was inserted." ); true
case false => log.debug( s"Item $field in the collection at '$key' was updated." ); false
} recover {
- case ExecutionFailedException( _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
+ case ExecutionFailedException( _, _, _, ex ) if ex.getMessage startsWith "WRONGTYPE" =>
log.warn( s"Value at '$key' is not a map." )
throw new IllegalArgumentException( s"Value at '$key' is not a map." )
}
@@ -324,5 +333,7 @@ private[ connector ] class RedisConnectorImpl( serializer: AkkaSerializer, redis
case values => log.debug( s"The collection at '$key' contains ${ values.size } values." ); values.map( decode[ T ]( key, _ ) ).toSet
}
+ // $COVERAGE-OFF$
override def toString = s"RedisConnector(name=$name)"
+ // $COVERAGE-ON$
}
diff --git a/src/main/scala/play/api/cache/redis/connector/RequestTimeout.scala b/src/main/scala/play/api/cache/redis/connector/RequestTimeout.scala
index eabaa0a9..77e3707e 100644
--- a/src/main/scala/play/api/cache/redis/connector/RequestTimeout.scala
+++ b/src/main/scala/play/api/cache/redis/connector/RequestTimeout.scala
@@ -72,6 +72,8 @@ trait FailEagerly extends RequestTimeout {
trait RedisRequestTimeout extends RequestTimeout {
import RequestTimeout._
+ private var initialized = false
+
/** indicates the timeout on the redis request */
protected def timeout: Option[ FiniteDuration ]
@@ -79,6 +81,21 @@ trait RedisRequestTimeout extends RequestTimeout {
// proceed with the command
@inline def continue = super.send( redisCommand )
// based on connection status
- timeout.fold( continue )( invokeOrFail( continue, _ ) )
+ if ( initialized ) timeout.fold( continue )( invokeOrFail( continue, _ ) ) else continue
}
+
+ // Note: Cannot RedisCluster invokes the `send` method during
+ // the class initialization. This call uses both timeout and scheduler
+ // properties although they are not initialized yet. Unfortunately,
+ // it seems there is no
+ // way to provide a `val timeout = configuration.timeout.redis`,
+ // which would be resolved before the use of the timeout property.
+ //
+ // As a workaround, the introduced boolean property initialized to false
+ // by JVM to efficintly disable the timeout mechanism while the trait
+ // initialization is not completed. Then the flag is set to true.
+ //
+ // This avoids the issue with the order of traits initialization.
+ //
+ initialized = true
}
diff --git a/src/main/scala/play/api/cache/redis/exceptions.scala b/src/main/scala/play/api/cache/redis/exceptions.scala
index fbbed39b..96c48789 100644
--- a/src/main/scala/play/api/cache/redis/exceptions.scala
+++ b/src/main/scala/play/api/cache/redis/exceptions.scala
@@ -21,7 +21,7 @@ case class TimeoutException( cause: Throwable ) extends RedisException( "Command
*
* @author Karel Cemus
*/
-case class ExecutionFailedException( key: Option[ String ], command: String, cause: Throwable ) extends RedisException( s"Execution of '$command'${ key.map( key => s" for key '$key'" ) getOrElse "" } failed", cause )
+case class ExecutionFailedException( key: Option[ String ], command: String, statement: String, cause: Throwable ) extends RedisException( s"Execution of '$command'${ key.map( key => s" for key '$key'" ) getOrElse "" } failed", cause )
/**
* Request succeeded but returned unexpected value
@@ -66,8 +66,8 @@ trait ExceptionImplicits {
/** helper indicating command execution failed with exception */
@throws[ ExecutionFailedException ]
- def failed( key: Option[ String ], command: String, cause: Throwable ): Nothing =
- throw ExecutionFailedException( key, command, cause )
+ def failed( key: Option[ String ], command: String, statement: String, cause: Throwable ): Nothing =
+ throw ExecutionFailedException( key, command, statement, cause )
/** helper indicating invalid configuration */
@throws[ IllegalStateException ]
diff --git a/src/main/scala/play/api/cache/redis/impl/AsyncRedis.scala b/src/main/scala/play/api/cache/redis/impl/AsyncRedis.scala
index 7a98620e..acd0473a 100644
--- a/src/main/scala/play/api/cache/redis/impl/AsyncRedis.scala
+++ b/src/main/scala/play/api/cache/redis/impl/AsyncRedis.scala
@@ -17,7 +17,8 @@ private[ impl ] class AsyncRedis( redis: RedisConnector )( implicit runtime: Red
with CacheAsyncApi
{
- def getOrElseUpdate[ T: ClassTag ]( key: String, expiration: Duration )( orElse: => Future[ T ] ) = getOrFuture[ T ]( key, expiration )( orElse )
+ def getOrElseUpdate[ T: ClassTag ]( key: String, expiration: Duration )( orElse: => Future[ T ] ) =
+ getOrFuture[ T ]( key, expiration )( orElse )
def removeAll( ): Future[ Done ] = invalidate()
}
diff --git a/src/main/scala/play/api/cache/redis/impl/Builders.scala b/src/main/scala/play/api/cache/redis/impl/Builders.scala
index 6e265cfa..b55d7c2e 100644
--- a/src/main/scala/play/api/cache/redis/impl/Builders.scala
+++ b/src/main/scala/play/api/cache/redis/impl/Builders.scala
@@ -15,17 +15,19 @@ object Builders {
trait ResultBuilder[ Result[ X ] ] {
/** name of the builder used for internal purposes */
- def name: String = this.getClass.getSimpleName
+ def name: String
/** converts future result produced by Redis to the result of desired type */
def toResult[ T ]( run: => Future[ T ], default: => Future[ T ] )( implicit runtime: RedisRuntime ): Result[ T ]
+ // $COVERAGE-OFF$
/** show the builder name */
override def toString = s"ResultBuilder($name)"
+ // $COVERAGE-ON$
}
/** returns the future itself without any transformation */
object AsynchronousBuilder extends ResultBuilder[ AsynchronousResult ] {
- override def name = "AsynchronousBuilder"
+ def name = "AsynchronousBuilder"
override def toResult[ T ]( run: => Future[ T ], default: => Future[ T ] )( implicit runtime: RedisRuntime ): AsynchronousResult[ T ] =
run recoverWith {
@@ -40,7 +42,7 @@ object Builders {
import scala.concurrent.Await
import scala.util._
- override def name = "SynchronousBuilder"
+ def name = "SynchronousBuilder"
override def toResult[ T ]( run: => Future[ T ], default: => Future[ T ] )( implicit runtime: RedisRuntime ): SynchronousResult[ T ] =
Try {
@@ -49,6 +51,7 @@ object Builders {
}.recover {
// it timed out, produce an expected exception
case cause: AskTimeoutException => timedOut( cause )
+ case cause: java.util.concurrent.TimeoutException => timedOut( cause )
}.recover {
// apply recovery policy to recover from expected exceptions
case failure: RedisException => Await.result( runtime.policy.recoverFrom( run, default, failure ), runtime.timeout.duration )
diff --git a/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala b/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala
index c9c4f34e..56411f26 100644
--- a/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala
+++ b/src/main/scala/play/api/cache/redis/impl/JavaRedis.scala
@@ -57,10 +57,11 @@ private[ impl ] class JavaRedis( internal: CacheAsyncApi, environment: Environme
implicit val context = HttpExecutionContext.fromThread( runtime.context )
// get the tag and decode it
def getClassTag = internal.get[ String ]( s"classTag::$key" )
- def decodeClassTag( name: String ): ClassTag[ T ] = if ( name == null ) ClassTag.Null.asInstanceOf[ ClassTag[ T ] ] else ClassTag( environment.classLoader.loadClass( name ) )
+ def decodeClassTag( name: String ): ClassTag[ T ] = if ( name == "" ) ClassTag.Null.asInstanceOf[ ClassTag[ T ] ] else ClassTag( environment.classLoader.loadClass( name ) )
def decodedClassTag( tag: Option[ String ] ) = tag.map( decodeClassTag )
// if tag is defined, get Option[ value ] otherwise None
def getValue = getClassTag.map( decodedClassTag ).flatMap {
+ case Some( ClassTag.Null ) => Future.successful( Some( null.asInstanceOf[ T ] ) )
case Some( tag ) => internal.get[ T ]( key )( tag )
case None => Future.successful( None )
}
@@ -85,11 +86,11 @@ private[ impl ] class JavaRedis( internal: CacheAsyncApi, environment: Environme
private[ impl ] object JavaRedis {
import scala.compat.java8.FutureConverters
- private implicit class Java8Compatibility[ T ]( val future: Future[ T ] ) extends AnyVal {
+ private[ impl ] implicit class Java8Compatibility[ T ]( val future: Future[ T ] ) extends AnyVal {
@inline def toJava: CompletionStage[ T ] = FutureConverters.toJava( future )
}
- private implicit class ScalaCompatibility[ T ]( val future: CompletionStage[ T ] ) extends AnyVal {
+ private[ impl ] implicit class ScalaCompatibility[ T ]( val future: CompletionStage[ T ] ) extends AnyVal {
@inline def toScala: Future[ T ] = FutureConverters.toScala( future )
}
}
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 912855f7..cc8eba81 100644
--- a/src/main/scala/play/api/cache/redis/impl/RedisCache.scala
+++ b/src/main/scala/play/api/cache/redis/impl/RedisCache.scala
@@ -114,5 +114,7 @@ private[ impl ] class RedisCache[ Result[ _ ] ]( redis: RedisConnector, builder:
new RedisMapImpl( key, redis )
}
+ // $COVERAGE-OFF$
override def toString = s"RedisCache(name=${ runtime.name })"
+ // $COVERAGE-ON$
}
diff --git a/src/main/scala/play/api/cache/redis/impl/RedisSetImpl.scala b/src/main/scala/play/api/cache/redis/impl/RedisSetImpl.scala
index c7df1634..2c8108b4 100644
--- a/src/main/scala/play/api/cache/redis/impl/RedisSetImpl.scala
+++ b/src/main/scala/play/api/cache/redis/impl/RedisSetImpl.scala
@@ -14,17 +14,31 @@ private[ impl ] class RedisSetImpl[ Elem: ClassTag, Result[ _ ] ]( key: String,
@inline
private def This: This = this
- def add( elements: Elem* ) = redis.setAdd( key, elements: _* ).map( _ => This ).recoverWithDefault( This )
+ def add( elements: Elem* ) = {
+ redis.setAdd( key, elements: _* ).map( _ => This ).recoverWithDefault( This )
+ }
- def contains( element: Elem ) = redis.setIsMember( key, element ).recoverWithDefault( false )
+ def contains( element: Elem ) = {
+ redis.setIsMember( key, element ).recoverWithDefault( false )
+ }
- def remove( element: Elem* ) = redis.setRemove( key, element: _* ).map( _ => This ).recoverWithDefault( This )
+ def remove( element: Elem* ) = {
+ redis.setRemove( key, element: _* ).map( _ => This ).recoverWithDefault( This )
+ }
- def toSet = redis.setMembers[ Elem ]( key ).recoverWithDefault( Set.empty )
+ def toSet = {
+ redis.setMembers[ Elem ]( key ).recoverWithDefault( Set.empty )
+ }
- def size = redis.setSize( key ).recoverWithDefault( 0 )
+ def size = {
+ redis.setSize( key ).recoverWithDefault( 0 )
+ }
- def isEmpty = redis.setSize( key ).map( _ == 0 ).recoverWithDefault( true )
+ def isEmpty = {
+ redis.setSize( key ).map( _ == 0 ).recoverWithDefault( true )
+ }
- def nonEmpty = redis.setSize( key ).map( _ > 0 ).recoverWithDefault( false )
+ def nonEmpty = {
+ redis.setSize( key ).map( _ > 0 ).recoverWithDefault( false )
+ }
}
diff --git a/src/main/scala/play/api/cache/redis/impl/dsl.scala b/src/main/scala/play/api/cache/redis/impl/dsl.scala
index db059a7b..dcc5a715 100644
--- a/src/main/scala/play/api/cache/redis/impl/dsl.scala
+++ b/src/main/scala/play/api/cache/redis/impl/dsl.scala
@@ -21,7 +21,7 @@ private[ impl ] object dsl {
}
/** helper function enabling us to recover from command execution */
- implicit class RecoveryFuture[ T ]( val future: Future[ T ] ) extends AnyVal {
+ implicit class RecoveryFuture[ T ]( future: => Future[ T ] ) {
/** Transforms the promise into desired builder results, possibly recovers with provided default value */
@inline def recoverWithDefault[ Result[ X ] ]( default: => T )( implicit builder: Builders.ResultBuilder[ Result ], runtime: RedisRuntime ): Result[ T ] =
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
index 62c02a8f..90541fd9 100644
--- a/src/test/resources/logback.xml
+++ b/src/test/resources/logback.xml
@@ -11,6 +11,8 @@
+
Test of cache to be sure that keys are differentiated, expires etc.
- */ -class IntegrationSpec extends Specification with Redis { - - private type Cache = CacheAsyncApi - - private val Cache = injector.instanceOf[ Cache ] - - "Use of 'CacheAsyncApi' out of 'play.api.cache.redis' package" should { - - "miss on get" in { - Cache.get[ String ]( "integration-test-1" ) must beNone - } - - "hit after set" in { - Cache.set( "integration-test-2", "value" ).sync - Cache.get[ String ]( "integration-test-2" ) must beSome[ Any ] - Cache.get[ String ]( "integration-test-2" ) must beSome( "value" ) - } - - "expire refreshes expiration" in { - Cache.set( "integration-test-10", "value", 2.second ).sync - Cache.get[ String ]( "integration-test-10" ) must beSome( "value" ) - Cache.expire( "integration-test-10", 1.minute ).sync - // wait until the first duration expires - Thread.sleep( 3000 ) - Cache.get[ String ]( "integration-test-10" ) must beSome( "value" ) - } - - "positive exists on existing keys" in { - Cache.set( "integration-test-11", "value" ).sync - Cache.exists( "integration-test-11" ) must beTrue - } - - "negative exists on expired and missing keys" in { - Cache.set( "integration-test-12A", "value", 1.second ).sync - // wait until the duration expires - Thread.sleep( 2000 ) - Cache.exists( "integration-test-12A" ) must beFalse - Cache.exists( "integration-test-12B" ) must beFalse - } - - "miss after remove" in { - Cache.set( "integration-test-3", "value" ).sync - Cache.get[ String ]( "integration-test-3" ) must beSome[ Any ] - Cache.remove( "integration-test-3" ).sync - Cache.get[ String ]( "integration-test-3" ) must beNone - } - - "miss after timeout" in { - // set - Cache.set( "integration-test-4", "value", 1.second ).sync - Cache.get[ String ]( "integration-test-4" ) must beSome[ Any ] - // wait until it expires - Thread.sleep( 1500 ) - // miss - Cache.get[ String ]( "integration-test-4" ) must beNone - } - - "find all matching keys" in { - Cache.set( "integration-test-13-key-A", "value", 3.second ).sync - Cache.set( "integration-test-13-note-A", "value", 3.second ).sync - Cache.set( "integration-test-13-key-B", "value", 3.second ).sync - Cache.matching( "integration-test-13*" ).sync.sorted mustEqual Seq( "integration-test-13-key-A", "integration-test-13-note-A", "integration-test-13-key-B" ).sorted - Cache.matching( "integration-test-13*A" ).sync.sorted mustEqual Seq( "integration-test-13-key-A", "integration-test-13-note-A" ).sorted - Cache.matching( "integration-test-13-key-*" ).sync.sorted mustEqual Seq( "integration-test-13-key-A", "integration-test-13-key-B" ).sorted - Cache.matching( "integration-test-13A*" ).sync mustEqual Seq.empty - } - - "remove all matching keys, wildcard at the end" in { - Cache.set( "integration-test-14-key-A", "value", 3.second ).sync - Cache.set( "integration-test-14-note-A", "value", 3.second ).sync - Cache.set( "integration-test-14-key-B", "value", 3.second ).sync - Cache.matching( "integration-test-14*" ).sync.sorted mustEqual Seq( "integration-test-14-key-A", "integration-test-14-note-A", "integration-test-14-key-B" ).sorted - Cache.removeMatching( "integration-test-14*" ).sync - Cache.matching( "integration-test-14*" ).sync mustEqual Seq.empty - } - - "remove all matching keys, wildcard in the middle" in { - Cache.set( "integration-test-15-key-A", "value", 3.second ).sync - Cache.set( "integration-test-15-note-A", "value", 3.second ).sync - Cache.set( "integration-test-15-key-B", "value", 3.second ).sync - Cache.matching( "integration-test-15*A" ).sync.sorted mustEqual Seq( "integration-test-15-key-A", "integration-test-15-note-A" ).sorted - Cache.removeMatching( "integration-test-15*A").sync - Cache.matching( "integration-test-15*A").sync mustEqual Seq.empty - } - - "remove all matching keys, no match" in { - Cache.matching( "integration-test-16*" ).sync mustEqual Seq.empty - Cache.removeMatching( "integration-test-16*").sync - Cache.matching( "integration-test-16*" ).sync mustEqual Seq.empty - } - - "propagate fail in future" in { - Cache.getOrFuture[ String ]( "integration-test-9" ){ - Future.failed( new IllegalStateException( "Exception in test." ) ) - }.sync must throwA( new IllegalStateException( "Exception in test." ) ) - } - - "support list" in { - // store value - Cache.set( "list", List( "A", "B", "C" ) ).sync - // recall - Cache.get[ List[ String ] ]( "list" ) must beSome[ List[ String ] ]( List( "A", "B", "C" ) ) - } - - "support a byte" in { - Cache.set( "type.byte", 0xAB.toByte ).sync - Cache.get[ Byte ]( "type.byte" ) must beSome[ Byte ] - Cache.get[ Byte ]( "type.byte" ) must beSome( 0xAB.toByte ) - } - - "support a char" in { - Cache.set( "type.char.1", 'a' ).sync - Cache.get[ Char ]( "type.char.1" ) must beSome[ Char ] - Cache.get[ Char ]( "type.char.1" ) must beSome( 'a' ) - Cache.set( "type.char.2", 'b' ).sync - Cache.get[ Char ]( "type.char.2" ) must beSome( 'b' ) - Cache.set( "type.char.3", 'č' ).sync - Cache.get[ Char ]( "type.char.3" ) must beSome( 'č' ) - } - - "support a short" in { - Cache.set( "type.short", 12.toShort ).sync - Cache.get[ Short ]( "type.short" ) must beSome[ Short ] - Cache.get[ Short ]( "type.short" ) must beSome( 12.toShort ) - } - - "support an int" in { - Cache.set( "type.int", 15 ).sync - Cache.get[ Int ]( "type.int" ) must beSome( 15 ) - } - - "support a long" in { - Cache.set( "type.long", 144L ).sync - Cache.get[ Long ]( "type.long" ) must beSome[ Long ] - Cache.get[ Long ]( "type.long" ) must beSome( 144L ) - } - - "support a float" in { - Cache.set( "type.float", 1.23f ).sync - Cache.get[ Float ]( "type.float" ) must beSome[ Float ] - Cache.get[ Float ]( "type.float" ) must beSome( 1.23f ) - } - - "support a double" in { - Cache.set( "type.double", 3.14 ).sync - Cache.get[ Double ]( "type.double" ) must beSome[ Double ] - Cache.get[ Double ]( "type.double" ) must beSome( 3.14 ) - } - - "support a date" in { - Cache.set( "type.date", new Date( 123 ) ).sync - Cache.get[ Date ]( "type.date" ) must beSome( new Date( 123 ) ) - } - - "support a datetime" in { - Cache.set( "type.datetime", new DateTime( 123456 ) ).sync - Cache.get[ DateTime ]( "type.datetime" ) must beSome( new DateTime( 123456 ) ) - } - - "remove multiple keys at once" in { - Cache.set( "integration-test-remove-multiple-1", "value" ).sync - Cache.get[ String ]( "integration-test-remove-multiple-1" ) must beSome[ Any ] - Cache.set( "integration-test-remove-multiple-2", "value" ).sync - Cache.get[ String ]( "integration-test-remove-multiple-2" ) must beSome[ Any ] - Cache.set( "integration-test-remove-multiple-3", "value" ).sync - Cache.get[ String ]( "integration-test-remove-multiple-3" ) must beSome[ Any ] - Cache.remove( "integration-test-remove-multiple-1", "integration-test-remove-multiple-2", "integration-test-remove-multiple-3" ).sync - Cache.get[ String ]( "integration-test-remove-multiple-1" ) must beNone - Cache.get[ String ]( "integration-test-remove-multiple-2" ) must beNone - Cache.get[ String ]( "integration-test-remove-multiple-3" ) must beNone - } - - "remove in batch" in { - Cache.set( "integration-test-remove-batch-1", "value" ).sync - Cache.get[ String ]( "integration-test-remove-batch-1" ) must beSome[ Any ] - Cache.set( "integration-test-remove-batch-2", "value" ).sync - Cache.get[ String ]( "integration-test-remove-batch-2" ) must beSome[ Any ] - Cache.set( "integration-test-remove-batch-3", "value" ).sync - Cache.get[ String ]( "integration-test-remove-batch-3" ) must beSome[ Any ] - Cache.removeAll( Seq( "integration-test-remove-batch-1", "integration-test-remove-batch-2", "integration-test-remove-batch-3" ): _* ).sync - Cache.get[ String ]( "integration-test-remove-batch-1" ) must beNone - Cache.get[ String ]( "integration-test-remove-batch-2" ) must beNone - Cache.get[ String ]( "integration-test-remove-batch-3" ) must beNone - } - } -} diff --git a/src/test/scala/play/api/cache/redis/ExpirationSpec.scala b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala new file mode 100644 index 00000000..7045a0e8 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala @@ -0,0 +1,36 @@ +package play.api.cache.redis + +import java.util.Date + +import scala.concurrent.duration._ + +import org.joda.time._ +import org.specs2.mutable.Specification + +/** + *This specification tests expiration conversion
+ */ +class ExpirationSpec extends Specification { + + "Expiration" should { + + def expireAt = DateTime.now().plusMinutes( 5 ).plusSeconds( 30 ) + + val expiration = 5.minutes + 30.seconds + val expirationFrom = expiration - 2.second + val expirationTo = expiration + 1.second + + "from java.util.Date" in { + new Date( expireAt.getMillis ).asExpiration must beBetween( expirationFrom, expirationTo ) + } + + "from org.joda.time.DateTime (deprecated)" in { + expireAt.asExpiration must beBetween( expirationFrom, expirationTo ) + } + + "from java.time.LocalDateTime" in { + import java.time._ + LocalDateTime.ofInstant( expireAt.toInstant.toDate.toInstant, ZoneId.systemDefault() ).asExpiration must beBetween( expirationFrom, expirationTo ) + } + } +} diff --git a/src/test/scala/play/api/cache/redis/Implicits.scala b/src/test/scala/play/api/cache/redis/Implicits.scala new file mode 100644 index 00000000..87c75ee2 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/Implicits.scala @@ -0,0 +1,112 @@ +package play.api.cache.redis + +import java.util.concurrent.Callable + +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} + +/** + * @author Karel Cemus + */ +object Implicits { + + val defaultCacheName = "play" + val localhost = "localhost" + val defaultPort = 6379 + + val defaults = RedisSettingsTest( "akka.actor.default-dispatcher", "lazy", RedisTimeouts( 1.second ), "log-and-default", "standalone" ) + + val defaultInstance = RedisStandalone( defaultCacheName, RedisHost( localhost, defaultPort ), defaults ) + + 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 await = 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, Unit ) + } + } +} + +trait ReducedMockito extends MocksCreation + with CalledMatchers + with MockitoStubs + with CapturedArgument + // with MockitoMatchers + with ArgThat + with Expectations + with MockitoFunctions + +object MockitoImplicits extends ReducedMockito + +trait WithApplication { + import play.api.Application + import play.api.inject.guice.GuiceApplicationBuilder + + protected def builder = new GuiceApplicationBuilder() + + private val theBuilder = builder + + protected val injector = theBuilder.injector + + protected val application: Application = injector.instanceOf[ Application ] + + implicit protected val system = injector.instanceOf[ ActorSystem ] +} + +trait WithHocon { + import play.api.Configuration + + import com.typesafe.config.ConfigFactory + + protected def hocon: String + + protected val config = { + val reference = ConfigFactory.load() + val local = ConfigFactory.parseString( hocon.stripMargin ) + local.withFallback( reference ) + } + + protected 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/RecoveryPolicySpecs.scala b/src/test/scala/play/api/cache/redis/RecoveryPolicySpecs.scala new file mode 100644 index 00000000..3a167aee --- /dev/null +++ b/src/test/scala/play/api/cache/redis/RecoveryPolicySpecs.scala @@ -0,0 +1,73 @@ +package play.api.cache.redis + +import scala.concurrent.Future + +import play.api.Logger + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mock.Mockito +import org.specs2.mutable.Specification + +class RecoveryPolicySpecs( implicit ee: ExecutionEnv ) extends Specification with Mockito { + + class BasicPolicy extends RecoveryPolicy { + def recoverFrom[ T ]( rerun: => Future[ T ], default: => Future[ T ], failure: RedisException ) = default + } + + 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 ) + def any = unexpectedAny + } + + "Recovery Policy" should { + + "log detailed report" in { + val policy = new BasicPolicy with DetailedReports { + override val log = mock[ Logger ] + } + + // note: there should be tested a logger and the message + policy.recoverFrom( rerun, default, ex.unexpectedAny ) mustEqual default + policy.recoverFrom( rerun, default, ex.unexpectedKey ) mustEqual default + policy.recoverFrom( rerun, default, ex.failedAny ) mustEqual default + policy.recoverFrom( rerun, default, ex.failedKey ) mustEqual default + policy.recoverFrom( rerun, default, ex.timeout ) mustEqual default + policy.recoverFrom( rerun, default, ex.serialization ) mustEqual default + } + + "log condensed report" in { + val policy = new BasicPolicy with CondensedReports { + override val log = mock[ Logger ] + } + + // note: there should be tested a logger and the message + policy.recoverFrom( rerun, default, ex.unexpectedAny ) mustEqual default + policy.recoverFrom( rerun, default, ex.unexpectedKey ) mustEqual default + policy.recoverFrom( rerun, default, ex.failedAny ) mustEqual default + policy.recoverFrom( rerun, default, ex.failedKey ) mustEqual default + policy.recoverFrom( rerun, default, ex.timeout ) mustEqual default + policy.recoverFrom( rerun, default, ex.serialization ) mustEqual default + } + + "fail through" in { + val policy = new BasicPolicy with FailThrough + + policy.recoverFrom( rerun, default, ex.any ) must throwA( ex.any ).await + } + + "recover with default" in { + val policy = new BasicPolicy with RecoverWithDefault + + policy.recoverFrom( rerun, default, ex.any ) mustEqual default + } + } +} diff --git a/src/test/scala/play/api/cache/redis/Redis.scala b/src/test/scala/play/api/cache/redis/Redis.scala deleted file mode 100644 index 1d5a15af..00000000 --- a/src/test/scala/play/api/cache/redis/Redis.scala +++ /dev/null @@ -1,146 +0,0 @@ -package play.api.cache.redis - -import scala.concurrent.duration._ -import scala.language.implicitConversions - -import play.api.Application -import play.api.inject.ApplicationLifecycle -import play.api.inject.guice.GuiceApplicationBuilder - -import akka.actor.ActorSystem -import akka.util.Timeout -import org.specs2.matcher._ -import org.specs2.specification._ -import redis.RedisClient - -/** - * Provides implicits and configuration for redis tests invocation - */ -trait Redis extends EmptyRedis with RedisMatcher with AfterAll { - - def injector = Redis.injector - - implicit val application: Application = injector.instanceOf[ Application ] - - implicit val system: ActorSystem = injector.instanceOf[ ActorSystem ] - - def afterAll( ) = { - Redis.close() - } -} - -trait Synchronization { - - import play.api.cache.redis.TestHelpers._ - - implicit val timeout = Timeout( 3.second ) - - implicit def async2synchronizer[ T ]( future: AsynchronousResult[ T ] ): Synchronizer[ T ] = new Synchronizer[ T ]( future ) -} - -trait RedisMatcher extends Synchronization { - - implicit def matcher[ T ]( matcher: Matcher[ T ] ): Matcher[ AsynchronousResult[ T ] ] = new Matcher[ AsynchronousResult[ T ] ] { - override def apply[ S <: AsynchronousResult[ T ] ]( value: Expectable[ S ] ): MatchResult[ S ] = { - val matched = value.map( ( _: AsynchronousResult[ T ] ).sync ).applyMatcher( matcher ) - result( matched, value ) - } - } -} - -trait RedisSettings { - - def host = "localhost" - - def port = 6379 - - def database = 0 -} - -/** - * Provides testing redis instance - * - * @author Karel Cemus - */ -trait TestRedisInstance extends RedisSettings with Synchronization { - - private var _redis: RedisClient = _ - - /** instance of brando */ - protected def redis( implicit application: Application, system: ActorSystem ) = synchronized { - if ( _redis == null ) _redis = RedisClient( host = host, port = port, db = Some( database ) ) - _redis - } -} - -/** - * Set up Redis server, empty its testing database to avoid any inference with previous tests - * - * @author Karel Cemus - */ -trait EmptyRedis extends BeforeAll { - self: Redis => - - implicit def application: Application - - implicit def system: ActorSystem - - /** before all specifications reset redis database */ - override def beforeAll( ): Unit = EmptyRedis.empty -} - -object EmptyRedis extends TestRedisInstance { - - /** already executed */ - private var executed = false - - /** empty redis database at the beginning of the test suite */ - def empty( implicit application: Application, system: ActorSystem ): Unit = synchronized { - // execute only once - if ( !executed ) { - redis.flushdb().sync - executed = true - } - } -} - -/** Plain test object to be cached */ -case class SimpleObject( key: String, value: Int ) - -object Redis { - - import java.util.concurrent.atomic.AtomicInteger - - private val stopped = new AtomicInteger( 0 ) - - val allSpecs = List( - classOf[ configuration.RedisHostSpec ], - classOf[ configuration.RedisInstanceSpec ], - classOf[ configuration.RedisSettingsSpec ], - classOf[ configuration.RedisInstanceManagerSpec ], - classOf[ connector.RediscalaSpec ], - classOf[ connector.RedisConnectorSpec ], - classOf[ impl.AsynchronousCacheSpec ], - classOf[ impl.JavaCacheSpec ], - classOf[ impl.PlayCacheSpec ], - classOf[ impl.RecoveryPolicySpec ], - classOf[ impl.RedisListSpec ], - classOf[ impl.RedisMapSpecs ], - classOf[ impl.RedisSetSpecs ], - classOf[ impl.RedisPrefixSpec ], - classOf[ impl.SynchronousCacheSpec ], - classOf[ util.ExpirationSpec ], - classOf[ RedisComponentsSpecs ] - ).size - - def close( ): Unit = if ( stopped.incrementAndGet() == allSpecs ) - injector.instanceOf[ ApplicationLifecycle ].stop() - - val injector = new GuiceApplicationBuilder() - // load required bindings - .bindings( Seq.empty: _* ) - // #19 enable Redis module - .bindings( new RedisCacheModule ) - // produce a fake application - .injector() -} diff --git a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpecs.scala b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpecs.scala new file mode 100644 index 00000000..85594c3f --- /dev/null +++ b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpecs.scala @@ -0,0 +1,51 @@ +package play.api.cache.redis + +import play.api._ +import play.api.inject.ApplicationLifecycle + +import org.specs2.mutable.Specification +import org.specs2.specification.AfterAll + +/** + * @author Karel Cemus + */ +class RedisCacheComponentsSpecs extends Specification with WithApplication with AfterAll { + + object components extends RedisCacheComponents { + def actorSystem = system + def applicationLifecycle = injector.instanceOf[ ApplicationLifecycle ] + def environment = injector.instanceOf[ Environment ] + def configuration = injector.instanceOf[ Configuration ] + val syncRedis = cacheApi( "play" ).sync + } + + private type Cache = CacheApi + + private val cache = components.syncRedis + + val prefix = "components-sync" + + "RedisComponents" should { + + "provide api" >> { + "miss on get" in { + cache.get[ String ]( s"$prefix-test-1" ) must beNone + } + + "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" ) + } + + "positive exists on existing keys" in { + cache.set( s"$prefix-test-11", "value" ) + cache.exists( s"$prefix-test-11" ) must beTrue + } + } + } + + def afterAll( ) = { + components.applicationLifecycle.stop() + } +} diff --git a/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala new file mode 100644 index 00000000..a6f2dc04 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala @@ -0,0 +1,179 @@ +package play.api.cache.redis + +import javax.inject.Provider + +import scala.concurrent.duration._ +import scala.reflect.ClassTag + +import play.api.inject._ + +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 ) + + 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.api.cache.AsyncCacheApi ] must beAnInstanceOf[ play.api.cache.AsyncCacheApi ] + injector.instanceOf[ play.api.cache.SyncCacheApi ] must beAnInstanceOf[ play.api.cache.SyncCacheApi ] + } + + "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 )( injector ) + 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 ] + } + + "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 )( injector ) + + injector.instanceOf( binding[ CacheApi ].named( defaultCacheName ) ) must beAnInstanceOf[ CacheApi ] + injector.instanceOf( binding[ CacheApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ CacheApi ] + + injector.instanceOf( binding[ CacheAsyncApi ].named( defaultCacheName ) ) must beAnInstanceOf[ CacheAsyncApi ] + injector.instanceOf( binding[ CacheAsyncApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ CacheAsyncApi ] + + injector.instanceOf( binding[ play.cache.AsyncCacheApi ].named( defaultCacheName ) ) must beAnInstanceOf[ play.cache.AsyncCacheApi ] + injector.instanceOf( binding[ play.cache.AsyncCacheApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ play.cache.AsyncCacheApi ] + + injector.instanceOf( binding[ play.cache.SyncCacheApi ].named( defaultCacheName ) ) must beAnInstanceOf[ play.cache.SyncCacheApi ] + injector.instanceOf( binding[ play.cache.SyncCacheApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ play.cache.SyncCacheApi ] + + injector.instanceOf( binding[ play.api.cache.AsyncCacheApi ].named( defaultCacheName ) ) must beAnInstanceOf[ play.api.cache.AsyncCacheApi ] + injector.instanceOf( binding[ play.api.cache.AsyncCacheApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ play.api.cache.AsyncCacheApi ] + + injector.instanceOf( binding[ play.api.cache.SyncCacheApi ].named( defaultCacheName ) ) must beAnInstanceOf[ play.api.cache.SyncCacheApi ] + injector.instanceOf( binding[ play.api.cache.SyncCacheApi ].namedCache( defaultCacheName ) ) must beAnInstanceOf[ 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 )( injector ) + protected def hocon = + """ + |play.cache.redis { + | instances { + | + | play { + | host: localhost + | port: 6379 + | database: 1 + | } + | + | other { + | host: redis.localhost.cz + | port: 6378 + | database: 2 + | password: something + | } + | } + | + | default-cache: other + |} + """.stripMargin + val other = "other" + + // something is bound to the default cache name + 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[ 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[ 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 )( injector ) + protected def hocon = "play.cache.redis.source: custom" + + // bind named caches + injector.instanceOf[ CacheApi ] must beAnInstanceOf[ CacheApi ] + injector.instanceOf[ CacheAsyncApi ] must beAnInstanceOf[ CacheAsyncApi ] + + // Note: there should be tested which recovery policy instance is actually used + } + } +} + +object RedisCacheModuleSpec { + import play.api.cache.redis.configuration._ + import play.cache.NamedCacheImpl + import Implicits._ + + class AnyProvider[ T ]( instance: => T ) extends Provider[ T ] { + lazy val get = instance + } + + def binding[ T: ClassTag ]: BindingKey[ T ] = BindingKey( implicitly[ ClassTag[ T ] ].runtimeClass.asInstanceOf[ Class[ T ] ] ) + + implicit class RichBindingKey[ T ]( val key: BindingKey[ T ] ) { + def named( name: String ) = key.qualifiedWith( name ) + def namedCache( name: String ) = key.qualifiedWith( new NamedCacheImpl( name ) ) + } + + def runAndStop[ T: AsResult ]( t: => T )( injector: Injector ) = { + try { + AsResult.effectively( t ) + } finally { + injector.instanceOf[ ApplicationLifecycle ].stop() + } + } + + object MyRedisInstance extends RedisStandalone { + + def name = defaultCacheName + def invocationContext = "akka.actor.default-dispatcher" + def invocationPolicy = "lazy" + def timeout = RedisTimeouts( 1.second ) + def recovery = "log-and-default" + def source = "my-instance" + def prefix = None + def host = localhost + def port = defaultPort + def database = None + def password = None + } +} diff --git a/src/test/scala/play/api/cache/redis/RedisComponentsSpecs.scala b/src/test/scala/play/api/cache/redis/RedisComponentsSpecs.scala deleted file mode 100644 index 37c483c0..00000000 --- a/src/test/scala/play/api/cache/redis/RedisComponentsSpecs.scala +++ /dev/null @@ -1,45 +0,0 @@ -package play.api.cache.redis - -import play.api._ -import play.api.inject.ApplicationLifecycle - -import org.specs2.mutable.Specification - -/** - * @author Karel Cemus - */ -class RedisComponentsSpecs extends Specification with Redis { - - object components extends RedisCacheComponents { - def actorSystem = system - def applicationLifecycle = injector.instanceOf[ ApplicationLifecycle ] - def environment = injector.instanceOf[ Environment ] - def configuration = injector.instanceOf[ Configuration ] - - val syncRedis = cacheApi( "play" ).sync - } - - private type Cache = CacheApi - - private val Cache = components.syncRedis - - val prefix = "components-sync" - - "SynchronousCacheApi" should { - - "miss on get" in { - Cache.get[ String ]( s"$prefix-test-1" ) must beNone - } - - "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" ) - } - - "positive exists on existing keys" in { - Cache.set( s"$prefix-test-11", "value" ) - Cache.exists( s"$prefix-test-11" ) must beTrue - } - } -} diff --git a/src/test/scala/play/api/cache/redis/TestHelpers.scala b/src/test/scala/play/api/cache/redis/TestHelpers.scala deleted file mode 100644 index db2126d7..00000000 --- a/src/test/scala/play/api/cache/redis/TestHelpers.scala +++ /dev/null @@ -1,35 +0,0 @@ -package play.api.cache.redis - -import scala.concurrent.Await -import scala.reflect.ClassTag - -import play.api.cache.redis.connector.AkkaSerializer - -import akka.util.Timeout - -/** - * TODO: Remove and refactor - * - * this helper exists as part of optimization of implicit classes. - * However, as the tests are about to be refactored, this is the - * easiest solution. - * - * @author Karel Cemus - */ -object TestHelpers { - - 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 Key( val key: String ) extends AnyVal - - /** waits for future responses and returns them synchronously */ - implicit class Synchronizer[ T ]( val future: AsynchronousResult[ T ] ) extends AnyVal{ - def sync( implicit timeout: Timeout ) = Await.result( future, timeout.duration ) - } -} diff --git a/src/test/scala/play/api/cache/redis/TestModule.scala b/src/test/scala/play/api/cache/redis/TestModule.scala deleted file mode 100644 index 0f3e60c0..00000000 --- a/src/test/scala/play/api/cache/redis/TestModule.scala +++ /dev/null @@ -1,31 +0,0 @@ -package play.api.cache.redis - -import javax.inject.{Inject, Provider} - -import play.api.cache.redis.configuration.{RedisInstanceManager, RedisInstanceResolver} -import play.api.cache.redis.connector.{AkkaSerializer, RedisConnectorProvider} -import play.api.inject._ -import play.api.{Configuration, Environment} - -import akka.actor.ActorSystem - -/** - * @author Karel Cemus - */ -class GuiceRedisConnectorProvider @Inject()( serializer: AkkaSerializer, configuration: Configuration )( implicit resolver: RedisInstanceResolver, system: ActorSystem, lifecycle: ApplicationLifecycle ) extends Provider[ RedisConnector ] { - - private val instance = configuration.get( "play.cache.redis" )( RedisInstanceManager ).defaultInstance.resolved - - private implicit def runtime = new connector.RedisRuntime { - def name = instance.name - implicit def context = system.dispatchers.lookup( instance.invocationContext ) - } - - lazy val get = new RedisConnectorProvider( instance, serializer ).get -} - -class TestModule extends Module { - def bindings( environment: Environment, configuration: Configuration ) = Seq( - bind[ connector.RedisConnector ].toProvider[ GuiceRedisConnectorProvider ] - ) -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala deleted file mode 100644 index 47830127..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala +++ /dev/null @@ -1,52 +0,0 @@ -package play.api.cache.redis.configuration - -import org.specs2.mutable.Specification - -class RedisHostSpec extends Specification { - - // implicitly expose RedisHost config loader - implicit val loader = RedisHost - - "RedisHost" should "read" >> { - - "standalone" in new WithConfiguration( - """ - |instance { - | host: localhost - | port: 6379 - |} - """ - ) { - config.get[ RedisHost ]( "instance" ) mustEqual RedisHost( host = "localhost", port = 6379 ) - } - - "standalone with password" in new WithConfiguration( - """ - |instance { - | host: localhost - | port: 6379 - | password: "my password" - |} - """ - ) { - config.get[ RedisHost ]( "instance" ) mustEqual RedisHost( host = "localhost", port = 6379, password = Some( "my password" ) ) - } - - "standalone with database" in new WithConfiguration( - """ - |instance { - | host: localhost - | port: 6379 - | database: 1 - |} - """ - ) { - config.get[ RedisHost ]( "instance" ) mustEqual RedisHost( host = "localhost", port = 6379, database = Some( 1 ) ) - } - - "connection string" in { - RedisHost.fromConnectionString( "redis://localhost:6379" ) mustEqual RedisHost( host = "localhost", port = 6379 ) - RedisHost.fromConnectionString( "redis://redis:my-password@localhost:6379" ) mustEqual RedisHost( host = "localhost", port = 6379, password = Some( "my-password" ) ) - } - } -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpecs.scala b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpecs.scala new file mode 100644 index 00000000..5a39a316 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpecs.scala @@ -0,0 +1,45 @@ +package play.api.cache.redis.configuration + +import play.api.cache.redis._ + +import org.specs2.mutable.Spec + +/** + * @author Karel Cemus + */ +class RedisHostSpecs extends Spec { + import Implicits._ + + private implicit val loader = 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 without database and password" in new WithConfiguration( + """ + |play.cache.redis { + | host: localhost + | port: 6378 + |} + """ + ) { + configuration.get[ RedisHost ]( "play.cache.redis" ) mustEqual RedisHost( "localhost", 6378, database = 0 ) + } + + "host from connection string" in { + RedisHost.fromConnectionString( "redis://redis:something@localhost:6378" ) mustEqual RedisHost( "localhost", 6378, password = "something" ) + RedisHost.fromConnectionString( "redis://localhost:6378" ) mustEqual RedisHost( "localhost", 6378 ) + // test invalid string + RedisHost.fromConnectionString( "redis:/localhost:6378" ) must throwA[ IllegalArgumentException ] + } +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala deleted file mode 100644 index 5113e588..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpec.scala +++ /dev/null @@ -1,77 +0,0 @@ -package play.api.cache.redis.configuration - -import scala.concurrent.duration._ - -import org.specs2.mutable.Specification - -class RedisInstanceManagerSpec extends Specification { - - implicit val loader = RedisInstanceManager - - implicit val resolver = new RedisInstanceResolver { - val resolve = PartialFunction.empty - } - - "Advanced RedisInstanceManager" should "read" >> { - - "multiple caches" in new WithConfiguration( - """ - |redis { - | instances { - | play { - | host: localhost - | port: 6379 - | } - | users { - | host: localhost - | port: 6380 - | } - | data { - | host: localhost - | port: 6381 - | } - | } - | - | dispatcher: default-dispatcher - | recovery: log-and-default - | invocation: lazy - | timeout: 1s - |} - """ - ) { - val defaults = RedisSettings( dispatcher = "default-dispatcher", recovery = "log-and-default", invocationPolicy = "lazy", timeout = RedisTimeouts( 1.second ), source = "standalone" ) - - val manager = config.get[ RedisInstanceManager ]( "redis" ) - manager.caches mustEqual Set( "play", "data", "users" ) - - manager.instanceOf( "play" ).resolved must beEqualTo( RedisStandalone( "play", RedisHost( "localhost", 6379 ), defaults ) ) - manager.instanceOf( "users" ).resolved must beEqualTo( RedisStandalone( "users", RedisHost( "localhost", 6380 ), defaults ) ) - manager.instanceOf( "data" ).resolved must beEqualTo( RedisStandalone( "data", RedisHost( "localhost", 6381 ), defaults ) ) - } - } - - "Fallback RedisInstanceManager" should "read" >> { - - "single default cache" in new WithConfiguration( - """ - |redis { - | host: localhost - | port: 6380 - | - | default-cache: play - | dispatcher: default-dispatcher - | recovery: log-and-default - | invocation: lazy - | timeout: 1s - |} - """ - ) { - val defaults = RedisSettings( dispatcher = "default-dispatcher", recovery = "log-and-default", invocationPolicy = "lazy", timeout = RedisTimeouts( 1.second ), source = "standalone" ) - - val manager = config.get[ RedisInstanceManager ]( "redis" ) - manager.caches mustEqual Set( "play" ) - - manager.instanceOf( "play" ).resolved must beEqualTo( RedisStandalone( "play", RedisHost( "localhost", 6380 ), defaults ) ) - } - } -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpecs.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpecs.scala new file mode 100644 index 00000000..a6336252 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerSpecs.scala @@ -0,0 +1,186 @@ +package play.api.cache.redis.configuration + +import scala.concurrent.duration._ + +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisInstanceManagerSpecs extends Specification { + import play.api.cache.redis.Implicits._ + + private implicit def implicitlyInstance2resolved( instance: RedisInstance ): RedisInstanceProvider = new ResolvedRedisInstance( instance ) + private implicit def implicitlyString2unresolved( name: String ): RedisInstanceProvider = new UnresolvedRedisInstance( name ) + + private val extras = RedisSettingsTest( "my-dispatcher", "eager", RedisTimeouts( 5.minutes, 5.seconds ), "log-and-fail", "standalone", "redis." ) + + "default configuration" in new WithRedisInstanceManager( + """ + |play.cache.redis {} + """ + ) { + val defaultCache: RedisInstanceProvider = RedisStandalone( defaultCacheName, RedisHost( localhost, defaultPort, database = 0 ), defaults ) + + manager mustEqual RedisInstanceManagerTest( defaultCacheName )( defaultCache ) + + manager.instanceOf( defaultCacheName ) mustEqual defaultCache + manager.instanceOfOption( defaultCacheName ) must beSome( defaultCache ) + manager.instanceOfOption( "other" ) must beNone + + manager.defaultInstance mustEqual defaultCache + } + + "single default instance" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | host: redis.localhost.cz + | port: 6378 + | database: 2 + | password: something + | + | sync-timeout: 5 minutes + | redis-timeout: 5 seconds + | prefix: "redis." + | dispatcher: my-dispatcher + | invocation: eager + | recovery: log-and-fail + |} + """ + ) { + manager mustEqual RedisInstanceManagerTest( defaultCacheName )( + RedisStandalone( defaultCacheName, RedisHost( "redis.localhost.cz", 6378, database = 2, password = "something" ), extras ) + ) + } + + "named caches" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | instances { + | + | play { + | host: localhost + | port: 6379 + | database: 1 + | + | sync-timeout: 5 minutes + | redis-timeout: 5 seconds + | prefix: "redis." + | dispatcher: my-dispatcher + | invocation: eager + | recovery: log-and-fail + | } + | + | other { + | host: redis.localhost.cz + | port: 6378 + | database: 2 + | password: something + | } + | } + | + | 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 ) + + manager mustEqual RedisInstanceManagerTest( "other" )( defaultCache, otherCache ) + manager.instanceOf( defaultCacheName ) mustEqual defaultCache + manager.instanceOf( "other" ) mustEqual otherCache + manager.defaultInstance mustEqual otherCache + } + + "cluster mode" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | instances { + | play { + | cluster: [ + | { host: "localhost", port: 6380 } + | { host: "localhost", port: 6381 } + | { host: "localhost", port: 6382 } + | { host: "localhost", port: 6383 } + | ] + | source: cluster + | } + | } + |} + """ + ) { + 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" ) ) + ) + } + + "connection string mode" in new WithRedisInstanceManager( + """ + |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" ) ) + ) + } + + "custom mode" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | source: custom + |} + """ + ) { + manager mustEqual RedisInstanceManagerTest( defaultCacheName )( defaultCacheName ) + } + + "typo in mode with simple syntax" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | source: typo + |} + """ + ) { + manager.defaultInstance must throwA[ IllegalStateException ] + } + + "typo in mode with advanced syntax" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | instances { + | play { + | source: typo + | } + | } + |} + """ + ) { + manager.defaultInstance must throwA[ IllegalStateException ] + } + + "fail when requesting undefined cache" in new WithRedisInstanceManager( + """ + |play.cache.redis { + | instances { + | play { + | host: localhost + | port: 6379 + | } + | } + | default-cache: other + |} + """ + ) { + + manager.instanceOfOption( defaultCacheName ) must beSome[ RedisInstanceProvider ] + manager.instanceOfOption( "other" ) must beNone + + manager.instanceOf( "other" ) must throwA[ IllegalArgumentException ] + manager.defaultInstance must throwA[ IllegalArgumentException ] + } +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala new file mode 100644 index 00000000..d733f14c --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala @@ -0,0 +1,24 @@ +package play.api.cache.redis.configuration + +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 = RedisInstanceManager + + protected val manager = configuration.get[ RedisInstanceManager ]( "play.cache.redis" ) +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpecs.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpecs.scala new file mode 100644 index 00000000..84ebb4bf --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpecs.scala @@ -0,0 +1,30 @@ +package play.api.cache.redis.configuration + +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisInstanceProviderSpecs extends Specification { + import play.api.cache.redis.Implicits._ + + val defaultCache = RedisStandalone( defaultCacheName, RedisHost( localhost, defaultPort, database = 0 ), defaults ) + + implicit val resolver = new RedisInstanceResolver { + def resolve = { + case `defaultCacheName` => defaultCache + } + } + + "resolve already resolved" in { + new ResolvedRedisInstance( defaultCache ).resolved mustEqual defaultCache + } + + "resolve unresolved" in { + new UnresolvedRedisInstance( defaultCacheName ).resolved mustEqual defaultCache + } + + "fail when not able to resolve" in { + new UnresolvedRedisInstance( "other" ).resolved must throwA[ Exception ] + } +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceSpec.scala deleted file mode 100644 index a0ea41a7..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceSpec.scala +++ /dev/null @@ -1,85 +0,0 @@ -package play.api.cache.redis.configuration - -import scala.concurrent.duration._ - -import org.specs2.mutable.Specification - -class RedisInstanceSpec extends Specification { - - // settings builder - def settings( source: String ) = RedisSettings( dispatcher = "default-dispatcher", recovery = "log-and-default", invocationPolicy = "lazy", timeout = RedisTimeouts( 1.second ), source = source ) - // default settings - implicit val defaults = settings( source = "standalone" ) - // implicitly expose RedisHost config loader - implicit val loader = RedisInstanceProvider.loader( "play" ) - // instance resolver - implicit def resolver = new RedisInstanceResolver { - val resolve: PartialFunction[ String, RedisInstance ] = { - case name => RedisStandalone( name = s"resolved-$name", host = RedisHost( "localhost", 6380 ), settings = defaults ) - } - } - - "RedisInstance" should "read" >> { - - "standalone" in new WithConfiguration( - """ - |redis { - | host: localhost - | port: 6379 - |} - """ - ) { - config.get[ RedisInstanceProvider ]( "redis" ).resolved must beEqualTo( RedisStandalone( "play", RedisHost( host = "localhost", port = 6379 ), defaults ) ) - } - - "standalone overriding defaults" in new WithConfiguration( - """ - |redis { - | host: localhost - | port: 6379 - | dispatcher: custom-dispatcher - | recovery: custom - | timeout: 2s - | source: standalone - |} - """ - ) { - config.get[ RedisInstanceProvider ]( "redis" ).resolved must beEqualTo( RedisStandalone( "play", RedisHost( host = "localhost", port = 6379 ), RedisSettings( "custom-dispatcher", invocationPolicy = "lazy", timeout = RedisTimeouts( 2.second ), "custom", "standalone" ) ) ) - } - - "cluster" in new WithConfiguration( - """ - |redis { - | source: cluster - | cluster: [ - | { host: localhost, port: 6379 }, - | { host: localhost, port: 6380 } - | ] - |} - """ - ) { - config.get[ RedisInstanceProvider ]( "redis" ).resolved must beEqualTo( RedisCluster( "play", nodes = List( RedisHost( host = "localhost", port = 6379 ), RedisHost( host = "localhost", port = 6380 ) ), settings( source = "cluster" ) ) ) - } - - "standalone with connection string" in new WithConfiguration( - """ - |redis { - | source: "connection-string" - | connection-string: "redis://localhost:6379" - |} - """ - ) { - config.get[ RedisInstanceProvider ]( "redis" ).resolved must beEqualTo( RedisStandalone( "play", RedisHost( host = "localhost", port = 6379 ), settings( source = "connection-string" ) ) ) - } - - "custom" in new WithConfiguration( - """ - |redis { - | source: custom - |} - """ - ) { - config.get[ RedisInstanceProvider ]( "redis" ).resolved must beEqualTo( RedisStandalone( "resolved-play", RedisHost( host = "localhost", port = 6380 ), defaults ) ) - } - } -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisSettingsSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisSettingsSpec.scala deleted file mode 100644 index 8269c53e..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/RedisSettingsSpec.scala +++ /dev/null @@ -1,57 +0,0 @@ -package play.api.cache.redis.configuration - -import scala.concurrent.duration._ - -import play.api.Configuration - -import com.typesafe.config.ConfigFactory -import org.specs2.mutable.Specification - -class RedisSettingsSpec extends Specification { - - // implicitly expose RedisHost config loader - implicit val loader = RedisSettings - - // default settings - val defaults = RedisSettings( dispatcher = "default-dispatcher", invocationPolicy = "lazy", recovery = "log-and-default", timeout = RedisTimeouts( 1.second ), source = "standalone" ) - - "RedisSettings" should "read" >> { - - "defaults" in new WithConfiguration( - """ - |redis { - | dispatcher: default-dispatcher - | recovery: log-and-default - | invocation: lazy - | timeout: 1s - | source: standalone - |} - """ - ) { - config.get[ RedisSettings ]( "redis" ) mustEqual defaults - } - - "empty but with fallback" in new WithConfiguration( - """ - |redis { - |} - """ - ) { - config.get( "redis" )( RedisSettings.withFallback( defaults ) ) mustEqual RedisSettings( dispatcher = "default-dispatcher", invocationPolicy = "lazy", recovery = "log-and-default", timeout = RedisTimeouts( 1.second ), source = "standalone" ) - } - - "filled with fallback" in new WithConfiguration( - """ - |redis { - | dispatcher: custom-dispatcher - | recovery: custom - | invocation: lazy - | timeout: 2s - | source: cluster - |} - """ - ) { - config.get( "redis" )( RedisSettings.withFallback( defaults ) ) mustEqual RedisSettings( dispatcher = "custom-dispatcher", invocationPolicy = "lazy", recovery = "custom", timeout = RedisTimeouts( 2.second ), source = "cluster" ) - } - } -} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala b/src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala new file mode 100644 index 00000000..505b25af --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisSettingsTest.scala @@ -0,0 +1,15 @@ +package play.api.cache.redis.configuration + +/** + * @author Karel Cemus + */ +case class RedisSettingsTest +( + invocationContext: String, + invocationPolicy: String, + timeout: RedisTimeouts, + recovery: String, + source: String, + prefix: Option[ String ] = None + +) extends RedisSettings diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpecs.scala b/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpecs.scala new file mode 100644 index 00000000..0fa796c5 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/configuration/RedisTimeoutsSpecs.scala @@ -0,0 +1,52 @@ +package play.api.cache.redis.configuration + +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.mutable.Spec + +/** + * @author Karel Cemus + */ +class RedisTimeoutsSpecs extends Spec { + import Implicits._ + + private def orDefault = RedisTimeouts( 1.second, 2.seconds ) + + "load defined timeouts" in new WithConfiguration( + """ + |play.cache.redis { + | + | sync-timeout: 5s + | redis-timeout: 7s + |} + """ + ) { + RedisTimeouts.load( config, "play.cache.redis" )( RedisTimeouts.requiredDefault ) mustEqual RedisTimeouts( 5.seconds, 7.seconds ) + } + + "load with default timeouts" in new WithConfiguration( + """ + |play.cache.redis { + |} + """ + ) { + RedisTimeouts.load( config, "play.cache.redis" )( orDefault ) mustEqual RedisTimeouts( 1.second, 2.seconds ) + } + + "load deprecated timeout option" in new WithConfiguration( + """ + |play.cache.redis { + | timeout: 5s + |} + """ + ) { + RedisTimeouts.load( config, "play.cache.redis" )( orDefault ) mustEqual RedisTimeouts( 5.second, 2.seconds ) + } + + "load defaults" in { + RedisTimeouts.requiredDefault.sync must throwA[ RuntimeException ] + RedisTimeouts.requiredDefault.redis must beNone + } +} diff --git a/src/test/scala/play/api/cache/redis/configuration/helpers.scala b/src/test/scala/play/api/cache/redis/configuration/helpers.scala deleted file mode 100644 index 44180897..00000000 --- a/src/test/scala/play/api/cache/redis/configuration/helpers.scala +++ /dev/null @@ -1,23 +0,0 @@ -package play.api.cache.redis.configuration - -import play.api.Configuration - -import com.typesafe.config.ConfigFactory -import org.specs2.execute.AsResult -import org.specs2.mutable.Around -import org.specs2.specification.Scope - -/** - * A helper parses a configuration provided into the example - * - * @author Karel Cemus - */ -class WithConfiguration( hocon: String ) extends Around with Scope { - - val config = Configuration { - ConfigFactory.parseString( hocon.stripMargin ) - } - - def around[ T ]( t: => T )( implicit evidence$6: AsResult[ T ] ) = - AsResult.effectively( t ) -} diff --git a/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpecs.scala b/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpecs.scala new file mode 100644 index 00000000..383781bc --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/ExpectedFutureSpecs.scala @@ -0,0 +1,75 @@ +package play.api.cache.redis.connector + +import scala.concurrent.{ExecutionContext, Future} + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class ExpectedFutureSpecs( implicit ee: ExecutionEnv ) extends Specification { + + import ExpectedFutureSpecs._ + + class Suite( name: String )( implicit f: ExpectationBuilder[ String ] ) { + + name >> { + + "expected value" in { + Future.successful( "expected" ).asExpected must beEqualTo( "ok" ).await + } + + "unexpected value" in { + Future.successful( "unexpected" ).asExpected must throwA[ UnexpectedResponseException ].await + } + + "failing expectation" in { + Future.successful( "failing" ).asExpected must throwA[ ExecutionFailedException ].await + } + + "failing future inside redis" in { + Future.failed[ String ]( TimeoutException( simulatedFailure ) ).asExpected must throwA[ TimeoutException ].await + } + + "failing future with runtime exception" in { + Future.failed[ String ]( simulatedFailure ).asExpected must throwA[ ExecutionFailedException ].await + } + } + } + + new Suite( "Execution without the key" )( ( future: Future[ String ] ) => future.executing( cmd ) ) + + new Suite( "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)" + Future.successful( "expected" ).executing( cmd ).withKey( "key" ).andParameters( "SET" ).andParameter( 1 ).toString mustEqual s"ExpectedFuture($cmd key SET 1)" + Future.successful( "expected" ).executing( cmd ).withKeys( Seq( "key1", "key2" ) ).andParameters( Seq( "SET", 1 ) ).toString mustEqual s"ExpectedFuture($cmd key1 key2 SET 1)" + + Future.successful( "expected" ).executing( cmd ).withKey( "key" ).asCommand( "other 2" ).toString mustEqual s"ExpectedFuture(TEST CMD other 2)" + } +} + +object ExpectedFutureSpecs { + + val cmd = "TEST CMD" + + def simulatedFailure = new RuntimeException( "Simulated runtime failure" ) + + val expectation: PartialFunction[ Any, String ] = { + case "failing" => throw simulatedFailure + case "expected" => "ok" + } + + implicit class ExpectationBuilder[ T ]( val f: Future[ T ] => ExpectedFuture[ String ] ) extends AnyVal { + def apply( future: Future[ T ] ): ExpectedFuture[ String ] = f( future ) + } + + implicit class FutureBuilder[ T ]( 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/FailEagerlySpecs.scala b/src/test/scala/play/api/cache/redis/connector/FailEagerlySpecs.scala new file mode 100644 index 00000000..1ee63351 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/FailEagerlySpecs.scala @@ -0,0 +1,74 @@ +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 +/** + * @author Karel Cemus + */ +class FailEagerlySpecs( implicit ee: ExecutionEnv ) extends Specification with WithApplication { + + import FailEagerlySpecs._ + import Implicits._ + import MockitoImplicits._ + + "FailEagerly" should { + + "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 + } + + "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 ) + } + + "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 ) + } + } +} + +object FailEagerlySpecs { + + import redis.RedisCommand + import redis.protocol.RedisReply + + trait RedisCommandTest extends RedisCommand[ RedisReply, String ] { + def returning: Future[ String ] + } + + class FailEagerlyBase( implicit system: ActorSystem ) extends RequestTimeout { + protected implicit val scheduler = system.scheduler + implicit val executionContext = system.dispatcher + + def send[ T ]( redisCommand: RedisCommand[ _ <: RedisReply, T ] ) = { + redisCommand.asInstanceOf[ RedisCommandTest ].returning.asInstanceOf[ Future[ T ] ] + } + } + + class FailEagerlyImpl( implicit system: ActorSystem ) extends FailEagerlyBase with FailEagerly { + def isConnected = connected + + def markConnected( ) = connected = true + + def markDisconnected( ) = connected = false + } +} diff --git a/src/test/scala/play/api/cache/redis/connector/FailingConnector.scala b/src/test/scala/play/api/cache/redis/connector/FailingConnector.scala deleted file mode 100644 index 6a2cd7de..00000000 --- a/src/test/scala/play/api/cache/redis/connector/FailingConnector.scala +++ /dev/null @@ -1,136 +0,0 @@ -package play.api.cache.redis.connector - -import scala.concurrent._ -import scala.concurrent.duration.Duration -import scala.reflect.ClassTag - -import play.api.cache.redis.{ExecutionFailedException, Synchronization} - -/** - * @author Karel Cemus - */ -object FailingConnector extends RedisConnector with Synchronization { - - implicit def context: ExecutionContext = ExecutionContext.Implicits.global - - private def theError = new IllegalStateException( "Redis connector failure reproduction" ) - - private def failKeyed( key: String, command: String ) = Future.failed { - ExecutionFailedException( Some( key ), command, theError ) - } - - private def failCommand( command: String ) = Future.failed { - ExecutionFailedException( None, command, theError ) - } - - def set( key: String, value: Any, expiration: Duration ): Future[ Unit ] = - failKeyed( key, "SET" ) - - def setIfNotExists( key: String, value: Any ): Future[ Boolean ] = - failKeyed( key, "SETNX" ) - - def mSet( keyValues: (String, Any)* ): Future[ Unit ] = - failKeyed( keyValues.map( _._1 ).mkString( " " ), "MSET" ) - - def mSetIfNotExist( keyValues: (String, Any)* ): Future[ Boolean ] = - failKeyed( keyValues.map( _._1 ).mkString( " " ), "MSETNX" ) - - def get[ T: ClassTag ]( key: String ): Future[ Option[ T ] ] = - failKeyed( key, "GET" ) - - def mGet[ T: ClassTag ]( keys: String* ): Future[ List[ Option[ T ] ] ] = - failKeyed( keys.mkString( " " ), "MGET" ) - - def expire( key: String, expiration: Duration ): Future[ Unit ] = - failKeyed( key, "EXPIRE" ) - - def remove( keys: String* ): Future[ Unit ] = - failKeyed( keys.mkString( " " ), "DEL" ) - - def matching( pattern: String ): Future[ Seq[ String ] ] = - failCommand( "KEYS" ) - - def invalidate( ): Future[ Unit ] = - failCommand( "FLUSHDB" ) - - def ping( ): Future[ Unit ] = - failCommand( "PING" ) - - def exists( key: String ): Future[ Boolean ] = - failKeyed( key, "EXISTS" ) - - def increment( key: String, by: Long ): Future[ Long ] = - failKeyed( key, "INCR" ) - - def append( key: String, value: String ): Future[ Long ] = - failKeyed( key, "APPEND" ) - - def listPrepend( key: String, value: Any* ) = - failKeyed( key, "LPUSH" ) - - def listAppend( key: String, value: Any* ) = - failKeyed( key, "RPUSH" ) - - def listSize( key: String ) = - failKeyed( key, "LLEN" ) - - def listSetAt( key: String, position: Int, value: Any ) = - failKeyed( key, "LSET" ) - - def listHeadPop[ T: ClassTag ]( key: String ) = - failKeyed( key, "LPOP" ) - - def listSlice[ T: ClassTag ]( key: String, start: Int, end: Int ) = - failKeyed( key, "LRANGE" ) - - def listRemove( key: String, value: Any, count: Int ) = - failKeyed( key, "LREM" ) - - def listTrim( key: String, start: Int, end: Int ) = - failKeyed( key, "LTRIM" ) - - def listInsert( key: String, pivot: Any, value: Any ) = - failKeyed( key, "LINSERT" ) - - def setAdd( key: String, value: Any* ) = - failKeyed( key, "SADD" ) - - def setSize( key: String ) = - failKeyed( key, "SCARD" ) - - def setMembers[ T: ClassTag ]( key: String ) = - failKeyed( key, "SMEMBERS" ) - - def setIsMember( key: String, value: Any ) = - failKeyed( key, "SISMEMBER" ) - - def setRemove( key: String, value: Any* ) = - failKeyed( key, "SREM" ) - - def hashRemove( key: String, field: String* ) = - failKeyed( key, "HREM" ) - - def hashExists( key: String, field: String ) = - failKeyed( key, "HEXISTS" ) - - def hashGet[ T: ClassTag ]( key: String, field: String ) = - failKeyed( key, "HGET" ) - - def hashGetAll[ T: ClassTag ]( key: String ) = - failKeyed( key, "HGETALL" ) - - def hashIncrement( key: String, field: String, incrementBy: Long ) = - failKeyed( key, "HINCRBY" ) - - def hashSize( key: String ) = - failKeyed( key, "HLEN" ) - - def hashKeys( key: String ) = - failKeyed( key, "HKEYS" ) - - def hashSet( key: String, field: String, value: Any ) = - failKeyed( key, "HSET" ) - - def hashValues[ T: ClassTag ]( key: String ) = - failKeyed( key, "HVALS" ) -} diff --git a/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala b/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala new file mode 100644 index 00000000..dcbae74c --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/MockedConnector.scala @@ -0,0 +1,33 @@ +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 + +/** + * @author Karel Cemus + */ +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/RedisClusterSpecs.scala b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpecs.scala new file mode 100644 index 00000000..74791c6c --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpecs.scala @@ -0,0 +1,50 @@ +package play.api.cache.redis.connector + +import scala.concurrent.ExecutionContext +import scala.concurrent.duration._ + +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 RedisClusterSpecs( implicit ee: ExecutionEnv ) extends Specification with BeforeAll with AfterAll with WithApplication { + + import Implicits._ + + implicit private val lifecycle = application.injector.instanceOf[ ApplicationLifecycle ] + + implicit private val runtime = RedisRuntime( "cluster", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation ) + + private val serializer = new AkkaSerializerImpl( system ) + + private val clusterInstance = RedisCluster( defaultCacheName, nodes = RedisHost( localhost, defaultPort ) :: Nil, defaults ) + +// todo enable cluster specs +// private val connector: RedisConnector = new RedisConnectorProvider( clusterInstance, serializer ).get +// +// val prefix = "cluster-test" +// +// "Redis cluster" should { +// +// "pong on ping" in new TestCase { +// connector.ping() must not( throwA[ Throwable ] ).await +// } +// } +// + def beforeAll( ) = { + // initialize the connector by flushing the database +// connector.matching( s"$prefix-*" ).flatMap( connector.remove ).await + } + + def afterAll( ) = { + lifecycle.stop() + } +} diff --git a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala new file mode 100644 index 00000000..0aa4ad91 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala @@ -0,0 +1,157 @@ +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 redis._ + +/** + * @author Karel Cemus + */ +class RedisConnectorFailureSpec( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + + import Implicits._ + + import org.mockito.ArgumentMatchers._ + + val key = "key" + val value = "value" + + val simulatedEx = new RuntimeException( "Simulated failure." ) + val simulatedFailure = Failure( simulatedEx ) + + val someValue = Some( value ) + + val disconnected = Future.failed( new IllegalStateException( "Simulated redis status: disconnected." ) ) + + def anySerializer = org.mockito.ArgumentMatchers.any[ ByteStringSerializer[ String ] ] + def anyDeserializer = org.mockito.ArgumentMatchers.any[ ByteStringDeserializer[ String ] ] + + "Serializer failure" should { + + "fail when serialization fails" in new MockedConnector { + serializer.encode( any[ Any ] ) returns simulatedFailure + // run the test + connector.set( key, value ) must throwA[ SerializationException ].await + } + + "fail when decoder fails" in new MockedConnector { + serializer.decode( anyString )( any[ ClassTag[ _ ] ] ) returns simulatedFailure + commands.get[ String ]( key ) returns someValue + // run the test + connector.get[ String ]( key ) must throwA[ SerializationException ].await + } + } + + "Redis returning error code" should { + + "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 + } + + "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 + } + } + + "Connector failure" should { + + "failed SETEX" in new MockedConnector { + serializer.encode( anyString ) returns "encoded" + commands.setex( anyString, anyLong, anyString )( anySerializer ) returns disconnected + // run the test + connector.set( key, value, 1.minute ) must throwA[ ExecutionFailedException ].await + } + + "failed SETNX" in new MockedConnector { + serializer.encode( anyString ) returns "encoded" + commands.setnx( anyString, anyString )( anySerializer ) returns disconnected + // run the test + connector.setIfNotExists( key, value ) must throwA[ ExecutionFailedException ].await + } + + "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 + } + + "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 + } + + "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 + } + + "failed EXPIRE" in new MockedConnector { + commands.expire( anyString, anyLong ) returns disconnected + // run the test + connector.expire( key, 1.minute ) must throwA[ ExecutionFailedException ].await + } + + "failed INCRBY" in new MockedConnector { + commands.incrby( anyString, anyLong ) returns disconnected + // run the test + connector.increment( key, 1L ) must throwA[ ExecutionFailedException ].await + } + + "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 + } + + "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 + } + + "failed LTRIM" in new MockedConnector { + commands.ltrim( anyString, anyLong, anyLong ) returns disconnected + // run the test + connector.listTrim( key, 1, 5 ) must throwA[ ExecutionFailedException ].await + } + + "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 + } + + "failed HINCRBY" in new MockedConnector { + commands.hincrby( anyString, anyString, anyLong ) returns disconnected + // run the test + connector.hashIncrement( key, "field", 1 ) must throwA[ ExecutionFailedException ].await + } + + "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 + } + } +} diff --git a/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala index 0f3fa98a..6e024a43 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala @@ -1,413 +1,403 @@ package play.api.cache.redis.connector -import java.util.Date - import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} import play.api.cache.redis._ +import play.api.cache.redis.impl._ +import play.api.inject.ApplicationLifecycle -import org.joda.time.DateTime +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 RedisConnectorSpec extends Specification with Redis { +class RedisConnectorSpec( implicit ee: ExecutionEnv ) extends Specification with BeforeAll with AfterAll with WithApplication { + import Implicits._ - private type Cache = RedisConnector + implicit private val lifecycle = application.injector.instanceOf[ ApplicationLifecycle ] - private val Cache = injector.instanceOf[ Cache ] + implicit private val runtime = RedisRuntime( "connector", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation ) - private val prefix = "connector" + private val serializer = new AkkaSerializerImpl( system ) - "RedisConnector" should { + private val connector: RedisConnector = new RedisConnectorProvider( defaultInstance, serializer ).get - "miss on get" in { - Cache.get[ String ]( s"$prefix-test-1" ) must beNone - } + val prefix = "connector-test" - "hit after set" in { - Cache.set( s"$prefix-test-2", "value" ).sync - Cache.get[ String ]( s"$prefix-test-2" ) must beSome[ Any ] - Cache.get[ String ]( s"$prefix-test-2" ) must beSome( "value" ) - } + "RedisConnector" should { - "ignore set if not exists when already defined" in { - Cache.set( s"$prefix-test-if-not-exists-when-exists", "previous" ).sync - Cache.setIfNotExists( s"$prefix-test-if-not-exists-when-exists", "value" ) must beFalse - Cache.get[ String ]( s"$prefix-test-if-not-exists-when-exists" ) must beSome[ Any ] - Cache.get[ String ]( s"$prefix-test-if-not-exists-when-exists" ) must beSome( "previous" ) + "pong on ping" in new TestCase { + connector.ping( ) must not( throwA[ Throwable ] ).await } - "perform set if not exists when undefined" in { - Cache.setIfNotExists( s"$prefix-test-if-not-exists", "value" ) must beTrue - Cache.get[ String ]( s"$prefix-test-if-not-exists" ) must beSome[ Any ] - Cache.get[ String ]( s"$prefix-test-if-not-exists" ) must beSome( "value" ) + "miss on get" in new TestCase { + connector.get[ String ]( s"$prefix-$idx" ) must beNone.await } - "hit after mset" in { - Cache.mSet( s"$prefix-test-mset-1" -> "value-1", s"$prefix-test-mset-2" -> "value-2" ).sync - Cache.mGet[ String ]( s"$prefix-test-mset-1", s"$prefix-test-mset-2" ).sync must beEqualTo( List( Some( "value-1" ), Some( "value-2" ) ) ) + "hit after set" in new TestCase { + connector.set( s"$prefix-$idx", "value" ).await + connector.get[ String ]( s"$prefix-$idx" ) must beSome[ Any ].await + connector.get[ String ]( s"$prefix-$idx" ) must beSome( "value" ).await } - "ignore msetnx if already defined" in { - Cache.mSetIfNotExist( s"$prefix-test-msetnx-1" -> "value-1", s"$prefix-test-msetnx-2" -> "value-2" ) must beTrue - Cache.mGet[ String ]( s"$prefix-test-msetnx-1", s"$prefix-test-msetnx-2" ).sync must beEqualTo( List( Some( "value-1" ), Some( "value-2" ) ) ) - Cache.mSetIfNotExist( s"$prefix-test-msetnx-3" -> "value-3", s"$prefix-test-msetnx-2" -> "value-2" ) must beFalse + "ignore set if not exists when already defined" in new TestCase { + connector.set( s"$prefix-if-not-exists-when-exists", "previous" ).await + connector.setIfNotExists( s"$prefix-if-not-exists-when-exists", "value" ) must beFalse.await + connector.get[ String ]( s"$prefix-if-not-exists-when-exists" ) must beSome[ Any ].await + connector.get[ String ]( s"$prefix-if-not-exists-when-exists" ) must beSome( "previous" ).await } - "expire refreshes expiration" in { - Cache.set( s"$prefix-test-10", "value", 2.second ).sync - Cache.get[ String ]( s"$prefix-test-10" ) must beSome( "value" ) - Cache.expire( s"$prefix-test-10", 1.minute ).sync - // wait until the first duration expires - Thread.sleep( 3000 ) - Cache.get[ String ]( s"$prefix-test-10" ) must beSome( "value" ) + "perform set if not exists when undefined" in new TestCase { + connector.setIfNotExists( s"$prefix-if-not-exists", "value" ) must beTrue.await + connector.get[ String ]( s"$prefix-if-not-exists" ) must beSome[ Any ].await + connector.get[ String ]( s"$prefix-if-not-exists" ) must beSome( "value" ).await } - "positive exists on existing keys" in { - Cache.set( s"$prefix-test-11", "value" ).sync - Cache.exists( s"$prefix-test-11" ) must beTrue + "hit after mset" in new TestCase { + connector.mSet( s"$prefix-mset-$idx-1" -> "value-1", s"$prefix-mset-$idx-2" -> "value-2" ).await + 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 ).await + 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 ).await + 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 } - "negative exists on expired and missing keys" in { - Cache.set( s"$prefix-test-12A", "value", 1.second ).sync - // wait until the duration expires - Thread.sleep( 2000 ) - Cache.exists( s"$prefix-test-12A" ) must beFalse - Cache.exists( s"$prefix-test-12B" ) must beFalse + "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 } - "miss after remove" in { - Cache.set( s"$prefix-test-3", "value" ).sync - Cache.get[ String ]( s"$prefix-test-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-3" ).sync - Cache.get[ String ]( s"$prefix-test-3" ) must beNone + "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 ).await + // 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 } - "miss after timeout" in { - // set - Cache.set( s"$prefix-test-4", "value", 1.second ).sync - Cache.get[ String ]( s"$prefix-test-4" ) must beSome[ Any ] - // wait until it expires - Thread.sleep( 1500 ) - // miss - Cache.get[ String ]( s"$prefix-test-4" ) must beNone + "positive exists on existing keys" in new TestCase { + connector.set( s"$prefix-$idx", "value" ).await + connector.exists( s"$prefix-$idx" ) must beTrue.await } - "find all matching keys" in { - Cache.set( s"$prefix-test-13-key-A", "value", 3.second ).sync - Cache.set( s"$prefix-test-13-note-A", "value", 3.second ).sync - Cache.set( s"$prefix-test-13-key-B", "value", 3.second ).sync - Cache.matching( s"$prefix-test-13*" ).sync.sorted mustEqual Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-note-A", s"$prefix-test-13-key-B" ).sorted - Cache.matching( s"$prefix-test-13*A" ).sync.sorted mustEqual Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-note-A" ).sorted - Cache.matching( s"$prefix-test-13-key-*" ).sync.sorted mustEqual Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-key-B" ).sorted - Cache.matching( s"$prefix-test-13A*" ).sync mustEqual Seq.empty + "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 } - "support list" in { - // store value - Cache.set( s"$prefix-list", List( "A", "B", "C" ) ).sync - // recall - Cache.get[ List[ String ] ]( s"$prefix-list" ) must beSome[ List[ String ] ]( List( "A", "B", "C" ) ) + "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 } - "support a byte" in { - Cache.set( s"$prefix-type.byte", 0xAB.toByte ).sync - Cache.get[ Byte ]( s"$prefix-type.byte" ) must beSome[ Byte ] - Cache.get[ Byte ]( s"$prefix-type.byte" ) must beSome( 0xAB.toByte ) + "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 } - "support a char" in { - Cache.set( s"$prefix-type.char.1", 'a' ).sync - Cache.get[ Char ]( s"$prefix-type.char.1" ) must beSome[ Char ] - Cache.get[ Char ]( s"$prefix-type.char.1" ) must beSome( 'a' ) - Cache.set( s"$prefix-type.char.2", 'b' ).sync - Cache.get[ Char ]( s"$prefix-type.char.2" ) must beSome( 'b' ) - Cache.set( s"$prefix-type.char.3", 'č' ).sync - Cache.get[ Char ]( s"$prefix-type.char.3" ) must beSome( 'č' ) + "remove with empty args" in new TestCase { + val toBeRemoved = List.empty + connector.remove( toBeRemoved: _* ) must not( throwA[ Throwable ] ).await } - "support a short" in { - Cache.set( s"$prefix-type.short", 12.toShort ).sync - Cache.get[ Short ]( s"$prefix-type.short" ) must beSome[ Short ] - Cache.get[ Short ]( s"$prefix-type.short" ) must beSome( 12.toShort ) + "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 } - "support an int" in { - Cache.set( s"$prefix-type.int", 15 ).sync - Cache.get[ Int ]( s"$prefix-type.int" ) must beSome( 15 ) + "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 } - "support a long" in { - Cache.set( s"$prefix-type.long", 144L ).sync - Cache.get[ Long ]( s"$prefix-type.long" ) must beSome[ Long ] - Cache.get[ Long ]( s"$prefix-type.long" ) must beSome( 144L ) + "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 } - "support a float" in { - Cache.set( s"$prefix-type.float", 1.23f ).sync - Cache.get[ Float ]( s"$prefix-type.float" ) must beSome[ Float ] - Cache.get[ Float ]( s"$prefix-type.float" ) must beSome( 1.23f ) + "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" ).await + 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 } - "support a double" in { - Cache.set( s"$prefix-type.double", 3.14 ).sync - Cache.get[ Double ]( s"$prefix-type.double" ) must beSome[ Double ] - Cache.get[ Double ]( s"$prefix-type.double" ) must beSome( 3.14 ) + "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" ).await + 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 } - "support a date" in { - Cache.set( s"$prefix-type.date", new Date( 123 ) ).sync - Cache.get[ Date ]( s"$prefix-type.date" ) must beSome( new Date( 123 ) ) + "set a zero when not exists and then increment" in new TestCase { + connector.increment( s"$prefix-incr-null", 1 ) must beEqualTo( 1 ).await } - "support a datetime" in { - Cache.set( s"$prefix-type.datetime", new DateTime( 123456 ) ).sync - Cache.get[ DateTime ]( s"$prefix-type.datetime" ) must beSome( new DateTime( 123456 ) ) + "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 } - "support a custom classes" in { - Cache.set( s"$prefix-type.object", SimpleObject( "B", 3 ) ).sync - Cache.get[ SimpleObject ]( s"$prefix-type.object" ) must beSome( SimpleObject( "B", 3 ) ) + "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 } - "support a null" in { - Cache.set( s"$prefix-type.null", null ).sync - Cache.get[ SimpleObject ]( s"$prefix-type.null" ) must beNone + "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 } - "remove multiple keys at once" in { - Cache.set( s"$prefix-test-remove-multiple-1", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-multiple-2", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-multiple-3", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-remove-multiple-1", s"$prefix-test-remove-multiple-2", s"$prefix-test-remove-multiple-3" ).sync - Cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must beNone + "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 } - "remove in batch" in { - Cache.set( s"$prefix-test-remove-batch-1", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-batch-1" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-batch-2", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-batch-2" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-batch-3", "value" ).sync - Cache.get[ String ]( s"$prefix-test-remove-batch-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-remove-batch-1", s"$prefix-test-remove-batch-2", s"$prefix-test-remove-batch-3" ).sync - Cache.get[ String ]( s"$prefix-test-remove-batch-1" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-batch-2" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-batch-3" ) must beNone + "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 } - "set a zero when not exists and then increment" in { - Cache.increment( s"$prefix-test-incr-null", 1 ).sync must beEqualTo( 1 ) + "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" ).await + connector.get[ String ]( s"$prefix-append-to-null" ) must beSome( "value" ).await } - "throw an exception when not integer" in { - Cache.set( s"$prefix-test-incr-string", "value" ).sync - Cache.increment( s"$prefix-test-incr-string", 1 ).sync must throwA[ ExecutionFailedException ] + "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" ).await + connector.get[ String ]( s"$prefix-append-to-some" ) must beSome( "some value" ).await } - "increment by one" in { - Cache.set( s"$prefix-test-incr-by-one", 5 ).sync - Cache.increment( s"$prefix-test-incr-by-one", 1 ).sync must beEqualTo( 6 ) - Cache.increment( s"$prefix-test-incr-by-one", 1 ).sync must beEqualTo( 7 ) - Cache.increment( s"$prefix-test-incr-by-one", 1 ).sync must beEqualTo( 8 ) + "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 } - "increment by some" in { - Cache.set( s"$prefix-test-incr-by-some", 5 ).sync - Cache.increment( s"$prefix-test-incr-by-some", 1 ).sync must beEqualTo( 6 ) - Cache.increment( s"$prefix-test-incr-by-some", 2 ).sync must beEqualTo( 8 ) - Cache.increment( s"$prefix-test-incr-by-some", 3 ).sync must beEqualTo( 11 ) + "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 } - "decrement by one" in { - Cache.set( s"$prefix-test-decr-by-one", 5 ).sync - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( 4 ) - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( 3 ) - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( 2 ) - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( 1 ) - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( 0 ) - Cache.increment( s"$prefix-test-decr-by-one", -1 ).sync must beEqualTo( -1 ) + "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 } - "decrement by some" in { - Cache.set( s"$prefix-test-decr-by-some", 5 ).sync - Cache.increment( s"$prefix-test-decr-by-some", -1 ).sync must beEqualTo( 4 ) - Cache.increment( s"$prefix-test-decr-by-some", -2 ).sync must beEqualTo( 2 ) - Cache.increment( s"$prefix-test-decr-by-some", -3 ).sync must beEqualTo( -1 ) + "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" ).await + 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 } - "append like set when value is undefined" in { - Cache.get[ String ]( s"$prefix-test-append-to-null" ) must beNone - Cache.append( s"$prefix-test-append-to-null", "value" ).sync - Cache.get[ String ]( s"$prefix-test-append-to-null" ) must beSome( "value" ) + "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 } - "append to existing string" in { - Cache.set( s"$prefix-test-append-to-some", "some" ).sync - Cache.get[ String ]( s"$prefix-test-append-to-some" ) must beSome( "some" ) - Cache.append( s"$prefix-test-append-to-some", " value" ).sync - Cache.get[ String ]( s"$prefix-test-append-to-some" ) must beSome( "some value" ) + "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 push left" in { - Cache.listPrepend( s"$prefix-test-list-prepend", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.listPrepend( s"$prefix-test-list-prepend", "D", "E", "F" ).sync must beEqualTo( 6 ) - Cache.listSlice[ String ]( s"$prefix-test-list-prepend", 0, -1 ).sync must beEqualTo( List( "F", "E", "D", "C", "B", "A" ) ) + "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 push right" in { - Cache.listAppend( s"$prefix-test-list-append", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.listAppend( s"$prefix-test-list-append", "D", "E", "A" ).sync must beEqualTo( 6 ) - Cache.listSlice[ String ]( s"$prefix-test-list-append", 0, -1 ).sync must beEqualTo( List( "A", "B", "C", "D", "E", "A" ) ) + "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 ).await + 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 size" in { - Cache.listSize( s"$prefix-test-list-size" ).sync must beEqualTo( 0 ) - Cache.listPrepend( s"$prefix-test-list-size", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.listSize( s"$prefix-test-list-size" ).sync must beEqualTo( 3 ) + "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 overwrite at index" in { - Cache.listPrepend( s"$prefix-test-list-set", "C", "B", "A" ).sync must beEqualTo( 3 ) - Cache.listSetAt( s"$prefix-test-list-set", 1, "D" ).sync - Cache.listSlice[ String ]( s"$prefix-test-list-set", 0, -1 ).sync must beEqualTo( List( "A", "D", "C" ) ) - Cache.listSetAt( s"$prefix-test-list-set", 3, "D" ).sync must throwA[ IndexOutOfBoundsException ] + "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 } - "list pop head" in { - Cache.listHeadPop[ String ]( s"$prefix-test-list-pop" ).sync must beNone - Cache.listPrepend( s"$prefix-test-list-pop", "C", "B", "A" ).sync must beEqualTo( 3 ) - Cache.listHeadPop[ String ]( s"$prefix-test-list-pop" ).sync must beSome( "A" ) - Cache.listHeadPop[ String ]( s"$prefix-test-list-pop" ).sync must beSome( "B" ) - Cache.listHeadPop[ String ]( s"$prefix-test-list-pop" ).sync must beSome( "C" ) - Cache.listHeadPop[ String ]( s"$prefix-test-list-pop" ).sync must beNone + "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 } - "list slice view" in { - Cache.listSlice[ String ]( s"$prefix-test-list-slice", 0, -1 ).sync must beEqualTo( List.empty ) - Cache.listPrepend( s"$prefix-test-list-slice", "C", "B", "A" ).sync must beEqualTo( 3 ) - Cache.listSlice[ String ]( s"$prefix-test-list-slice", 0, -1 ).sync must beEqualTo( List( "A", "B", "C" ) ) - Cache.listSlice[ String ]( s"$prefix-test-list-slice", 0, 0 ).sync must beEqualTo( List( "A" ) ) - Cache.listSlice[ String ]( s"$prefix-test-list-slice", -2, -1 ).sync must beEqualTo( List( "B", "C" ) ) + "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 } - "list remove by value" in { - Cache.listRemove( s"$prefix-test-list-remove", "A", count = 1 ).sync must beEqualTo( 0 ) - Cache.listPrepend( s"$prefix-test-list-remove", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.listRemove( s"$prefix-test-list-remove", "A", count = 1 ).sync must beEqualTo( 1 ) - Cache.listSize( s"$prefix-test-list-remove" ).sync must beEqualTo( 2 ) - } + "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 - "list trim" in { - Cache.listPrepend( s"$prefix-test-list-trim", "C", "B", "A" ).sync must beEqualTo( 3 ) - Cache.listTrim( s"$prefix-test-list-trim", 1, 2 ).sync - Cache.listSize( s"$prefix-test-list-trim" ).sync must beEqualTo( 2 ) - Cache.listSlice[ String ]( s"$prefix-test-list-trim", 0, -1 ).sync must beEqualTo( List( "B", "C" ) ) - } + 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 - "list insert" in { - Cache.listSize( s"$prefix-test-list-insert-1" ).sync must beEqualTo( 0 ) - Cache.listInsert( s"$prefix-test-list-insert-1", "C", "B" ).sync must beNone - Cache.listPrepend( s"$prefix-test-list-insert-1", "C", "A" ).sync must beEqualTo( 2 ) - Cache.listInsert( s"$prefix-test-list-insert-1", "C", "B" ).sync must beSome( 3 ) - Cache.listInsert( s"$prefix-test-list-insert-1", "E", "D" ).sync must beNone - Cache.listSlice[ String ]( s"$prefix-test-list-insert-1", 0, -1 ).sync must beEqualTo( List( "A", "B", "C" ) ) + connector.setAdd( s"$prefix-set-rank", "C", "B" ) must beEqualTo( 1 ).await - Cache.set( s"$prefix-test-list-insert-2", "string value" ).sync - Cache.listInsert( s"$prefix-test-list-insert-2", "C", "B" ).sync must throwA[ IllegalArgumentException ] + 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 add" in { - Cache.setSize( s"$prefix-test-set-add" ).sync must beEqualTo( 0 ) - Cache.setAdd( s"$prefix-test-set-add", "A", "B" ).sync must beEqualTo( 2 ) - Cache.setSize( s"$prefix-test-set-add" ).sync must beEqualTo( 2 ) - Cache.setAdd( s"$prefix-test-set-add", "C", "B" ).sync must beEqualTo( 1 ) - Cache.setSize( s"$prefix-test-set-add" ).sync must beEqualTo( 3 ) + "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 rank" in { - Cache.setSize( s"$prefix-test-set-rank" ).sync must beEqualTo( 0 ) - Cache.setAdd( s"$prefix-test-set-rank", "A", "B" ).sync must beEqualTo( 2 ) - Cache.setSize( s"$prefix-test-set-rank" ).sync must beEqualTo( 2 ) + "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 - Cache.setIsMember( s"$prefix-test-set-rank", "A" ).sync must beTrue - Cache.setIsMember( s"$prefix-test-set-rank", "B" ).sync must beTrue - Cache.setIsMember( s"$prefix-test-set-rank", "C" ).sync must beFalse - - Cache.setAdd( s"$prefix-test-set-rank", "C", "B" ).sync must beEqualTo( 1 ) - - Cache.setIsMember( s"$prefix-test-set-rank", "A" ).sync must beTrue - Cache.setIsMember( s"$prefix-test-set-rank", "B" ).sync must beTrue - Cache.setIsMember( s"$prefix-test-set-rank", "C" ).sync must beTrue + 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 size" in { - Cache.setSize( s"$prefix-test-set-size" ).sync must beEqualTo( 0 ) - Cache.setAdd( s"$prefix-test-set-size", "A", "B" ).sync must beEqualTo( 2 ) - Cache.setSize( s"$prefix-test-set-size" ).sync must beEqualTo( 2 ) - } + "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 - "set rem" in { - Cache.setSize( s"$prefix-test-set-rem" ).sync must beEqualTo( 0 ) - Cache.setAdd( s"$prefix-test-set-rem", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.setSize( s"$prefix-test-set-rem" ).sync must beEqualTo( 3 ) + connector.setMembers[ String ]( s"$prefix-set-slice" ) must beEqualTo( Set( "A", "B", "C" ) ).await - Cache.setRemove( s"$prefix-test-set-rem", "A" ).sync must beEqualTo( 1 ) - Cache.setSize( s"$prefix-test-set-rem" ).sync must beEqualTo( 2 ) - Cache.setRemove( s"$prefix-test-set-rem", "B", "C", "D" ).sync must beEqualTo( 2 ) - Cache.setSize( s"$prefix-test-set-rem" ).sync must beEqualTo( 0 ) + connector.setSize( s"$prefix-set-slice" ) must beEqualTo( 3 ).await } - "set slice" in { - Cache.setSize( s"$prefix-test-set-slice" ).sync must beEqualTo( 0 ) - Cache.setAdd( s"$prefix-test-set-slice", "A", "B", "C" ).sync must beEqualTo( 3 ) - Cache.setSize( s"$prefix-test-set-slice" ).sync must beEqualTo( 3 ) + "hash set values" in new TestCase { + val key = s"$prefix-hash-set" - Cache.setMembers[ String ]( s"$prefix-test-set-slice" ).sync must beEqualTo( Set( "A", "B", "C" ) ) + connector.hashSize( key ) must beEqualTo( 0 ).await + connector.hashGetAll( key ) must beEqualTo( Map.empty ).await + connector.hashKeys( key ) must beEqualTo( Set.empty ).await + connector.hashValues[ String ]( key ) must beEqualTo( Set.empty ).await - Cache.setSize( s"$prefix-test-set-slice" ).sync must beEqualTo( 3 ) - } - - "hash set values" in { - val key = s"$prefix-test-hash-set" + 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 - Cache.hashSize( key ).sync must beEqualTo( 0 ) - Cache.hashGetAll( key ).sync must beEqualTo( Map.empty ) - Cache.hashKeys( key ).sync must beEqualTo( Set.empty ) - Cache.hashValues[ String ]( key ).sync must beEqualTo( Set.empty ) + connector.hashExists( key, "KB" ) must beTrue.await + connector.hashExists( key, "KC" ) must beFalse.await - Cache.hashGet[ String ]( key, "KA" ).sync must beNone - Cache.hashSet( key, "KA", "VA1" ).sync must beTrue - Cache.hashGet[ String ]( key, "KA" ).sync must beSome( "VA1" ) - Cache.hashSet( key, "KA", "VA2" ).sync must beFalse - Cache.hashGet[ String ]( key, "KA" ).sync must beSome( "VA2" ) - Cache.hashSet( key, "KB", "VB" ).sync must beTrue + 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 - Cache.hashExists( key, "KB" ).sync must beTrue - Cache.hashExists( key, "KC" ).sync must beFalse + 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 - Cache.hashSize( key ).sync must beEqualTo( 2 ) - Cache.hashGetAll[ String ]( key ).sync must beEqualTo( Map( "KA" -> "VA2", "KB" -> "VB" ) ) - Cache.hashKeys( key ).sync must beEqualTo( Set( "KA", "KB" ) ) - Cache.hashValues[ String ]( key ).sync must beEqualTo( Set( "VA2", "VB" ) ) + 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 - Cache.hashRemove( key, "KB" ).sync must beEqualTo( 1 ) - Cache.hashRemove( key, "KC" ).sync must beEqualTo( 0 ) - Cache.hashExists( key, "KB" ).sync must beFalse - Cache.hashExists( key, "KA" ).sync must beTrue + 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 + } - Cache.hashSize( key ).sync must beEqualTo( 1 ) - Cache.hashGetAll[ String ]( key ).sync must beEqualTo( Map( "KA" -> "VA2" ) ) - Cache.hashKeys( key ).sync must beEqualTo( Set( "KA" ) ) - Cache.hashValues[ String ]( key ).sync must beEqualTo( Set( "VA2" ) ) + "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 } } + def beforeAll( ) = { + // initialize the connector by flushing the database + connector.matching( s"$prefix-*" ).flatMap( connector.remove ).await + } + + def afterAll( ) = { + lifecycle.stop() + } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpecs.scala b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpecs.scala new file mode 100644 index 00000000..74b52648 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/RedisRequestTimeoutSpecs.scala @@ -0,0 +1,51 @@ +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 + +/** + * @author Karel Cemus + */ +class RedisRequestTimeoutSpecs( implicit ee: ExecutionEnv ) extends Specification with WithApplication { + + import Implicits._ + import MockitoImplicits._ + import RedisRequestTimeoutSpecs._ + + "RedisRequestTimeout" should { + + "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" ) + // run the test + impl.send[ String ]( cmd ) must throwA[ redis.actors.NoConnectionException.type ].awaitFor( 5.seconds ) + } + } +} + +object RedisRequestTimeoutSpecs { + + import redis.RedisCommand + import redis.protocol.RedisReply + + trait RedisCommandTest extends RedisCommand[ RedisReply, String ] { + def returning: Future[ String ] + } + + class RequestTimeoutBase( implicit system: ActorSystem ) extends RequestTimeout { + protected implicit val scheduler = system.scheduler + implicit val executionContext = system.dispatcher + + def send[ T ]( redisCommand: RedisCommand[ _ <: RedisReply, T ] ) = { + redisCommand.asInstanceOf[ RedisCommandTest ].returning.asInstanceOf[ Future[ T ] ] + } + } + + class RedisRequestTimeoutImpl( val timeout: Option[ FiniteDuration ] )( implicit system: ActorSystem ) extends RequestTimeoutBase with RedisRequestTimeout +} diff --git a/src/test/scala/play/api/cache/redis/connector/RediscalaSpec.scala b/src/test/scala/play/api/cache/redis/connector/RediscalaSpec.scala deleted file mode 100644 index 56561c64..00000000 --- a/src/test/scala/play/api/cache/redis/connector/RediscalaSpec.scala +++ /dev/null @@ -1,73 +0,0 @@ -package play.api.cache.redis.connector - -import scala.concurrent.Future - -import play.api.cache.redis.{Redis, TestRedisInstance} - -import org.specs2.matcher.MustExpectable -import org.specs2.mutable.Specification - -/** - *Test of brando to be sure that it works etc.
- */ -class RediscalaSpec extends Specification with Redis with TestRedisInstance { - - sequential - - private implicit def theFutureValue[ T ]( t: => Future[ T ] ): MustExpectable[ T ] = createMustExpectable( t.sync ) - - "ScRedis" should { - - "ping" in { - redis.ping() must beEqualTo( "PONG" ) - } - - "set value" in { - redis.set( "some-key", "this-value" ) must beTrue - } - - "set values" in { - redis.mset( Map( "some-key1" -> "this-value", "some-key2" -> "that-value" ) ) must not( beNull ) - } - - "get stored value" in { - redis.get[ String ]( "some-key" ) must beSome( "this-value" ) - } - - "get stored values" in { - redis.mget[ String ]( "some-key1", "some-key2" ) must beEqualTo( List( Some( "this-value" ), Some( "that-value" ) ) ) - } - - "get non-existing value" in { - redis.get[ String ]( "non-existing" ) must beNone - } - - "determine whether it contains already stored keys" in { - redis.exists( "some-key" ) must beTrue - redis.exists( "some-key1" ) must beTrue - redis.exists( "some-key2" ) must beTrue - } - - "determine whether it contains non-existent key" in { - redis.exists( "non-existing" ) must beFalse - } - - "delete stored value" in { - redis.del( "some-key", "some-key1", "some-key2" ) must beEqualTo( 3L ) - } - - "delete already deleted value" in { - redis.del( "some-key" ) must beEqualTo( 0L ) - } - - "delete non-existing value" in { - redis.del( "non-existing" ) must beEqualTo( 0L ) - } - - "determine whether it contains deleted keys" in { - redis.exists( "some-key" ) must beFalse - redis.exists( "some-key1" ) must beFalse - redis.exists( "some-key2" ) must beFalse - } - } -} diff --git a/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala b/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala index 247ed3bb..eb849375 100644 --- a/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala +++ b/src/test/scala/play/api/cache/redis/connector/SerializerSpecs.scala @@ -4,19 +4,21 @@ import java.util.Date import scala.reflect.ClassTag -import play.api.cache.redis.{Redis, SimpleObject} +import play.api.inject.guice.GuiceApplicationBuilder import org.joda.time.{DateTime, DateTimeZone} +import org.specs2.mock.Mockito import org.specs2.mutable.Specification /** * @author Karel Cemus */ -class SerializerSpecs extends Specification { +class SerializerSpecs extends Specification with Mockito { + import SerializerSpecs._ - import play.api.cache.redis.TestHelpers._ + private val system = GuiceApplicationBuilder().build().actorSystem - private implicit val serializer: AkkaSerializer = Redis.injector.instanceOf[ AkkaSerializer ] + private implicit val serializer: AkkaSerializer = new AkkaSerializerImpl( system ) "AkkaEncoder" should "encode" >> { @@ -30,6 +32,10 @@ class SerializerSpecs extends Specification { 'š'.encoded mustEqual "š" } + "boolean" in { + true.encoded mustEqual "true" + } + "short" in { 12.toShort.toByte.encoded mustEqual "12" } @@ -69,10 +75,11 @@ class SerializerSpecs extends Specification { } "custom classes" in { - SimpleObject( "B", 3 ).encoded mustEqual """ - |rO0ABXNyACFwbGF5LmFwaS5jYWNoZS5yZWRpcy5TaW1wbGVPYmplY3Te6Peta/KhNAIAAkkABXZh - |bHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJpbmc7eHAAAAADdAABQg== - """.stripMargin.trim + SimpleObject( "B", 3 ).encoded mustEqual """ + |rO0ABXNyADtwbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplclNwZWNzJFNp + |bXBsZU9iamVjdMm6wvThiaEsAgACSQAFdmFsdWVMAANrZXl0ABJMamF2YS9sYW5nL1N0cmluZzt4 + |cAAAAAN0AAFC + """.stripMargin.trim } "null" in { @@ -81,10 +88,10 @@ class SerializerSpecs extends Specification { "list" in { List( "A", "B", "C" ).encoded mustEqual """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 - |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu - |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== - """.stripMargin.trim + |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 + |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu + |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== + """.stripMargin.trim } } @@ -101,6 +108,10 @@ class SerializerSpecs extends Specification { "š".decoded[ Char ] mustEqual 'š' } + "boolean" in { + "true".decoded[ Boolean ] mustEqual true + } + "short" in { "12".decoded[ Short ] mustEqual 12.toShort.toByte } @@ -125,6 +136,10 @@ class SerializerSpecs extends Specification { "some string".decoded[ String ] mustEqual "some string" } + "null" in { + "".decoded[ String ] must beNull + } + "date" in { "rO0ABXNyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAAAAAAB7eA==".decoded[ Date ] mustEqual new Date( 123 ) } @@ -141,8 +156,9 @@ class SerializerSpecs extends Specification { "custom classes" in { """ - |rO0ABXNyACFwbGF5LmFwaS5jYWNoZS5yZWRpcy5TaW1wbGVPYmplY3Te6Peta/KhNAIAAkkABXZh - |bHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJpbmc7eHAAAAADdAABQg== + |rO0ABXNyADtwbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplclNwZWNzJFNp + |bXBsZU9iamVjdMm6wvThiaEsAgACSQAFdmFsdWVMAANrZXl0ABJMamF2YS9sYW5nL1N0cmluZzt4 + |cAAAAAN0AAFC """.stripMargin.trim.decoded[ SimpleObject ] mustEqual SimpleObject( "B", 3 ) } @@ -154,6 +170,23 @@ class SerializerSpecs extends Specification { """.stripMargin.trim.decoded[ List[ String ] ] mustEqual List( "A", "B", "C" ) } + "forgotten type" in { + def decoded: String = "something".decoded + decoded must throwA[ IllegalArgumentException ] + } + } +} + +object SerializerSpecs { + + 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 } + /** Plain test object to be cached */ + 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 new file mode 100644 index 00000000..681fb0c8 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/connector/TestCase.scala @@ -0,0 +1,23 @@ +package play.api.cache.redis.connector + +import java.util.concurrent.atomic.AtomicInteger + +import org.specs2.execute.{AsResult, Result} +import org.specs2.specification.{Around, Scope} + +/** + * @author Karel Cemus + */ +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/AsyncRedisSpecs.scala b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpecs.scala new file mode 100644 index 00000000..476c5ece --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpecs.scala @@ -0,0 +1,69 @@ +package play.api.cache.redis.impl + +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class AsyncRedisSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + import Implicits._ + import RedisCacheImplicits._ + + import org.mockito.ArgumentMatchers._ + + "AsyncRedis" should { + + "removeAll" in new MockedAsyncRedis { + connector.invalidate( ) returns unit + cache.removeAll() must beDone.await + there was one( connector ).invalidate( ) + } + + "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 + } + + "getOrElseUpdate (miss)" in new MockedAsyncRedis with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.getOrElseUpdate( key )( doFuture( value ) ) must beEqualTo( value ).await + orElse mustEqual 1 + } + + "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 + } + + "getOrElseUpdate (failing orElse)" in new MockedAsyncRedis with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + cache.getOrElseUpdate( key )( failedFuture ) must throwA[ TimeoutException ].await + orElse mustEqual 2 + } + + "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 ] ) returns unit + // run the test + cache.getOrElseUpdate( key ) { attempts match { + case 0 => attempt( failedFuture ) + case _ => attempt( doFuture( value ) ) + } } must beEqualTo( value ).await + // verification + orElse mustEqual 2 + MockitoImplicits.there were MockitoImplicits.two( connector ).get[ String ]( anyString )( anyClassTag ) + MockitoImplicits.there was MockitoImplicits.one( connector ).set( anyString, anyString, any[ Duration ] ) + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/AsynchronousCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/AsynchronousCacheSpec.scala deleted file mode 100644 index 2cbfc1bf..00000000 --- a/src/test/scala/play/api/cache/redis/impl/AsynchronousCacheSpec.scala +++ /dev/null @@ -1,396 +0,0 @@ -package play.api.cache.redis.impl - -import java.util.Date -import java.util.concurrent.atomic.AtomicInteger - -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ -import scala.concurrent.ExecutionContext.Implicits.global - -import play.api.cache.redis._ - -import org.joda.time.DateTime -import org.specs2.mutable.Specification - -/** - *Test of cache to be sure that keys are differentiated, expires etc.
- */ -class AsynchronousCacheSpec extends Specification with Redis { - - private type Cache = RedisCache[ AsynchronousResult ] - - private val workingConnector = injector.instanceOf[ RedisConnector ] - - def runtime( policy: RecoveryPolicy ) = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, policy, invocation = LazyInvocation ) - - // test proper implementation, no fails - new RedisCacheSuite( "implement", "redis-cache-implements", new RedisCache( workingConnector, Builders.AsynchronousBuilder )( runtime( FailThrough ) ), AlwaysSuccess ) - - new RedisCacheSuite( "recover from with working connector", "redis-cache-implements-and-recovery", new RedisCache( workingConnector, Builders.AsynchronousBuilder )( runtime( RecoverWithDefault ) ), SuccessOrDefault ) - - new RedisCacheSuite( "recover from", "redis-cache-recovery", new RedisCache( FailingConnector, Builders.AsynchronousBuilder )( runtime( RecoverWithDefault ) ), AlwaysDefault ) - - new RedisCacheSuite( "fail on", "redis-cache-fail", new RedisCache( FailingConnector, Builders.AsynchronousBuilder )( runtime( FailThrough ) ), AlwaysException ) - - class RedisCacheSuite( suiteName: String, prefix: String, cache: Cache, expectation: Expectation ) { - - "AsynchronousCacheApi" should { - - import expectation._ - - suiteName >> { - - "miss on get" in { - cache.get[ String ]( s"$prefix-test-1" ) must expects( beNone ) - } - - "hit after set" in { - cache.set( s"$prefix-test-2", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-2" ) must expects( beSome[ Any ], beNone ) - cache.get[ String ]( s"$prefix-test-2" ) must expects( beSome( "value" ), beNone ) - } - - "expire refreshes expiration" in { - cache.set( s"$prefix-test-10", "value", 2.second ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-10" ) must expects( beSome( "value" ), beNone ) - cache.expire( s"$prefix-test-10", 1.minute ) must expects( beUnit ) - // wait until the first duration expires - Thread.sleep( 3000 ) - cache.get[ String ]( s"$prefix-test-10" ) must expects( beSome( "value" ), beNone ) - } - - "positive exists on existing keys" in { - cache.set( s"$prefix-test-11", "value" ) must expects( beUnit ) - cache.exists( s"$prefix-test-11" ) must expects( beTrue, beFalse ) - } - - "negative exists on expired and missing keys" in { - cache.set( s"$prefix-test-12A", "value", 1.second ) must expects( beUnit ) - // wait until the duration expires - Thread.sleep( 2000 ) - cache.exists( s"$prefix-test-12A" ) must expects( beFalse, beFalse ) - cache.exists( s"$prefix-test-12B" ) must expects( beFalse, beFalse ) - } - - "ignore set if not exists when already defined and preserve original expiration mark" in { - cache.set( s"$prefix-test-if-not-exists-when-exists", "previous", 2.seconds ) must expects( beUnit ) - cache.setIfNotExists( s"$prefix-test-if-not-exists-when-exists", "value", 5.seconds ) must expects( beFalse, beTrue ) - cache.get[ String ]( s"$prefix-test-if-not-exists-when-exists" ) must expects( beSome[ Any ], beNone ) - cache.get[ String ]( s"$prefix-test-if-not-exists-when-exists" ) must expects( beSome( "previous" ), beNone ) - // wait until the duration expires - Thread.sleep( 3000 ) - cache.get[ String ]( s"$prefix-test-if-not-exists-when-exists" ) must expects( beNone ) - } - - "perform set if not exists when undefined" in { - cache.setIfNotExists( s"$prefix-test-if-not-exists", "value" ) must expects( beTrue ) - cache.get[ String ]( s"$prefix-test-if-not-exists" ) must expects( beSome[ Any ], beNone ) - cache.get[ String ]( s"$prefix-test-if-not-exists" ) must expects( beSome( "value" ), beNone ) - } - - "perform set if not exists when undefined but expire after some time" in { - cache.setIfNotExists( s"$prefix-test-if-not-exists-and-expire", "value", 1.seconds ) must expects( beTrue ) - cache.get[ String ]( s"$prefix-test-if-not-exists-and-expire" ) must expects( beSome[ Any ], beNone ) - cache.get[ String ]( s"$prefix-test-if-not-exists-and-expire" ) must expects( beSome( "value" ), beNone ) - // wait until the duration expires - Thread.sleep( 2000 ) - cache.get[ String ]( s"$prefix-test-if-not-exists-and-expire" ) must expects( beNone ) - } - - "miss after remove" in { - cache.set( s"$prefix-test-3", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-3" ) must expects( beSome[ Any ], beNone ) - cache.remove( s"$prefix-test-3" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-3" ) must expects( beNone ) - } - - "miss after timeout" in { - // set - cache.set( s"$prefix-test-4", "value", 1.second ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-4" ) must expects( beSome[ Any ], beNone ) - // wait until it expires - Thread.sleep( 1500 ) - // miss - cache.get[ String ]( s"$prefix-test-4" ) must expects( beNone ) - } - - "miss at first getOrElse " in { - val counter = new AtomicInteger( 0 ) - cache.getOrElseCounting( s"$prefix-test-5" )( counter ) must expects( beEqualTo( "value" ) ) - counter.get must expectsNow( beEqualTo( 1 ), beEqualTo( 1 ), beEqualTo( 0 ) ) - } - - "hit at second getOrElse" in { - val counter = new AtomicInteger( 0 ) - for ( index <- 1 to 10 ) cache.getOrElseCounting( s"$prefix-test-6" )( counter ) must expects( beEqualTo( "value" ) ) - counter.get must expectsNow( beEqualTo( 1 ), beEqualTo( 10 ), beEqualTo( 0 ) ) - } - - "find all matching keys" in { - cache.set( s"$prefix-test-13-key-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-13-note-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-13-key-B", "value", 3.second ) must expects( beUnit ) - cache.matching( s"$prefix-test-13*" ).map( _.sorted ) must expects( beEqualTo( Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-note-A", s"$prefix-test-13-key-B" ).sorted ), beEqualTo( Seq.empty ) ) - cache.matching( s"$prefix-test-13*A" ).map( _.sorted ) must expects( beEqualTo( Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-note-A" ).sorted ), beEqualTo( Seq.empty ) ) - cache.matching( s"$prefix-test-13-key-*" ).map( _.sorted ) must expects( beEqualTo( Seq( s"$prefix-test-13-key-A", s"$prefix-test-13-key-B" ).sorted ), beEqualTo( Seq.empty ) ) - cache.matching( s"$prefix-test-13A*" ) must expects( beEqualTo( Seq.empty ) ) - } - - "remove all matching keys, wildcard at the end" in { - cache.set( s"$prefix-test-14-key-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-14-note-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-14-key-B", "value", 3.second ) must expects( beUnit ) - cache.matching( s"$prefix-test-14*" ).map( _.sorted ) must expects( beEqualTo( Seq( s"$prefix-test-14-key-A", s"$prefix-test-14-note-A", s"$prefix-test-14-key-B" ).sorted ), beEqualTo( Seq.empty ) ) - cache.removeMatching( s"$prefix-test-14*" ) must expects( beUnit ) - cache.matching( s"$prefix-test-14*" ) must expects( beEqualTo( Seq.empty ) ) - } - - "remove all matching keys, wildcard in the middle" in { - cache.set( s"$prefix-test-15-key-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-15-note-A", "value", 3.second ) must expects( beUnit ) - cache.set( s"$prefix-test-15-key-B", "value", 3.second ) must expects( beUnit ) - cache.matching( s"$prefix-test-15*A" ).map( _.sorted ) must expects( beEqualTo( Seq( s"$prefix-test-15-key-A", s"$prefix-test-15-note-A" ).sorted ), beEqualTo( Seq.empty ) ) - cache.removeMatching( s"$prefix-test-15*A" ) must expects( beUnit ) - cache.matching( s"$prefix-test-15*A" ) must expects( beEqualTo( Seq.empty ) ) - } - - "remove all matching keys, no match" in { - cache.matching( s"$prefix-test-16*" ) must expects( beEqualTo( Seq.empty ) ) - cache.removeMatching( s"$prefix-test-16*" ) must expects( beUnit ) - cache.matching( s"$prefix-test-16*" ) must expects( beEqualTo( Seq.empty ) ) - } - - "distinct different keys" in { - val counter = new AtomicInteger( 0 ) - cache.getOrElseCounting( s"$prefix-test-7A" )( counter ) must expects( beEqualTo( "value" ) ) - cache.getOrElseCounting( s"$prefix-test-7B" )( counter ) must expects( beEqualTo( "value" ) ) - counter.get must expectsNow( beEqualTo( 2 ), beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "perform future and store result" in { - val counter = new AtomicInteger( 0 ) - // perform test - for ( index <- 1 to 5 ) cache.getOrFutureCounting( s"$prefix-test-8" )( counter ) must expects( beEqualTo( "value" ) ) - // verify - counter.get must expectsNow( beEqualTo( 1 ), beEqualTo( 5 ), beEqualTo( 0 ) ) - } - - "propagate fail in future" in { - cache.getOrFuture[ String ]( s"$prefix-test-9" ) { - Future.failed( new IllegalStateException( "Exception in test." ) ) - } must expects( throwA( new IllegalStateException( "Exception in test." ) ) ) - } - - "support list" in { - // store value - cache.set( s"$prefix-list", List( "A", "B", "C" ) ) must expects( beUnit ) - // recall - cache.get[ List[ String ] ]( s"$prefix-list" ) must expects( beSome[ List[ String ] ]( List( "A", "B", "C" ) ), beNone ) - } - - "support a byte" in { - cache.set( s"$prefix-type.byte", 0xAB.toByte ) must expects( beUnit ) - cache.get[ Byte ]( s"$prefix-type.byte" ) must expects( beSome[ Byte ], beNone ) - cache.get[ Byte ]( s"$prefix-type.byte" ) must expects( beSome( 0xAB.toByte ), beNone ) - } - - "support a char" in { - cache.set( s"$prefix-type.char.1", 'a' ) must expects( beUnit ) - cache.get[ Char ]( s"$prefix-type.char.1" ) must expects( beSome[ Char ], beNone ) - cache.get[ Char ]( s"$prefix-type.char.1" ) must expects( beSome( 'a' ), beNone ) - cache.set( s"$prefix-type.char.2", 'b' ) must expects( beUnit ) - cache.get[ Char ]( s"$prefix-type.char.2" ) must expects( beSome( 'b' ), beNone ) - cache.set( s"$prefix-type.char.3", 'č' ) must expects( beUnit ) - cache.get[ Char ]( s"$prefix-type.char.3" ) must expects( beSome( 'č' ), beNone ) - } - - "support a short" in { - cache.set( s"$prefix-type.short", 12.toShort ) must expects( beUnit ) - cache.get[ Short ]( s"$prefix-type.short" ) must expects( beSome[ Short ], beNone ) - cache.get[ Short ]( s"$prefix-type.short" ) must expects( beSome( 12.toShort ), beNone ) - } - - "support an int" in { - cache.set( s"$prefix-type.int", 15 ) must expects( beUnit ) - cache.get[ Int ]( s"$prefix-type.int" ) must expects( beSome( 15 ), beNone ) - } - - "support a long" in { - cache.set( s"$prefix-type.long", 144L ) must expects( beUnit ) - cache.get[ Long ]( s"$prefix-type.long" ) must expects( beSome[ Long ], beNone ) - cache.get[ Long ]( s"$prefix-type.long" ) must expects( beSome( 144L ), beNone ) - } - - "support a float" in { - cache.set( s"$prefix-type.float", 1.23f ) must expects( beUnit ) - cache.get[ Float ]( s"$prefix-type.float" ) must expects( beSome[ Float ], beNone ) - cache.get[ Float ]( s"$prefix-type.float" ) must expects( beSome( 1.23f ), beNone ) - } - - "support a double" in { - cache.set( s"$prefix-type.double", 3.14 ) must expects( beUnit ) - cache.get[ Double ]( s"$prefix-type.double" ) must expects( beSome[ Double ], beNone ) - cache.get[ Double ]( s"$prefix-type.double" ) must expects( beSome( 3.14 ), beNone ) - } - - "support a date" in { - cache.set( s"$prefix-type.date", new Date( 123 ) ) must expects( beUnit ) - cache.get[ Date ]( s"$prefix-type.date" ) must expects( beSome( new Date( 123 ) ), beNone ) - } - - "support a datetime" in { - cache.set( s"$prefix-type.datetime", new DateTime( 123456 ) ) must expects( beUnit ) - cache.get[ DateTime ]( s"$prefix-type.datetime" ) must expects( beSome( new DateTime( 123456 ) ), beNone ) - } - - "support a custom classes" in { - cache.set( s"$prefix-type.object", SimpleObject( "B", 3 ) ) must expects( beUnit ) - cache.get[ SimpleObject ]( s"$prefix-type.object" ) must expects( beSome( SimpleObject( "B", 3 ) ), beNone ) - } - - "support a null" in { - cache.set( s"$prefix-type.null", null ) must expects( beUnit ) - cache.get[ SimpleObject ]( s"$prefix-type.null" ) must expects( beNone ) - } - - "remove multiple keys at once" in { - cache.set( s"$prefix-test-remove-multiple-1", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must expects( beSome[ Any ], beNone ) - cache.set( s"$prefix-test-remove-multiple-2", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must expects( beSome[ Any ], beNone ) - cache.set( s"$prefix-test-remove-multiple-3", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must expects( beSome[ Any ], beNone ) - cache.remove( s"$prefix-test-remove-multiple-1", s"$prefix-test-remove-multiple-2", s"$prefix-test-remove-multiple-3" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must expects( beNone ) - cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must expects( beNone ) - cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must expects( beNone ) - } - - "remove in batch" in { - cache.set( s"$prefix-test-remove-batch-1", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-batch-1" ) must expects( beSome[ Any ], beNone ) - cache.set( s"$prefix-test-remove-batch-2", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-batch-2" ) must expects( beSome[ Any ], beNone ) - cache.set( s"$prefix-test-remove-batch-3", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-batch-3" ) must expects( beSome[ Any ], beNone ) - cache.removeAll( Seq( s"$prefix-test-remove-batch-1", s"$prefix-test-remove-batch-2", s"$prefix-test-remove-batch-3" ): _* ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-remove-batch-1" ) must expects( beNone ) - cache.get[ String ]( s"$prefix-test-remove-batch-2" ) must expects( beNone ) - cache.get[ String ]( s"$prefix-test-remove-batch-3" ) must expects( beNone ) - } - - "set a zero when not exists and then increment" in { - cache.increment( s"$prefix-test-incr-null" ) must expects( beEqualTo( 1 ), beEqualTo( 1 ) ) - } - - "throw an exception when not integer" in { - cache.set( s"$prefix-test-incr-string", "value" ) must expects( beUnit ) - cache.increment( s"$prefix-test-incr-string", 1 ) must expects( throwA[ ExecutionFailedException ], beEqualTo( 1 ) ) - } - - "increment by one" in { - cache.set( s"$prefix-test-incr-by-one", 5 ) must expects( beUnit ) - cache.increment( s"$prefix-test-incr-by-one" ) must expects( beEqualTo( 6 ), beEqualTo( 1 ) ) - cache.increment( s"$prefix-test-incr-by-one" ) must expects( beEqualTo( 7 ), beEqualTo( 1 ) ) - cache.increment( s"$prefix-test-incr-by-one" ) must expects( beEqualTo( 8 ), beEqualTo( 1 ) ) - } - - "increment by some" in { - cache.set( s"$prefix-test-incr-by-some", 5 ) must expects( beUnit ) - cache.increment( s"$prefix-test-incr-by-some", 1 ) must expects( beEqualTo( 6 ), beEqualTo( 1 ) ) - cache.increment( s"$prefix-test-incr-by-some", 2 ) must expects( beEqualTo( 8 ), beEqualTo( 2 ) ) - cache.increment( s"$prefix-test-incr-by-some", 3 ) must expects( beEqualTo( 11 ), beEqualTo( 3 ) ) - } - - "decrement by one" in { - cache.set( s"$prefix-test-decr-by-one", 5 ) must expects( beUnit ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( 4 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( 3 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( 2 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( 1 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( 0 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-one" ) must expects( beEqualTo( -1 ), beEqualTo( -1 ) ) - } - - "decrement by some" in { - cache.set( s"$prefix-test-decr-by-some", 5 ) must expects( beUnit ) - cache.decrement( s"$prefix-test-decr-by-some", 1 ) must expects( beEqualTo( 4 ), beEqualTo( -1 ) ) - cache.decrement( s"$prefix-test-decr-by-some", 2 ) must expects( beEqualTo( 2 ), beEqualTo( -2 ) ) - cache.decrement( s"$prefix-test-decr-by-some", 3 ) must expects( beEqualTo( -1 ), beEqualTo( -3 ) ) - } - - "append like set when value is undefined" in { - cache.get[ String ]( s"$prefix-test-append-to-null" ) must expects( beNone ) - cache.append( s"$prefix-test-append-to-null", "value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-to-null" ) must expects( beSome( "value" ), beNone ) - } - - "append to existing string" in { - cache.set( s"$prefix-test-append-to-some", "some" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-to-some" ) must expects( beSome( "some" ), beNone ) - cache.append( s"$prefix-test-append-to-some", " value" ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-to-some" ) must expects( beSome( "some value" ), beNone ) - } - - "append with applied expiration" in { - cache.get[ String ]( s"$prefix-test-append-and-expire" ) must expects( beNone ) - cache.append( s"$prefix-test-append-and-expire", "value", 2.seconds ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-and-expire" ) must expects( beSome( "value" ), beNone ) - // wait until the first duration expires - Thread.sleep( 3000 ) - cache.get[ String ]( s"$prefix-test-append-and-expire" ) must expects( beNone ) - } - - "append but do not apply expiration" in { - cache.set( s"$prefix-test-append-and-not-expire", "some", 5.seconds ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-and-not-expire" ) must expects( beSome( "some" ), beNone ) - cache.append( s"$prefix-test-append-and-not-expire", " value", 2.seconds ) must expects( beUnit ) - cache.get[ String ]( s"$prefix-test-append-and-not-expire" ) must expects( beSome( "some value" ), beNone ) - // wait until the first duration expires - Thread.sleep( 3000 ) - cache.get[ String ]( s"$prefix-test-append-and-not-expire" ) must expects( beSome( "some value" ), beNone ) - } - - "with failing serialization (lazy)" in { - cache.set( s"$prefix-test-with-failing-serialization-lazy", UnserializableObject( "some" ), 5.seconds ) must expects( throwA[ SerializationException ], beUnit ) - cache.getOrElse( s"$prefix-test-with-failing-serialization-lazy" )( UnserializableObject( "some" ) ) must expects( - throwA[ SerializationException ], beEqualTo( UnserializableObject( "some" ) ) - ) - } - - "with failing serialization (eager)" in { - val runtime = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, FailThrough, invocation = EagerInvocation ) - val cache = new RedisCache( workingConnector, Builders.AsynchronousBuilder )( runtime ) - - cache.set( s"$prefix-test-with-failing-serialization-eager", UnserializableObject( "some" ), 5.seconds ) must expects( throwA[ SerializationException ], throwA[ SerializationException ], throwA[ SerializationException ] ) - cache.getOrElse( s"$prefix-test-with-failing-serialization-eager" )( UnserializableObject( "some" ) ) must expects( - beEqualTo( UnserializableObject( "some" ) ), beEqualTo( UnserializableObject( "some" ) ), beEqualTo( UnserializableObject( "some" ) ) - ) - } - - "with failing serialization (eager)(failing connector)" in { - val runtime = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, FailThrough, invocation = EagerInvocation ) - val cache = new RedisCache( FailingConnector, Builders.AsynchronousBuilder )( runtime ) - - cache.set( s"$prefix-test-with-failing-serialization-eager", UnserializableObject( "some" ), 5.seconds ) must expects( throwA[ ExecutionFailedException ], throwA[ ExecutionFailedException ], throwA[ ExecutionFailedException ] ) - cache.getOrElse( s"$prefix-test-with-failing-serialization-eager" )( UnserializableObject( "some" ) ) must expects( - throwA[ ExecutionFailedException ], throwA[ ExecutionFailedException ], throwA[ ExecutionFailedException ] - ) - } - } - } - } -} - -case class UnserializableObject( value: String ) - -class FailingSerializer extends akka.serialization.Serializer { - - def identifier = 2017052801 - - def toBinary( o: AnyRef ) = throw new IllegalStateException("Failing serialization") - - def includeManifest = false - - def fromBinary( bytes: Array[ Byte ], manifest: Option[ Class[ _ ] ] ) = throw new IllegalStateException("Failing deserialization") -} diff --git a/src/test/scala/play/api/cache/redis/impl/BuildersSpecs.scala b/src/test/scala/play/api/cache/redis/impl/BuildersSpecs.scala new file mode 100644 index 00000000..91f0aef6 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/BuildersSpecs.scala @@ -0,0 +1,106 @@ +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._ + +/** + * @author Karel Cemus + */ +class BuildersSpecs( implicit ee: ExecutionEnv ) extends Specification with Mockito with WithApplication { + + import Builders._ + import BuildersSpecs._ + import Implicits._ + + def defaultTask = Future.successful( "default" ) + + def regularTask = Future( "response" ) + + def longTask = Future.after( seconds = 2, "response" ) + + def failingTask = Future.failed( TimeoutException( new IllegalArgumentException( "Simulated failure." ) ) ) + + "AsynchronousBuilder" should { + + "match on name" in { + AsynchronousBuilder.name mustEqual "AsynchronousBuilder" + } + + "run" in new RuntimeMock { + AsynchronousBuilder.toResult( regularTask, defaultTask ) must beEqualTo( "response" ).await + } + + "run long running task" in new RuntimeMock { + AsynchronousBuilder.toResult( longTask, defaultTask ) must beEqualTo( "response" ).awaitFor( 3.seconds ) + } + + "recover with default policy" in new RuntimeMock { + runtime.policy returns defaultPolicy + AsynchronousBuilder.toResult( failingTask, defaultTask ) must beEqualTo( "default" ).await + } + + "recover with fail through policy" in new RuntimeMock { + runtime.policy returns failThrough + AsynchronousBuilder.toResult( failingTask, defaultTask ) must throwA[ TimeoutException ].await + } + } + + "SynchronousBuilder" should { + + "match on name" in { + SynchronousBuilder.name mustEqual "SynchronousBuilder" + } + + "run" in new RuntimeMock { + SynchronousBuilder.toResult( regularTask, defaultTask ) must beEqualTo( "response" ) + } + + "recover from failure with default policy" in new RuntimeMock { + runtime.policy returns defaultPolicy + SynchronousBuilder.toResult( failingTask, defaultTask ) must beEqualTo( "default" ) + } + + "recover from failure with fail through policy" in new RuntimeMock { + runtime.policy returns failThrough + SynchronousBuilder.toResult( failingTask, defaultTask ) must throwA[ TimeoutException ] + } + + "recover from timeout due to long running task" in new RuntimeMock { + runtime.policy returns failThrough + SynchronousBuilder.toResult( longTask, defaultTask ) must throwA[ TimeoutException ] + } + + "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 ] + } + } +} + +object BuildersSpecs { + + 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 {} + + protected def defaultPolicy = new RecoverWithDefault {} + } + +} diff --git a/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpecs.scala b/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpecs.scala new file mode 100644 index 00000000..52102b80 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/InvocationPolicySpecs.scala @@ -0,0 +1,42 @@ +package play.api.cache.redis.impl + +import scala.concurrent.Future +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class InvocationPolicySpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito with WithApplication { + + import Implicits._ + import RedisCacheImplicits._ + + val andThen = "then" + + def longTask( andThen: => Unit ) = Future.after( seconds = 2, { andThen; "result" } ) + + "InvocationPolicy" should { + + "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 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 + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/JavaCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/JavaCacheSpec.scala deleted file mode 100644 index fe09ed73..00000000 --- a/src/test/scala/play/api/cache/redis/impl/JavaCacheSpec.scala +++ /dev/null @@ -1,158 +0,0 @@ -package play.api.cache.redis.impl - -import java.util.Date -import java.util.concurrent.Callable -import java.util.concurrent.atomic.AtomicInteger - -import play.api.cache.redis.{Redis, SimpleObject} -import play.mvc.Http -import play.test.Helpers - -import org.joda.time.DateTime -import org.specs2.mutable.Specification - -/** - *Test of cache to be sure that keys are differentiated, expires etc.
- */ -class JavaCacheSpec extends Specification with Redis { - - private type Cache = play.cache.SyncCacheApi - - private val Cache = injector.instanceOf[ play.cache.SyncCacheApi ] - - private val prefix = "java" - - "play.cache.CacheApi" should { - - "miss on get" in { - Cache.get[ String ]( s"$prefix-test-1" ) must beNull - } - - "hit after set" in { - Cache.set( s"$prefix-test-2", "value" ) - Cache.get[ String ]( s"$prefix-test-2" ) mustEqual "value" - } - - "miss after remove" in { - Cache.set( s"$prefix-test-3", "value" ) - Cache.get[ String ]( s"$prefix-test-3" ) mustEqual "value" - Cache.remove( s"$prefix-test-3" ) - Cache.get[ String ]( s"$prefix-test-3" ) must beNull - } - - "miss after timeout" in { - // set - Cache.set( s"$prefix-test-4", "value", 1 ) - Cache.get[ String ]( s"$prefix-test-4" ) mustEqual "value" - // wait until it expires - Thread.sleep( 1500 ) - // miss - Cache.get[ String ]( s"$prefix-test-4" ) must beNull - } - - "miss at first getOrElse " in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-5" )( counter ) mustEqual "value" - counter.get must beEqualTo( 1 ) - } - - "hit at second getOrElse" in { - val counter = new AtomicInteger( 0 ) - for ( index <- 1 to 10 ) Cache.getOrElseCounting( s"$prefix-test-6" )( counter ) mustEqual "value" - counter.get mustEqual 1 - } - - "getOrElseUpdate" in { - Cache.get[ String ]( s"$prefix-test-getOrElseUpdate" ) must beNull - val orElse = new Callable[ String ] { def call( ) = "value" } - Cache.getOrElseUpdate[ String ]( s"$prefix-test-getOrElseUpdate", orElse ) mustEqual "value" - Cache.get[ String ]( s"$prefix-test-getOrElseUpdate" ) mustEqual "value" - } - - "getOrElseUpdate uses HttpContext" in { - Cache.get[ String ]( s"$prefix-test-getOrElseUpdate-2" ) must beNull - val request = Helpers.fakeRequest().path( "request-path" ).build() - val context = Helpers.httpContext( request ) - Http.Context.current.set( context ) - val orElse = new Callable[ String ] { - def call( ) = Http.Context.current().request().path() - } - Http.Context.current().request().path() mustEqual "request-path" - Cache.getOrElseUpdate[ String ]( s"$prefix-test-getOrElseUpdate-2", orElse ) mustEqual "request-path" - Cache.get[ String ]( s"$prefix-test-getOrElseUpdate-2" ) mustEqual "request-path" - } - - "distinct different keys" in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-7A" )( counter ) mustEqual "value" - Cache.getOrElseCounting( s"$prefix-test-7B" )( counter ) mustEqual "value" - counter.get mustEqual 2 - } - - "support list" in { - // store value - Cache.set( s"$prefix-list", List( "A", "B", "C" ) ) - // recall - Cache.get[ List[ String ] ]( s"$prefix-list" ) mustEqual List( "A", "B", "C" ) - } - - "support a byte" in { - Cache.set( s"$prefix-type.byte", 0xAB.toByte ) - Cache.get[ Byte ]( s"$prefix-type.byte" ) mustEqual 0xAB.toByte - } - - "support a char" in { - Cache.set( s"$prefix-type.char.1", 'a' ) - Cache.get[ Char ]( s"$prefix-type.char.1" ) mustEqual 'a' - Cache.set( s"$prefix-type.char.2", 'b' ) - Cache.get[ Char ]( s"$prefix-type.char.2" ) mustEqual 'b' - Cache.set( s"$prefix-type.char.3", 'č' ) - Cache.get[ Char ]( s"$prefix-type.char.3" ) mustEqual 'č' - } - - "support a short" in { - Cache.set( s"$prefix-type.short", 12.toShort ) - Cache.get[ Short ]( s"$prefix-type.short" ) mustEqual 12.toShort - } - - "support an int" in { - Cache.set( s"$prefix-type.int", 0xAB.toByte ) - Cache.get[ Byte ]( s"$prefix-type.int" ) mustEqual 0xAB.toByte - } - - "support a long" in { - Cache.set( s"$prefix-type.long", 144L ) - Cache.get[ Long ]( s"$prefix-type.long" ) mustEqual 144L - } - - "support a float" in { - Cache.set( s"$prefix-type.float", 1.23f ) - Cache.get[ Float ]( s"$prefix-type.float" ) mustEqual 1.23f - } - - "support a double" in { - Cache.set( s"$prefix-type.double", 3.14 ) - Cache.get[ Double ]( s"$prefix-type.double" ) mustEqual 3.14 - } - - "support a date" in { - Cache.set( s"$prefix-type.date", new Date( 123 ) ) - Cache.get[ Date ]( s"$prefix-type.date" ) mustEqual new Date( 123 ) - } - - "support a datetime" in { - Cache.set( s"$prefix-type.datetime", new DateTime( 123456 ) ) - Cache.get[ DateTime ]( s"$prefix-type.datetime" ) mustEqual new DateTime( 123456 ) - } - - "support a custom classes" in { - Cache.set( s"$prefix-type.object", SimpleObject( "B", 3 ) ) - Cache.get[ SimpleObject ]( s"$prefix-type.object" ) mustEqual SimpleObject( "B", 3 ) - } - - "support a null" in { - Cache.set( s"$prefix-type.null", null ) - Cache.get[ SimpleObject ]( s"$prefix-type.null" ) must beNull - } - } -} diff --git a/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala b/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala new file mode 100644 index 00000000..61f27d98 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/JavaRedisSpecs.scala @@ -0,0 +1,109 @@ +package play.api.cache.redis.impl + +import java.util.concurrent.Callable + +import scala.concurrent.duration.Duration + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class JavaRedisSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + + import Implicits._ + import JavaRedis._ + 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 ).toScala must beNull.await + } + + "get and hit" in new MockedJavaRedis { + async.get[ String ]( beEq( key ) )( anyClassTag ) returns Some( value ) + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "java.lang.String" ) + cache.get[ String ]( key ).toScala must beEqualTo( value ).await + } + + "get null" in new MockedJavaRedis { + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "" ) + cache.get[ String ]( key ).toScala must beNull.await + there was one( async ).get[ String ]( s"classTag::$key" ) + } + + "set" in new MockedJavaRedis { + async.set( anyString, anyString, any[ Duration ] ) returns execDone + cache.set( key, value ).toScala must beDone.await + there was one( async ).set( key, value, Duration.Inf ) + there was one( async ).set( s"classTag::$key", "java.lang.String", Duration.Inf ) + } + + "set with expiration" in new MockedJavaRedis { + async.set( anyString, anyString, any[ Duration ] ) returns execDone + cache.set( key, value, expiration.toSeconds.toInt ).toScala must beDone.await + there was one( async ).set( key, value, expiration ) + there was one( async ).set( s"classTag::$key", "java.lang.String", expiration ) + } + + "set null" in new MockedJavaRedis { + async.set( anyString, any, any[ Duration ] ) returns execDone + cache.set( key, null ).toScala must beDone.await + there was one( async ).set( key, null, Duration.Inf ) + there was one( async ).set( s"classTag::$key", "", Duration.Inf ) + } + + "get or else (hit)" in new MockedJavaRedis with OrElse { + async.get[ String ]( beEq( key ) )( anyClassTag ) returns Some( value ) + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "java.lang.String" ) + cache.getOrElseUpdate( key, doFuture( value ).toJava ).toScala must beEqualTo( value ).await + orElse mustEqual 0 + there was one( async ).get[ String ]( key ) + } + + "get or else (miss)" in new MockedJavaRedis with OrElse { + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns None + async.set( anyString, anyString, any[ Duration ] ) returns execDone + cache.getOrElseUpdate( key, doFuture( value ).toJava ).toScala must beEqualTo( value ).await + orElse mustEqual 1 + there was one( async ).get[ String ]( s"classTag::$key" ) + there was one( async ).set( key, value, Duration.Inf ) + there was one( async ).set( s"classTag::$key", "java.lang.String", Duration.Inf ) + } + + "get or else with expiration (hit)" in new MockedJavaRedis with OrElse { + async.get[ String ]( beEq( key ) )( anyClassTag ) returns Some( value ) + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns Some( "java.lang.String" ) + cache.getOrElseUpdate( key, doFuture( value ).toJava, expiration.toSeconds.toInt ).toScala must beEqualTo( value ).await + orElse mustEqual 0 + there was one( async ).get[ String ]( key ) + } + + "get or else with expiration (miss)" in new MockedJavaRedis with OrElse { + async.get[ String ]( beEq( s"classTag::$key" ) )( anyClassTag ) returns None + async.set( anyString, anyString, any[ Duration ] ) returns execDone + cache.getOrElseUpdate( key, doFuture( value ).toJava, expiration.toSeconds.toInt ).toScala must beEqualTo( value ).await + orElse mustEqual 1 + there was one( async ).get[ String ]( s"classTag::$key" ) + there was one( async ).set( key, value, expiration ) + there was one( async ).set( s"classTag::$key", "java.lang.String", expiration ) + } + + "remove" in new MockedJavaRedis { + async.remove( anyString ) returns execDone + cache.remove( key ).toScala must beDone.await + } + + "remove all" in new MockedJavaRedis { + async.invalidate() returns execDone + cache.removeAll().toScala must beDone.await + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/PlayCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/PlayCacheSpec.scala deleted file mode 100644 index e81989df..00000000 --- a/src/test/scala/play/api/cache/redis/impl/PlayCacheSpec.scala +++ /dev/null @@ -1,70 +0,0 @@ -package play.api.cache.redis.impl - -import java.util.concurrent.atomic.AtomicInteger - -import play.api.cache.redis.Redis - -import org.specs2.mutable.Specification - -/** - *Test of play.api.cache.CacheApi to verify compatibility with Redis implementation.
- */ -class PlayCacheSpec extends Specification with Redis { - - private type Cache = play.api.cache.SyncCacheApi - - private val Cache = injector.instanceOf[ Cache ] - - private val prefix = "play" - - "play.api.cache.CacheApi" should { - - "miss on get" in { - Cache.get[ String ]( s"$prefix-test-1" ) must beNone - } - - "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" ) - } - - "miss at first getOrElse " in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-5" )( counter ) mustEqual "value" - counter.get must beEqualTo( 1 ) - } - - "hit at second getOrElse" in { - val counter = new AtomicInteger( 0 ) - for ( index <- 1 to 10 ) Cache.getOrElseCounting( s"$prefix-test-6" )( counter ) mustEqual "value" - counter.get must beEqualTo( 1 ) - } - - "distinct different keys" in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-7A" )( counter ) mustEqual "value" - Cache.getOrElseCounting( s"$prefix-test-7B" )( counter ) mustEqual "value" - counter.get must beEqualTo( 2 ) - } - - "miss after remove" in { - Cache.set( s"$prefix-test-3", "value" ) - Cache.get[ String ]( s"$prefix-test-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-3" ) - Cache.get[ String ]( s"$prefix-test-3" ) must beNone - } - } - - implicit class AccumulatorCache( cache: Cache ) { - private type Accumulator = AtomicInteger - - /** invokes internal getOrElse but it accumulate invocations of orElse clause in the accumulator */ - def getOrElseCounting( key: String )( accumulator: Accumulator ) = cache.getOrElseUpdate( key ) { - // increment miss counter - accumulator.incrementAndGet() - // return the value to store into the cache - "value" - } - } -} diff --git a/src/test/scala/play/api/cache/redis/impl/RecoveryPolicySpec.scala b/src/test/scala/play/api/cache/redis/impl/RecoveryPolicySpec.scala deleted file mode 100644 index ce795830..00000000 --- a/src/test/scala/play/api/cache/redis/impl/RecoveryPolicySpec.scala +++ /dev/null @@ -1,83 +0,0 @@ -package play.api.cache.redis.impl - -import scala.concurrent.Future -import scala.reflect.ClassTag - -import play.api.cache.redis._ - -import akka.pattern.AskTimeoutException -import org.specs2.matcher.Matcher -import org.specs2.mutable.Specification - -/** - *Behavior of recovery policy.
- */ -class RecoveryPolicySpec extends Specification { - - val futureDefault = Future.successful { } - - new PolicySpecs( - "LogAndFail", - new LogAndFailPolicy, - throw new IllegalStateException( "Default should not trigger" ), - new Expectation { - override def expects[ T <: Throwable : ClassTag ]: Matcher[ Any ] = throwA[ T ] - } - ) - - - new PolicySpecs( - "LogAndDefault", - new LogAndDefaultPolicy, - futureDefault, - new Expectation { - override def expects[ T <: Throwable : ClassTag ]: Matcher[ Any ] = beEqualTo( futureDefault ) - } - ) - - - trait Expectation { - def expects[ T <: Throwable : ClassTag ]: Matcher[ Any ] - } - - - class PolicySpecs( name: String, policy: RecoveryPolicy, default: => Future[ Unit ], expectation: Expectation ) { - - import expectation._ - - def rerun: Future[ Unit ] = - throw new IllegalStateException( "Rerun should not trigger" ) - - val reason = - new IllegalStateException( "Failure reason" ) - - s"$name policy" should "recover from" >> { - - "TimeoutException with a key" in { - policy.recoverFrom( rerun, default, new TimeoutException( new AskTimeoutException( "Simulated execution timeout" ) ) ) must expects[ TimeoutException ] - } - - "SerializationException" in { - policy.recoverFrom( rerun, default, new SerializationException( "key", "Serialization failed", reason ) ) must expects[ SerializationException ] - } - - "ExecutionFailedException with a key" in { - policy.recoverFrom( rerun, default, new ExecutionFailedException( Some( "key" ), "GET", reason ) ) must expects[ ExecutionFailedException ] - } - - "ExecutionFailedException without a key" in { - policy.recoverFrom( rerun, default, new ExecutionFailedException( None, "GET", reason ) ) must expects[ ExecutionFailedException ] - } - - "UnexpectedResponseException with a key" in { - policy.recoverFrom( rerun, default, new UnexpectedResponseException( Some( "key" ), "GET" ) ) must expects[ UnexpectedResponseException ] - } - - "UnexpectedResponseException without a key" in { - policy.recoverFrom( rerun, default, new UnexpectedResponseException( None, "GET" ) ) must expects[ UnexpectedResponseException ] - } - } - - } - -} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala b/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala new file mode 100644 index 00000000..60bd429d --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala @@ -0,0 +1,135 @@ +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 + +/** + * @author Karel Cemus + */ +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." ) ) + + trait AbstractMocked extends Scope { + val key = "key" + val value = "value" + val other = "other" + + protected def invocation = LazyInvocation + + protected def policy: RecoveryPolicy = new RecoverWithDefault {} + + protected implicit val runtime = 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 val cache = new RedisCache( connector, Builders.AsynchronousBuilder ) + } + + class MockedList extends MockedCache { + val data = new mutable.ListBuffer[ String ] + data.append( other, value, value ) + + protected val list = cache.list[ String ]( "key" ) + } + + class MockedSet extends MockedCache { + val data = mutable.Set[ String ]( other, value ) + + protected val set = cache.set[ String ]( "key" ) + } + + class MockedMap extends MockedCache { + val field = "field" + + protected val map = cache.map[ String ]( "key" ) + } + + class MockedAsyncRedis extends MockedConnector { + protected val cache = new AsyncRedis( connector ) + } + + class MockedSyncRedis extends MockedConnector { + protected val cache = new SyncRedis( connector ) + runtime.timeout returns akka.util.Timeout( 1.second ) + } + + class MockedJavaRedis extends AbstractMocked { + val expiration = 5.seconds + + protected val environment = mock[ Environment ] + protected val async = mock[ AsyncRedis ] + protected val cache: play.cache.AsyncCacheApi = new JavaRedis( async, environment ) + + environment.classLoader returns getClass.getClassLoader + } + + 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 new file mode 100644 index 00000000..64677868 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala @@ -0,0 +1,290 @@ +package play.api.cache.redis.impl + +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisCacheSpec( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + import Implicits._ + import RedisCacheImplicits._ + + import org.mockito.ArgumentMatchers._ + + val key = "key" + val value = "value" + val expiration = 1.second + + "Redis Cache" should { + + "get and miss" in new MockedCache { + connector.get[ String ]( anyString )( anyClassTag ) returns None + cache.get[ String ]( key ) must beNone.await + } + + "get and hit" in new MockedCache { + connector.get[ String ]( anyString )( anyClassTag ) returns Some( value ) + cache.get[ String ]( key ) must beSome( value ).await + } + + "get recover with default" in new MockedCache { + connector.get[ String ]( anyString )( anyClassTag ) returns ex + cache.get[ String ]( key ) must beNone.await + } + + "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 + } + + "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 + } + + "set" in new MockedCache { + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.set( key, value ) must beDone.await + } + + "set recover with default" in new MockedCache { + connector.set( anyString, anyString, any[ Duration ] ) returns ex + cache.set( key, value ) must beDone.await + } + + "set if not exists (exists)" in new MockedCache { + connector.setIfNotExists( anyString, anyString ) returns false + cache.setIfNotExists( key, value ) must beFalse.await + } + + "set if not exists (not exists)" in new MockedCache { + connector.setIfNotExists( anyString, anyString ) returns true + cache.setIfNotExists( key, value ) must beTrue.await + } + + "set if not exists (exists) with expiration" in new MockedCache { + connector.setIfNotExists( anyString, anyString ) returns false + connector.expire( anyString, any[ Duration ] ) returns unit + cache.setIfNotExists( key, value, expiration ) must beFalse.await + } + + "set if not exists (not exists) with expiration" in new MockedCache { + connector.setIfNotExists( anyString, anyString ) returns true + connector.expire( anyString, any[ Duration ] ) returns unit + cache.setIfNotExists( key, value, expiration ) must beTrue.await + } + + "set if not exists recover with default" in new MockedCache { + connector.setIfNotExists( anyString, anyString ) returns ex + cache.setIfNotExists( key, value ) must beTrue.await + } + + "set all" in new MockedCache { + connector.mSet( anyVarArgs ) returns unit + cache.setAll( key -> value ) must beDone.await + } + + "set all recover with default" in new MockedCache { + connector.mSet( anyVarArgs ) returns ex + cache.setAll( key -> value ) must beDone.await + } + + "set all if not exists (exists)" in new MockedCache { + connector.mSetIfNotExist( anyVarArgs ) returns false + cache.setAllIfNotExist( key -> value ) must beFalse.await + } + + "set all if not exists (not exists)" in new MockedCache { + connector.mSetIfNotExist( anyVarArgs ) returns true + cache.setAllIfNotExist( key -> value ) must beTrue.await + } + + "set all if not exists recover with default" in new MockedCache { + connector.mSetIfNotExist( anyVarArgs ) returns ex + cache.setAllIfNotExist( key -> value ) must beTrue.await + } + + "append" in new MockedCache { + connector.append( anyString, anyString ) returns 5L + cache.append( key, value ) must beDone.await + } + + "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 + } + + "append recover with default" in new MockedCache { + connector.append( anyString, anyString) returns ex + cache.append( key, value ) must beDone.await + } + + "expire" in new MockedCache { + connector.expire( anyString, any[ Duration ] ) returns unit + cache.expire( key, expiration ) must beDone.await + } + + "expire recover with default" in new MockedCache { + connector.expire( anyString, any[ Duration ] ) returns ex + cache.expire( key, expiration ) must beDone.await + } + + "matching" in new MockedCache { + connector.matching( anyString ) returns Seq( key ) + cache.matching( "pattern" ) must beEqualTo( Seq( key ) ).await + } + + "matching recover with default" in new MockedCache { + connector.matching( anyString ) returns ex + cache.matching( "pattern" ) must beEqualTo( Seq.empty ).await + } + + "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 + } + + "get or else (miss)" in new MockedCache with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.getOrElse( key )( doElse( value ) ) must beEqualTo( value ).await + orElse mustEqual 1 + } + + "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 + } + + "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 + } + + "get or future (miss)" in new MockedCache with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.getOrFuture( key )( doFuture( value ) ) must beEqualTo( value ).await + orElse mustEqual 1 + } + + "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 + } + + "get or future (failing orElse)" in new MockedCache with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + cache.getOrFuture( key )( failedFuture ) must throwA[ TimeoutException ].await + orElse mustEqual 2 + } + + "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 ] ) returns unit + // 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( anyString, anyString, any[ Duration ] ) + } + + "remove" in new MockedCache { + connector.remove( anyVarArgs ) returns unit + cache.remove( key ) must beDone.await + } + + "remove recover with default" in new MockedCache { + connector.remove( anyVarArgs ) returns ex + cache.remove( key ) must beDone.await + } + + "remove multiple" in new MockedCache { + connector.remove( anyVarArgs ) returns unit + cache.remove( key, key, key, key ) must beDone.await + } + + "remove multiple recover with default" in new MockedCache { + connector.remove( anyVarArgs ) returns ex + cache.remove( key, key, key, key ) must beDone.await + } + + "remove all" in new MockedCache { + connector.remove( anyVarArgs ) returns unit + cache.removeAll( Seq( key, key, key, key ): _* ) must beDone.await + } + + "remove all recover with default" in new MockedCache { + connector.remove( anyVarArgs ) returns ex + cache.removeAll( Seq( key, key, key, key ): _* ) must beDone.await + } + + "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 + } + + "remove matching recover with default" in new MockedCache { + connector.matching( anyVarArgs ) returns ex + cache.removeMatching( "pattern" ) must beDone.await + } + + "invalidate" in new MockedCache { + connector.invalidate( ) returns unit + cache.invalidate() must beDone.await + } + + "invalidate recover with default" in new MockedCache { + connector.invalidate( ) returns ex + cache.invalidate() must beDone.await + } + + "exists" in new MockedCache { + connector.exists( key ) returns true + cache.exists( key ) must beTrue.await + } + + "exists recover with default" in new MockedCache { + connector.exists( key ) returns ex + cache.exists( key ) must beFalse.await + } + + "increment" in new MockedCache { + connector.increment( key, 5L ) returns 10L + cache.increment( key, 5L ) must beEqualTo( 10L ).await + } + + "increment recover with default" in new MockedCache { + connector.increment( key, 5L ) returns ex + cache.increment( key, 5L ) must beEqualTo( 5L ).await + } + + "decrement" in new MockedCache { + connector.increment( key, -5L ) returns 10L + cache.decrement( key, 5L ) must beEqualTo( 10L ).await + } + + "decrement recover with default" in new MockedCache { + connector.increment( key, -5L ) returns ex + cache.decrement( key, 5L ) must beEqualTo( -5L ).await + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala deleted file mode 100644 index ddcbc0a4..00000000 --- a/src/test/scala/play/api/cache/redis/impl/RedisListSpec.scala +++ /dev/null @@ -1,268 +0,0 @@ -package play.api.cache.redis.impl - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.reflect.ClassTag - -import play.api.cache.redis._ - -import org.specs2.mutable.Specification - -/** - *Test of cache to be sure that keys are differentiated, expires etc.
- */ -class RedisListSpec extends Specification with Redis { - outer => - - private type Cache = RedisCache[ SynchronousResult ] - - private val workingConnector = injector.instanceOf[ RedisConnector ] - - def runtime( policy: RecoveryPolicy ) = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, policy, invocation = LazyInvocation ) - - // test proper implementation, no fails - new RedisListSuite( "implement", "redis-cache-implements", new RedisCache( workingConnector, Builders.SynchronousBuilder )( runtime( FailThrough ) ), AlwaysSuccess ) - - new RedisListSuite( "recover from", "redis-cache-recovery", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( runtime( RecoverWithDefault ) ), AlwaysDefault ) - - new RedisListSuite( "fail on", "redis-cache-fail", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( runtime( FailThrough ) ), AlwaysException ) - - class RedisListSuite( suiteName: String, prefix: String, cache: Cache, expectation: Expectation ) { - - def list[ T: ClassTag ]( key: String ) = cache.list[ T ]( key ) - - def strings( key: String ) = list[ String ]( key ) - - def objects( key: String ) = list[ SimpleObject ]( key ) - - "SynchronousRedisList" should { - - import expectation._ - - suiteName >> { - - "empty list when empty key" in { - strings( s"$prefix-list-size-1" ).size must expectsNow( beEqualTo( 0 ) ) - } - - "prepend head into empty key" in { - strings( s"$prefix-list-prepend-1A" ).prepend( "A" ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - ( "A" +: strings( s"$prefix-list-prepend-1B" ) ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - } - - "prepend head into existing key" in { - strings( s"$prefix-list-prepend-2A" ).prepend( "B" ).prepend( "A" ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - ( "A" +: "B" +: strings( s"$prefix-list-prepend-2B" ) ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "fail head prepending into non-list key" in { - cache.set( s"$prefix-list-prepend-3", "ABC" ) must expectsNow( beUnit ) - strings( s"$prefix-list-prepend-3" ).prepend( "A" ) must expectsNow( throwA[ IllegalArgumentException ], beAnInstanceOf[ RedisList[ String, AsynchronousResult ] ] ) - } - - "append into empty key" in { - strings( s"$prefix-list-append-1A" ).append( "A" ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - ( strings( s"$prefix-list-append-1B" ) :+ "A" ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - } - - "append into existing key" in { - strings( s"$prefix-list-append-2A" ).append( "B" ).append( "A" ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - ( strings( s"$prefix-list-append-2B" ) :+ "B" :+ "A" ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "prepend multiple values into empty key" in { - strings( s"$prefix-list-prepend-multiple-1" ).size must expectsNow( beEqualTo( 0 ) ) - ( List( "A", "B" ) ++: strings( s"$prefix-list-prepend-multiple-1" ) ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "prepend multiple values into existing key" in { - strings( s"$prefix-list-prepend-multiple-2" ).size must expectsNow( beEqualTo( 0 ) ) - ( List( "A", "B" ) ++: strings( s"$prefix-list-prepend-multiple-2" ) ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - ( List( "C", "D" ) ++: strings( s"$prefix-list-prepend-multiple-2" ) ).size must expectsNow( beEqualTo( 4 ), beEqualTo( 0 ) ) - } - - "append multiple values into empty key" in { - strings( s"$prefix-list-append-multiple-1" ).size must expectsNow( beEqualTo( 0 ) ) - ( strings( s"$prefix-list-append-multiple-1" ) :++ List( "A", "B" ) ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "append multiple values into existing key" in { - strings( s"$prefix-list-append-multiple-2" ).size must expectsNow( beEqualTo( 0 ) ) - ( strings( s"$prefix-list-append-multiple-2" ) :++ List( "A", "B" ) ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - ( strings( s"$prefix-list-append-multiple-2" ) :++ List( "C", "D" ) ).size must expectsNow( beEqualTo( 4 ), beEqualTo( 0 ) ) - } - - "fail on applying non-existing index" in { - strings( s"$prefix-list-apply-1" ) apply 0 must expectsNow( throwA[ NoSuchElementException ] ) - ( strings( s"$prefix-list-apply-1" ) :++ List( "A", "B", "C", "D", "E" ) ).toList must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-apply-1" ) apply 5 must expectsNow( throwA[ NoSuchElementException ] ) - } - - "apply existing key" in { - ( strings( s"$prefix-list-apply-2" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-apply-2" ) apply 0 must expectsNow( beEqualTo( "A" ), throwA[ NoSuchElementException ] ) - strings( s"$prefix-list-apply-2" ) apply 1 must expectsNow( beEqualTo( "B" ), throwA[ NoSuchElementException ] ) - strings( s"$prefix-list-apply-2" ) apply 2 must expectsNow( beEqualTo( "C" ), throwA[ NoSuchElementException ] ) - strings( s"$prefix-list-apply-2" ) apply -1 must expectsNow( beEqualTo( "E" ), throwA[ NoSuchElementException ] ) - } - - "get None on non-existing key" in { - strings( s"$prefix-list-get-1" ) get 0 must expectsNow( beNone ) - ( strings( s"$prefix-list-get-1" ) :++ List( "A", "B", "C", "D", "E" ) ).toList must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-get-1" ) get 5 must expectsNow( beNone ) - } - - "get Some on existing key" in { - ( strings( s"$prefix-list-get-2" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-get-2" ) get 0 must expectsNow( beSome( "A" ), beNone ) - strings( s"$prefix-list-get-2" ) get 1 must expectsNow( beSome( "B" ), beNone ) - strings( s"$prefix-list-get-2" ) get 2 must expectsNow( beSome( "C" ), beNone ) - strings( s"$prefix-list-get-2" ) get -1 must expectsNow( beSome( "E" ), beNone ) - } - - "get head on existing key" in { - ( strings( s"$prefix-list-head-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-head-1" ).head must expectsNow( beEqualTo( "A" ), throwA[ NoSuchElementException ] ) - } - - "fail head on non-existing key" in { - strings( s"$prefix-list-head-2" ).head must expectsNow( throwA[ NoSuchElementException ] ) - } - - "get Some on headOption on existing key" in { - ( strings( s"$prefix-list-head-option-2" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-head-option-2" ).headOption must expectsNow( beSome( "A" ), beNone ) - } - - "fail None on headOption on non-existing key" in { - strings( s"$prefix-list-head-option-1" ).headOption must expectsNow( beNone ) - } - - "get and remove on headPop on existing key" in { - strings( s"$prefix-list-head-pop-1" ).size must expectsNow( beEqualTo( 0 ) ) - ( strings( s"$prefix-list-head-pop-1" ) :++ List( "A", "B", "C", "D", "E" ) ).toList must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-head-pop-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-head-pop-1" ).headPop must expectsNow( beSome( "A" ), beNone ) - strings( s"$prefix-list-head-pop-1" ).headPop must expectsNow( beSome( "B" ), beNone ) - strings( s"$prefix-list-head-pop-1" ).size must expectsNow( beEqualTo( 3 ), beEqualTo( 0 ) ) - } - - "get None on headPop on non-existing key" in { - strings( s"$prefix-list-head-pop-2" ).size must expectsNow( beEqualTo( 0 ) ) - strings( s"$prefix-list-head-pop-2" ).headPop must expectsNow( beNone ) - strings( s"$prefix-list-head-pop-2" ).size must expectsNow( beEqualTo( 0 ) ) - } - - "get last on existing key" in { - ( strings( s"$prefix-list-last-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-last-1" ).last must expectsNow( beEqualTo( "E" ), throwA[ NoSuchElementException ] ) - } - - "fail last on non-existing key" in { - strings( s"$prefix-list-last-2" ).last must expectsNow( throwA[ NoSuchElementException ] ) - } - - "get empty list on toList on non-existing key" in { - strings( s"$prefix-list-toList-1" ).toList must expectsNow( beEqualTo( List.empty ) ) - } - - "get all values on toList on existing key" in { - ( List( "H", "G" ) ++: strings( s"$prefix-list-toList-2" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 7 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-toList-2" ).toList must expectsNow( beEqualTo( List( "G", "H", "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - } - - "insert before in empty list" in { - strings( s"$prefix-list-insert-1" ).size must expectsNow( beEqualTo( 0 ) ) - strings( s"$prefix-list-insert-1" ).insertBefore( "B", "A" ) must expectsNow( beNone ) - } - - "insert before in single-valued list" in { - ( strings( s"$prefix-list-insert-3" ) :++ List( "A" ) ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-insert-3" ).size must expectsNow( beEqualTo( 1 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-insert-3" ).insertBefore( "A", "B" ) must expectsNow( beSome( 2 ), beNone ) - strings( s"$prefix-list-insert-3" ).toList must expectsNow( beEqualTo( List( "B", "A" ) ), beEqualTo( List.empty ) ) - } - - "insert on index in existing list" in { - ( strings( s"$prefix-list-insert-2" ) :++ List( "A", "B", "C", "E", "F" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-insert-2" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-insert-2" ).insertBefore( "E", "D" ) must expectsNow( beSome( 6 ), beNone ) - strings( s"$prefix-list-insert-2" ).toList must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E", "F" ) ), beEqualTo( List.empty ) ) - } - - "override existing value in existing list" in { - ( strings( s"$prefix-list-set-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-set-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-set-1" ).set( 2, "G" ).toList must expectsNow( beEqualTo( List( "A", "B", "G", "D", "E" ) ), beEqualTo( List.empty ) ) - } - - "fail overwriting existing value at non-existing position" in { - ( strings( s"$prefix-list-set-2" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-set-2" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-set-2" ).set( 6, "G" ).toList must expectsNow( throwA[ IndexOutOfBoundsException ], beEqualTo( List.empty ) ) - } - - "return subset of list and not change the underlying collection" in { - ( strings( s"$prefix-list-view-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-view-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-view-1" ).view.all must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-view-1" ).view.take( 2 ) must expectsNow( beEqualTo( List( "A", "B" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-view-1" ).view.drop( 2 ) must expectsNow( beEqualTo( List( "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-view-1" ).view.slice( 2, 3 ) must expectsNow( beEqualTo( List( "C", "D" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-view-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - } - - "modify collection when trimming existing collection" in { - ( strings( s"$prefix-list-modify-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-modify-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-modify-1" ).view.all must expectsNow( beEqualTo( List( "A", "B", "C", "D", "E" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-modify-1" ).modify.take( 4 ).collection.view.all must expectsNow( beEqualTo( List( "A", "B", "C", "D" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-modify-1" ).modify.drop( 1 ).collection.view.all must expectsNow( beEqualTo( List( "B", "C", "D" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-modify-1" ).modify.slice( 1, 2 ).collection.view.all must expectsNow( beEqualTo( List( "C", "D" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-modify-1" ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "clear existing collection" in { - ( strings( s"$prefix-list-clear-1" ) :++ List( "A", "B", "C", "D", "E" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-clear-1" ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-clear-1" ).modify.clear().collection.size must expectsNow( beEqualTo( 0 ) ) - } - - "not fail on clearing non-existing collection" in { - strings( s"$prefix-list-clear-2" ).size must expectsNow( beEqualTo( 0 ) ) - strings( s"$prefix-list-clear-2" ).modify.clear().collection.size must expectsNow( beEqualTo( 0 ) ) - } - - "remove elements by value" in { - ( strings( s"$prefix-list-remove-by-value-1" ) :++ List( "A", "B", "A", "D", "A" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-remove-by-value-1" ).remove( "A" ).size must expectsNow( beEqualTo( 4 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-remove-by-value-1" ).remove( "A", count = 5 ).size must expectsNow( beEqualTo( 2 ), beEqualTo( 0 ) ) - } - - "remove elements by index" in { - ( strings( s"$prefix-list-remove-by-index-1" ) :++ List( "A", "B", "A", "D", "A" ) ).size must expectsNow( beEqualTo( 5 ), beEqualTo( 0 ) ) - strings( s"$prefix-list-remove-by-index-1" ).removeAt( 0 ).toList must expectsNow( beEqualTo( List( "B", "A", "D", "A" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-remove-by-index-1" ).removeAt( 2 ).toList must expectsNow( beEqualTo( List( "B", "A", "A" ) ), beEqualTo( List.empty ) ) - strings( s"$prefix-list-remove-by-index-1" ).removeAt( 5 ).toList must expectsNow( throwA[ IndexOutOfBoundsException ], beEqualTo( List.empty ) ) - } - - "work with objects" in { - val A = SimpleObject( "A", 1 ) - val B = SimpleObject( "B", 2 ) - val C = SimpleObject( "C", 3 ) - val D = SimpleObject( "D", 4 ) - val E = SimpleObject( "E", 5 ) - - ( A +: ( List( B ) ++: ( ( objects( s"$prefix-list-objects-1" ) :+ C ) :++ List( D ) ) ) ).size must expectsNow( beEqualTo( 4 ), beEqualTo( 0 ) ) - objects( s"$prefix-list-objects-1" ).view.all must expectsNow( beEqualTo( List( A, B, C, D ) ), beEqualTo( List.empty ) ) - objects( s"$prefix-list-objects-1" ).view.slice( 1, 2 ) must expectsNow( beEqualTo( List( B, C ) ), beEqualTo( List.empty ) ) - objects( s"$prefix-list-objects-1" ).set( 2, E ).toList must expectsNow( beEqualTo( List( A, B, E, D ) ), beEqualTo( List.empty ) ) - objects( s"$prefix-list-objects-1" ).headOption must expectsNow( beSome( A ), beNone ) - objects( s"$prefix-list-objects-1" ).lastOption must expectsNow( beSome( D ), beNone ) - objects( s"$prefix-list-objects-1" ).get( 1 ) must expectsNow( beSome( B ), beNone ) - } - } - } - } - -} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisListSpecs.scala b/src/test/scala/play/api/cache/redis/impl/RedisListSpecs.scala new file mode 100644 index 00000000..946fe1f6 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisListSpecs.scala @@ -0,0 +1,314 @@ +package play.api.cache.redis.impl + +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisListSpecs( 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 + + there were two( connector ).listAppend( key, value ) + there were one( connector ).listAppend( key, value, value ) + } + + "append (failing)" in new MockedList { + connector.listAppend( key, value ) returns ex + // verify + list.append( value ) must beEqualTo( list ).await + + there were one( connector ).listAppend( key, value ) + } + + "get (miss)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns Seq( value ) + list.get( 5 ) must beSome( value ).await + } + + "get (hit)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns Seq.empty[ String ] + list.get( 5 ) must beNone.await + } + + "get (failure)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns ex + list.get( 5 ) must beNone.await + } + + "apply (hit)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns Seq( value ) + list( 5 ) must beEqualTo( value ).await + } + + "apply (miss)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns Seq.empty[ String ] + list( 5 ) must throwA[ NoSuchElementException ].await + } + + "apply (failing)" in new MockedList { + connector.listSlice[ String ]( beEq( key ), anyInt, anyInt )( anyClassTag ) returns ex + list( 5 ) must throwA[ NoSuchElementException ].await + } + + "head pop" in new MockedList { + connector.listHeadPop[ String ]( beEq( key ) )( anyClassTag ) returns None + list.headPop must beNone.await + } + + "head pop (failing)" in new MockedList { + connector.listHeadPop[ String ]( beEq( key ) )( anyClassTag ) returns ex + list.headPop must beNone.await + } + + "size" in new MockedList { + connector.listSize( key ) returns 2L + list.size must beEqualTo( 2L ).await + } + + "size (failing)" in new MockedList { + connector.listSize( key ) returns ex + list.size must beEqualTo( 0L ).await + } + + "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 ) + } + + "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 ) + } + + "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 ) + } + + "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 ) + } + + "empty list" in new MockedList { + connector.listSize( beEq( key ) ) returns 0L + list.isEmpty must beTrue.await + list.nonEmpty must beFalse.await + } + + "non-empty list" in new MockedList { + connector.listSize( beEq( key ) ) returns 1L + list.isEmpty must beFalse.await + list.nonEmpty must beTrue.await + } + + "empty/non-empty list (failing)" in new MockedList { + connector.listSize( beEq( key ) ) returns ex + list.isEmpty must beTrue.await + list.nonEmpty must beFalse.await + } + + "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 ) + } + + "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 ) + } + + "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 ) + } + + "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" ) + } + + "view all" in new MockedList { + connector.listSlice[ String ]( anyString, anyInt, anyInt )( anyClassTag ) returns data + list.view.all must beEqualTo( data ).await + there were one( connector ).listSlice[ String ]( key, 0, -1 ) + } + + "view take" in new MockedList { + connector.listSlice[ String ]( anyString, anyInt, anyInt )( anyClassTag ) returns data + list.view.take( 2 ) must beEqualTo( data ).await + there were one( connector ).listSlice[ String ]( key, 0, 1 ) + } + + "view drop" in new MockedList { + connector.listSlice[ String ]( anyString, anyInt, anyInt )( anyClassTag ) returns data + list.view.drop( 2 ) must beEqualTo( data ).await + there were one( connector ).listSlice[ String ]( key, 2, -1 ) + } + + "view slice" in new MockedList { + connector.listSlice[ String ]( anyString, anyInt, anyInt )( anyClassTag ) returns data + list.view.slice( 1, 2 ) must beEqualTo( data ).await + there were one( connector ).listSlice[ String ]( key, 1, 2 ) + } + + "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 + } + + "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 ) + } + + "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 ) + } + + "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 + } + + "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 ) + } + + "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 ) + } + + "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 + } + + "toList" in new MockedList { + connector.listSlice[ String ]( anyString, anyInt, anyInt )( anyClassTag ) returns data + list.toList must beEqualTo( data ).await + there were one( connector ).listSlice[ String ]( key, 0, -1 ) + } + + "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 ) + } + + "modify collection" in new MockedList { + list.modify.collection mustEqual list + } + + "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 ) + } + + "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 ) + } + + "modify clear" in new MockedList { + connector.remove( anyVarArgs ) returns unit + list.modify.clear() must not( throwA[ Throwable ] ).await + there were one( connector ).remove( key ) + } + + "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 ) + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisMapSpecs.scala b/src/test/scala/play/api/cache/redis/impl/RedisMapSpecs.scala index 4b390fc2..627f8d7b 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisMapSpecs.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisMapSpecs.scala @@ -1,152 +1,144 @@ package play.api.cache.redis.impl -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.reflect.ClassTag - import play.api.cache.redis._ +import org.specs2.concurrent.ExecutionEnv import org.specs2.mutable.Specification /** - *Test of cache to be sure that keys are differentiated, expires etc.
+ * @author Karel Cemus */ -class RedisMapSpecs extends Specification with Redis { - outer => - - import play.api.cache.redis.TestHelpers._ - - private type Cache = RedisCache[ SynchronousResult ] - - private val workingConnector = injector.instanceOf[ RedisConnector ] - - implicit def runtime( policy: RecoveryPolicy ) = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, policy, invocation = LazyInvocation ) - - // test proper implementation, no fails - new RedisMapSuite( "implement", "redis-cache-implements", new RedisCache( workingConnector, Builders.SynchronousBuilder )( FailThrough ), AlwaysSuccess ) - - new RedisMapSuite( "recover from", "redis-cache-recovery", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( RecoverWithDefault ), AlwaysDefault ) - - new RedisMapSuite( "fail on", "redis-cache-fail", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( FailThrough ), AlwaysException ) - - class RedisMapSuite( suiteName: String, prefix: String, cache: Cache, expectation: Expectation ) { - - def map[ T: ClassTag ]( key: String ) = cache.map[ T ]( key ) +class RedisMapSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { - def numbers( implicit theKey: Key ) = map[ Long ]( theKey.key ) + import Implicits._ + import RedisCacheImplicits._ - def strings( implicit theKey: Key ) = map[ String ]( theKey.key ) + import org.mockito.ArgumentMatchers._ - def objects( implicit theKey: Key ) = map[ SimpleObject ]( theKey.key ) + "Redis Map" should { - "SynchronousRedisMap" should { - - import expectation._ - - suiteName >> { - - "add into the map" in { - implicit val key: Key = s"$prefix-map-add" - - strings.size must expectsNow( 0 ) - strings.isEmpty must expectsNow( beTrue ) - strings.nonEmpty must expectsNow( beFalse ) - - strings.add( "KA", "A1" ).add( "KB", "B" ).add( "KC", "C" ).add( "KA", "A2" ).size must expectsNow( 3, 0 ) - strings.isEmpty must expectsNow( beFalse, beTrue ) - strings.nonEmpty must expectsNow( beTrue, beFalse ) - - strings.toMap must expectsNow( Map( "KA" -> "A2", "KB" -> "B", "KC" -> "C" ), Map.empty[ String, String ] ) - strings.contains( "KA" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KB" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KC" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KD" ) must expectsNow( beFalse, beFalse ) - } - - "increment in the map" in { - implicit val key: Key = s"$prefix-map-increment" + "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 ) + } - numbers.size must expectsNow( 0 ) - numbers.isEmpty must expectsNow( beTrue ) - numbers.nonEmpty must expectsNow( beFalse ) + "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 ) + } - numbers.add( "A", 0 ).add( "B", 5 ).add( "C", 10 ).size must expectsNow( 3, 0 ) - numbers.isEmpty must expectsNow( beFalse, beTrue ) - numbers.nonEmpty must expectsNow( beTrue, beFalse ) + "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 ) + } - numbers.increment( "A" ) must expectsNow( 1, 1 ) - numbers.increment( "B", 2 ) must expectsNow( 7, 2 ) - numbers.increment( "C", -2 ) must expectsNow( 8, -2 ) - numbers.increment( "D", 10 ) must expectsNow( 10, 10 ) - numbers.toMap must expectsNow( Map( "A" -> 1, "B" -> 7, "C" -> 8, "D" -> 10 ), Map.empty[ String, Long ] ) - } + "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 ) + } - "remove from the map" in { - implicit val key: Key = s"$prefix-map-remove" + "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 + } - strings.size must expectsNow( 0 ) - strings.add( "KA", "A1" ).add( "KB", "B" ).add( "KC", "C" ).add( "KA", "A2" ).size must expectsNow( 3, 0 ) + "contains (failing)" in new MockedMap { + connector.hashExists( anyString, anyString ) returns ex + map.contains( field ) must beFalse.await + there were one( connector ).hashExists( key, field ) + } - strings.contains( "KA" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KB" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KC" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KD" ) must expectsNow( beFalse, beFalse ) + "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 ) + } - strings.remove( "KA" ).size must expectsNow( 2, 0 ) - strings.contains( "KA" ) must expectsNow( beFalse, beFalse ) - strings.contains( "KB" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KC" ) must expectsNow( beTrue, beFalse ) - strings.contains( "KD" ) must expectsNow( beFalse, beFalse ) + "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 ) + } - strings.remove( "KB", "KC" ).size must expectsNow( 0, 0 ) - strings.contains( "KA" ) must expectsNow( beFalse, beFalse ) - strings.contains( "KB" ) must expectsNow( beFalse, beFalse ) - strings.contains( "KC" ) must expectsNow( beFalse, beFalse ) - strings.contains( "KD" ) must expectsNow( beFalse, beFalse ) - } + "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 ) + } - "working with the map at non-map key" in { - implicit val key: Key = s"$prefix-map-invalid" + "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 ) + } - cache.set( key.key, "invalid" ) must expectsNow( beUnit ) - strings.add( "KA", "A" ) must expectsNow( throwA[ IllegalArgumentException ], beAnInstanceOf[ RedisMap[ String, AsynchronousResult ] ] ) - } + "toMap" in new MockedMap { + connector.hashGetAll[ String ]( anyString )( anyClassTag ) returns Map( field -> value ) + map.toMap must beEqualTo( Map( field -> value ) ).await + } - "objects in the map" in { - implicit val key: Key = s"$prefix-map-objects" + "toMap (failing)" in new MockedMap { + connector.hashGetAll[ String ]( anyString )( anyClassTag ) returns ex + map.toMap must beEqualTo( Map.empty ).await + } - def A = SimpleObject( "A", 1 ) + "keySet" in new MockedMap { + connector.hashKeys( anyString ) returns Set( field ) + map.keySet must beEqualTo( Set( field ) ).await + } - def B = SimpleObject( "B", 2 ) + "keySet (failing)" in new MockedMap { + connector.hashKeys( anyString ) returns ex + map.keySet must beEqualTo( Set.empty ).await + } - def C = SimpleObject( "C", 3 ) + "values" in new MockedMap { + connector.hashValues[ String ]( anyString )( anyClassTag ) returns Set( value ) + map.values must beEqualTo( Set( value ) ).await + } - def D = SimpleObject( "D", 4 ) + "values (failing)" in new MockedMap { + connector.hashValues[ String ]( anyString )( anyClassTag ) returns ex + map.values must beEqualTo( Set.empty ).await + } - def E = SimpleObject( "E", 5 ) + "size" in new MockedMap { + connector.hashSize( key ) returns 2L + map.size must beEqualTo( 2L ).await + } - objects.size must expectsNow( 0 ) - objects.add( "A", A ).add( "B", B ).add( "C", C ).add( "A", A ).size must expectsNow( 3, 0 ) + "size (failing)" in new MockedMap { + connector.hashSize( key ) returns ex + map.size must beEqualTo( 0L ).await + } - objects.contains( "A" ) must expectsNow( beTrue, beFalse ) - objects.contains( "B" ) must expectsNow( beTrue, beFalse ) - objects.contains( "C" ) must expectsNow( beTrue, beFalse ) - objects.contains( "D" ) must expectsNow( beFalse, beFalse ) + "empty map" in new MockedMap { + connector.hashSize( beEq( key ) ) returns 0L + map.isEmpty must beTrue.await + map.nonEmpty must beFalse.await + } - objects.remove( "A" ).size must expectsNow( 2, 0 ) - objects.contains( "A" ) must expectsNow( beFalse, beFalse ) - objects.contains( "B" ) must expectsNow( beTrue, beFalse ) - objects.contains( "C" ) must expectsNow( beTrue, beFalse ) - objects.contains( "D" ) must expectsNow( beFalse, beFalse ) + "non-empty map" in new MockedMap { + connector.hashSize( beEq( key ) ) returns 1L + map.isEmpty must beFalse.await + map.nonEmpty must beTrue.await + } - objects.remove( "B", "C", "D" ).size must expectsNow( 0, 0 ) - objects.contains( "A" ) must expectsNow( beFalse, beFalse ) - objects.contains( "B" ) must expectsNow( beFalse, beFalse ) - objects.contains( "C" ) must expectsNow( beFalse, beFalse ) - objects.contains( "D" ) must expectsNow( beFalse, beFalse ) - } - } + "empty/non-empty map (failing)" in new MockedMap { + connector.hashSize( beEq( key ) ) returns ex + map.isEmpty must beTrue.await + map.nonEmpty must beFalse.await } } - } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala deleted file mode 100644 index cbdd6aae..00000000 --- a/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpec.scala +++ /dev/null @@ -1,40 +0,0 @@ -package play.api.cache.redis.impl - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ - -import play.api.cache.redis._ - -import org.specs2.mutable.Specification - -/** - *Test of cache to be sure that keys are differentiated, expires etc.
- */ -class RedisPrefixSpec extends Specification with Redis { outer => - import RedisRuntime._ - - private type Cache = RedisCache[ SynchronousResult ] - - private val workingConnector = injector.instanceOf[ RedisConnector ] - - def runtime( prefix: Option[ String ] ) = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, FailThrough, invocation = LazyInvocation, prefix ) - - val unprefixed = new RedisCache( workingConnector, Builders.SynchronousBuilder )( runtime( prefix = None ) ) - - "RedisPrefix" should { - - "apply when defined" in { - val cache = new RedisCache( workingConnector, Builders.SynchronousBuilder )( runtime( prefix = Some( "prefixed" ) ) ) - workingConnector.get[ String ]( "prefixed:prefix-test:defined" ).sync must beNone - cache.set( "prefix-test:defined", "value" ) - workingConnector.get[ String ]( "prefixed:prefix-test:defined" ).sync must beSome( "value" ) - } - - "not apply when is empty" in { - val cache = new RedisCache( workingConnector, Builders.SynchronousBuilder )( runtime( prefix = None ) ) - workingConnector.get[ String ]( "prefix-test:defined" ).sync must beNone - cache.set( "prefix-test:defined", "value" ) - workingConnector.get[ String ]( "prefix-test:defined" ).sync must beSome( "value" ) - } - } -} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpecs.scala b/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpecs.scala new file mode 100644 index 00000000..91891674 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisPrefixSpecs.scala @@ -0,0 +1,39 @@ +package play.api.cache.redis.impl + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisPrefixSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + + import Implicits._ + import RedisCacheImplicits._ + + import org.mockito.ArgumentMatchers._ + + "RedisPrefix" should { + + "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" ) + } + + "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 ) + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpecs.scala b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpecs.scala new file mode 100644 index 00000000..215f80a2 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpecs.scala @@ -0,0 +1,55 @@ +package play.api.cache.redis.impl + +import play.api.cache.redis._ + +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class RedisRuntimeSpecs extends Specification with WithApplication { + + import Implicits._ + + implicit val recoveryResolver = 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 (B)" in { + val runtime = RedisRuntime( + instance = defaultInstance, + recovery = "log-and-default", + invocation = "lazy", + prefix = Some( "prefix" ) + ) + + runtime.policy must beAnInstanceOf[ 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 ] + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/RedisSetSpecs.scala b/src/test/scala/play/api/cache/redis/impl/RedisSetSpecs.scala index e29c6f01..a7ed6b3a 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisSetSpecs.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisSetSpecs.scala @@ -1,129 +1,98 @@ package play.api.cache.redis.impl -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.reflect.ClassTag - import play.api.cache.redis._ +import org.specs2.concurrent.ExecutionEnv import org.specs2.mutable.Specification /** - *Test of cache to be sure that keys are differentiated, expires etc.
+ * @author Karel Cemus */ -class RedisSetSpecs extends Specification with Redis { - outer => - - import play.api.cache.redis.TestHelpers._ - - private type Cache = RedisCache[ SynchronousResult ] - - private val workingConnector = injector.instanceOf[ RedisConnector ] - - implicit def runtime( policy: RecoveryPolicy ) = RedisRuntime( "play", 3.minutes, ExecutionContext.Implicits.global, policy, invocation = LazyInvocation ) - - // test proper implementation, no fails - new RedisSetSuite( "implement", "redis-cache-implements", new RedisCache( workingConnector, Builders.SynchronousBuilder )( FailThrough ), AlwaysSuccess ) - - new RedisSetSuite( "recover from", "redis-cache-recovery", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( RecoverWithDefault ), AlwaysDefault ) - - new RedisSetSuite( "fail on", "redis-cache-fail", new RedisCache( FailingConnector, Builders.SynchronousBuilder )( FailThrough ), AlwaysException ) - - class RedisSetSuite( suiteName: String, prefix: String, cache: Cache, expectation: Expectation ) { - - def set[ T: ClassTag ]( key: String ) = cache.set[ T ]( key ) - - def strings( implicit theKey: Key ) = set[ String ]( theKey.key ) - - def objects( implicit theKey: Key ) = set[ SimpleObject ]( theKey.key ) - - "SynchronousRedisSet" should { - - import expectation._ - - suiteName >> { +class RedisSetSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + import Implicits._ + import RedisCacheImplicits._ - "add into the set" in { - implicit val key: Key = s"$prefix-set-add" + import org.mockito.ArgumentMatchers._ - strings.size must expectsNow( 0 ) - strings.isEmpty must expectsNow( beTrue ) - strings.nonEmpty must expectsNow( beFalse ) + "Redis Set" should { - strings.add( "A", "B", "D" ).add( "C" ).add( "A" ).size must expectsNow( 4, 0 ) - strings.isEmpty must expectsNow( beFalse, beTrue ) - strings.nonEmpty must expectsNow( beTrue, beFalse ) - - strings.toSet must expectsNow( Set( "A", "B", "C", "D" ), Set.empty ) - strings.contains( "A" ) must expectsNow( beTrue, beFalse ) - strings.contains( "B" ) must expectsNow( beTrue, beFalse ) - strings.contains( "C" ) must expectsNow( beTrue, beFalse ) - strings.contains( "D" ) must expectsNow( beTrue, beFalse ) - strings.contains( "E" ) must expectsNow( beFalse, beFalse ) - } - - "remove from the set" in { - implicit val key: Key = s"$prefix-set-remove" + "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 ) + } - strings.size must expectsNow( 0 ) - strings.add( "A", "B", "D" ).size must expectsNow( 3, 0 ) + "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 ) + } - strings.contains( "A" ) must expectsNow( beTrue, beFalse ) - strings.contains( "B" ) must expectsNow( beTrue, beFalse ) - strings.contains( "C" ) must expectsNow( beFalse, beFalse ) - strings.contains( "D" ) must expectsNow( beTrue, beFalse ) + "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 + } - strings.remove( "A" ).size must expectsNow( 2, 0 ) - strings.contains( "A" ) must expectsNow( beFalse, beFalse ) - strings.contains( "B" ) must expectsNow( beTrue, beFalse ) - strings.contains( "C" ) must expectsNow( beFalse, beFalse ) - strings.contains( "D" ) must expectsNow( beTrue, beFalse ) + "contains (failing)" in new MockedSet { + connector.setIsMember( anyString, anyString ) returns ex + set.contains( value ) must beFalse.await + there were one( connector ).setIsMember( key, value ) + } - strings.remove( "B", "C", "D" ).size must expectsNow( 0, 0 ) - strings.contains( "A" ) must expectsNow( beFalse, beFalse ) - strings.contains( "B" ) must expectsNow( beFalse, beFalse ) - strings.contains( "C" ) must expectsNow( beFalse, beFalse ) - strings.contains( "D" ) must expectsNow( beFalse, beFalse ) - } + "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 ) + } - "working with the set at non-set key" in { - implicit val key: Key = s"$prefix-set-invalid" + "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 ) + } - cache.set( key.key, "invalid" ) must expectsNow( beUnit ) - strings.add( "A" ) must expectsNow( throwA[ IllegalArgumentException ], beAnInstanceOf[ RedisSet[ String, AsynchronousResult ] ] ) - } + "toSet" in new MockedSet { + connector.setMembers[ String ]( anyString )( anyClassTag ) returns ( data.toSet: Set[ String ] ) + set.toSet must beEqualTo( data ).await + } - "objects in the set" in { - implicit val key: Key = s"$prefix-set-objects" + "toSet (failing)" in new MockedSet { + connector.setMembers[ String ]( anyString )( anyClassTag ) returns ex + set.toSet must beEqualTo( Set.empty ).await + } - def A = SimpleObject( "A", 1 ) - def B = SimpleObject( "B", 2 ) - def C = SimpleObject( "C", 3 ) - def D = SimpleObject( "D", 4 ) - def E = SimpleObject( "E", 5 ) + "size" in new MockedSet { + connector.setSize( key ) returns 2L + set.size must beEqualTo( 2L ).await + } - objects.size must expectsNow( 0 ) - objects.add( A, B, D ).add( A ).size must expectsNow( 3, 0 ) + "size (failing)" in new MockedSet { + connector.setSize( key ) returns ex + set.size must beEqualTo( 0L ).await + } - objects.contains( A ) must expectsNow( beTrue, beFalse ) - objects.contains( B ) must expectsNow( beTrue, beFalse ) - objects.contains( C ) must expectsNow( beFalse, beFalse ) - objects.contains( D ) must expectsNow( beTrue, beFalse ) + "empty set" in new MockedSet { + connector.setSize( beEq( key ) ) returns 0L + set.isEmpty must beTrue.await + set.nonEmpty must beFalse.await + } - objects.remove( A ).size must expectsNow( 2, 0 ) - objects.contains( A ) must expectsNow( beFalse, beFalse ) - objects.contains( B ) must expectsNow( beTrue, beFalse ) - objects.contains( C ) must expectsNow( beFalse, beFalse ) - objects.contains( D ) must expectsNow( beTrue, beFalse ) + "non-empty set" in new MockedSet { + connector.setSize( beEq( key ) ) returns 1L + set.isEmpty must beFalse.await + set.nonEmpty must beTrue.await + } - objects.remove( B, C, D ).size must expectsNow( 0, 0 ) - objects.contains( A ) must expectsNow( beFalse, beFalse ) - objects.contains( B ) must expectsNow( beFalse, beFalse ) - objects.contains( C ) must expectsNow( beFalse, beFalse ) - objects.contains( D ) must expectsNow( beFalse, beFalse ) - } - } + "empty/non-empty set (failing)" in new MockedSet { + connector.setSize( beEq( key ) ) returns ex + set.isEmpty must beTrue.await + set.nonEmpty must beFalse.await } } - } diff --git a/src/test/scala/play/api/cache/redis/impl/SyncRedisSpecs.scala b/src/test/scala/play/api/cache/redis/impl/SyncRedisSpecs.scala new file mode 100644 index 00000000..2c6d9b5d --- /dev/null +++ b/src/test/scala/play/api/cache/redis/impl/SyncRedisSpecs.scala @@ -0,0 +1,42 @@ +package play.api.cache.redis.impl + +import scala.concurrent.duration._ + +import play.api.cache.redis._ + +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification + +/** + * @author Karel Cemus + */ +class SyncRedisSpecs( implicit ee: ExecutionEnv ) extends Specification with ReducedMockito { + + import Implicits._ + import RedisCacheImplicits._ + + import org.mockito.ArgumentMatchers._ + + "SyncRedis" should { + + "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 + } + + "get or else (miss)" in new MockedSyncRedis with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns None + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.getOrElse( key )( doElse( value ) ) must beEqualTo( value ) + orElse mustEqual 1 + } + + "get or else (failure)" in new MockedSyncRedis with OrElse { + connector.get[ String ]( anyString )( anyClassTag ) returns ex + connector.set( anyString, anyString, any[ Duration ] ) returns unit + cache.getOrElse( key )( doElse( value ) ) must beEqualTo( value ) + orElse mustEqual 1 + } + } +} diff --git a/src/test/scala/play/api/cache/redis/impl/SynchronousCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/SynchronousCacheSpec.scala deleted file mode 100644 index 65be952c..00000000 --- a/src/test/scala/play/api/cache/redis/impl/SynchronousCacheSpec.scala +++ /dev/null @@ -1,194 +0,0 @@ -package play.api.cache.redis.impl - -import java.util.Date -import java.util.concurrent.atomic.AtomicInteger - -import scala.concurrent.Future -import scala.concurrent.duration._ - -import play.api.cache.redis.{CacheApi, Redis, SimpleObject} - -import org.joda.time.DateTime -import org.specs2.mutable.Specification - -/** - *Test of cache to be sure that keys are differentiated, expires etc.
- */ -class SynchronousCacheSpec extends Specification with Redis { - - private type Cache = CacheApi - - private val Cache = injector.instanceOf[ Cache ] - - val prefix = "sync" - - "SynchronousCacheApi" should { - - "miss on get" in { - Cache.get[ String ]( s"$prefix-test-1" ) must beNone - } - - "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" ) - } - - "expire refreshes expiration" in { - Cache.set( s"$prefix-test-10", "value", 2.second ) - Cache.get[ String ]( s"$prefix-test-10" ) must beSome( "value" ) - Cache.expire( s"$prefix-test-10", 1.minute ) - // wait until the first duration expires - Thread.sleep( 3000 ) - Cache.get[ String ]( s"$prefix-test-10" ) must beSome( "value" ) - } - - "positive exists on existing keys" in { - Cache.set( s"$prefix-test-11", "value" ) - Cache.exists( s"$prefix-test-11" ) must beTrue - } - - "negative exists on expired and missing keys" in { - Cache.set( s"$prefix-test-12A", "value", 1.second ) - // wait until the duration expires - Thread.sleep( 2000 ) - Cache.exists( s"$prefix-test-12A" ) must beFalse - Cache.exists( s"$prefix-test-12B" ) must beFalse - } - - "miss after remove" in { - Cache.set( s"$prefix-test-3", "value" ) - Cache.get[ String ]( s"$prefix-test-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-3" ) - Cache.get[ String ]( s"$prefix-test-3" ) must beNone - } - - "miss after timeout" in { - // set - Cache.set( s"$prefix-test-4", "value", 1.second ) - Cache.get[ String ]( s"$prefix-test-4" ) must beSome[ Any ] - // wait until it expires - Thread.sleep( 1500 ) - // miss - Cache.get[ String ]( s"$prefix-test-4" ) must beNone - } - - "miss at first getOrElse " in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-5" )( counter ) mustEqual "value" - counter.get must beEqualTo( 1 ) - } - - "hit at second getOrElse" in { - val counter = new AtomicInteger( 0 ) - for ( index <- 1 to 10 ) Cache.getOrElseCounting( s"$prefix-test-6" )( counter ) mustEqual "value" - counter.get must beEqualTo( 1 ) - } - - "distinct different keys" in { - val counter = new AtomicInteger( 0 ) - Cache.getOrElseCounting( s"$prefix-test-7A" )( counter ) mustEqual "value" - Cache.getOrElseCounting( s"$prefix-test-7B" )( counter ) mustEqual "value" - counter.get must beEqualTo( 2 ) - } - - "perform future and store result" in { - val counter = new AtomicInteger( 0 ) - // perform test - for ( index <- 1 to 5 ) Cache.getOrFutureCounting( s"$prefix-test-8" )( counter ).sync mustEqual "value" - // verify - counter.get must beEqualTo( 1 ) - } - - "propagate fail in future" in { - Cache.getOrFuture[ String ]( s"$prefix-test-9" ){ - Future.failed( new IllegalStateException( "Exception in test." ) ) - }.sync must throwA( new IllegalStateException( "Exception in test." ) ) - } - - "support list" in { - // store value - Cache.set( s"$prefix-list", List( "A", "B", "C" ) ) - // recall - Cache.get[ List[ String ] ]( s"$prefix-list" ) must beSome[ List[ String ] ]( List( "A", "B", "C" ) ) - } - - "support a byte" in { - Cache.set( s"$prefix-type.byte", 0xAB.toByte ) - Cache.get[ Byte ]( s"$prefix-type.byte" ) must beSome[ Byte ] - Cache.get[ Byte ]( s"$prefix-type.byte" ) must beSome( 0xAB.toByte ) - } - - "support a char" in { - Cache.set( s"$prefix-type.char.1", 'a' ) - Cache.get[ Char ]( s"$prefix-type.char.1" ) must beSome[ Char ] - Cache.get[ Char ]( s"$prefix-type.char.1" ) must beSome( 'a' ) - Cache.set( s"$prefix-type.char.2", 'b' ) - Cache.get[ Char ]( s"$prefix-type.char.2" ) must beSome( 'b' ) - Cache.set( s"$prefix-type.char.3", 'č' ) - Cache.get[ Char ]( s"$prefix-type.char.3" ) must beSome( 'č' ) - } - - "support a short" in { - Cache.set( s"$prefix-type.short", 12.toShort ) - Cache.get[ Short ]( s"$prefix-type.short" ) must beSome[ Short ] - Cache.get[ Short ]( s"$prefix-type.short" ) must beSome( 12.toShort ) - } - - "support an int" in { - Cache.set( s"$prefix-type.int", 15 ) - Cache.get[ Int ]( s"$prefix-type.int" ) must beSome( 15 ) - } - - "support a long" in { - Cache.set( s"$prefix-type.long", 144L ) - Cache.get[ Long ]( s"$prefix-type.long" ) must beSome[ Long ] - Cache.get[ Long ]( s"$prefix-type.long" ) must beSome( 144L ) - } - - "support a float" in { - Cache.set( s"$prefix-type.float", 1.23f ) - Cache.get[ Float ]( s"$prefix-type.float" ) must beSome[ Float ] - Cache.get[ Float ]( s"$prefix-type.float" ) must beSome( 1.23f ) - } - - "support a double" in { - Cache.set( s"$prefix-type.double", 3.14 ) - Cache.get[ Double ]( s"$prefix-type.double" ) must beSome[ Double ] - Cache.get[ Double ]( s"$prefix-type.double" ) must beSome( 3.14 ) - } - - "support a date" in { - Cache.set( s"$prefix-type.date", new Date( 123 ) ) - Cache.get[ Date ]( s"$prefix-type.date" ) must beSome( new Date( 123 ) ) - } - - "support a datetime" in { - Cache.set( s"$prefix-type.datetime", new DateTime( 123456 ) ) - Cache.get[ DateTime ]( s"$prefix-type.datetime" ) must beSome( new DateTime( 123456 ) ) - } - - "support a custom classes" in { - Cache.set( s"$prefix-type.object", SimpleObject( "B", 3 ) ) - Cache.get[ SimpleObject ]( s"$prefix-type.object" ) must beSome( SimpleObject( "B", 3 ) ) - } - - "support a null" in { - Cache.set( s"$prefix-type.null", null ) - Cache.get[ SimpleObject ]( s"$prefix-type.null" ) must beNone - } - - "remove multiple keys at once" in { - Cache.set( s"$prefix-test-remove-multiple-1", "value" ) - Cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-multiple-2", "value" ) - Cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must beSome[ Any ] - Cache.set( s"$prefix-test-remove-multiple-3", "value" ) - Cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must beSome[ Any ] - Cache.remove( s"$prefix-test-remove-multiple-1", s"$prefix-test-remove-multiple-2", s"$prefix-test-remove-multiple-3" ) - Cache.get[ String ]( s"$prefix-test-remove-multiple-1" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-multiple-2" ) must beNone - Cache.get[ String ]( s"$prefix-test-remove-multiple-3" ) must beNone - } - } -} diff --git a/src/test/scala/play/api/cache/redis/impl/package.scala b/src/test/scala/play/api/cache/redis/impl/package.scala deleted file mode 100644 index 35d02560..00000000 --- a/src/test/scala/play/api/cache/redis/impl/package.scala +++ /dev/null @@ -1,109 +0,0 @@ -package play.api.cache.redis - -import java.util.concurrent.Callable -import java.util.concurrent.atomic.AtomicInteger - -import scala.concurrent.Future -import scala.language.{higherKinds, implicitConversions} - -import org.specs2.matcher._ - -/** - * @author Karel Cemus - */ -package object impl extends LowPriorityImplicits { - - val FailingConnector = connector.FailingConnector - - val FailThrough = new FailThrough { - override def name = "FailThrough" - } - - val RecoverWithDefault = new RecoverWithDefault { - override def name = "RecoverWithDefault" - } - - trait Expectation extends RedisMatcher { - - import ExceptionMatchers._ - - def expectsNow[ T ]( success: => Matcher[ T ] ): Matcher[ T ] = - expectsNow( success, success ) - - def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ] ): Matcher[ T ] = - expectsNow( success, default, throwA[ ExecutionFailedException ] ) - - def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ T ] - - def expects[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ AsynchronousResult[ T ] ] = - expectsNow( success, default, exception ) - - def expects[ T ]( success: => Matcher[ T ], default: => Matcher[ T ] ): Matcher[ AsynchronousResult[ T ] ] = - expects( success, default, throwA[ ExecutionFailedException ] ) - - def expects[ T ]( successAndDefault: => Matcher[ T ] ): Matcher[ AsynchronousResult[ T ] ] = - expects( successAndDefault, successAndDefault ) - } - - object AlwaysDefault extends Expectation { - override def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ T ] = default - } - - object AlwaysException extends Expectation { - override def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ T ] = exception - } - - object AlwaysSuccess extends Expectation { - override def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ T ] = success - } - - object SuccessOrDefault extends Expectation { - override def expectsNow[ T ]( success: => Matcher[ T ], default: => Matcher[ T ], exception: => Matcher[ T ] ): Matcher[ T ] = success or default - } - - object beUnit extends Matcher[ Any ] { - def apply[ S <: Any ]( value: Expectable[ S ] ): MatchResult[ S ] = result( test = true, value.description + " is Unit", value.description + " is not Unit", value.evaluate ) - } - - implicit class JavaAccumulatorCache( val cache: play.cache.SyncCacheApi ) extends AnyVal { - private type Accumulator = AtomicInteger - - /** invokes internal getOrElse but it accumulate invocations of orElse clause in the accumulator */ - def getOrElseCounting( key: String )( accumulator: Accumulator ) = cache.getOrElseUpdate[ String ]( key, new Callable[ String ] { - override def call( ): String = { - // increment miss counter - accumulator.incrementAndGet() - // return the value to store into the cache - "value" - } - } ) - } - - implicit def expected2matcher[T]( expected: => T ): Matcher[ T ] = Matchers.beEqualTo(expected) -} - -trait LowPriorityImplicits { - - implicit class AccumulatorCache[ Result[ _ ] ]( cache: AbstractCacheApi[ Result ] ) { - private type Accumulator = AtomicInteger - - /** invokes internal getOrElse but it accumulate invocations of orElse clause in the accumulator */ - def getOrElseCounting( key: String )( accumulator: Accumulator ) = cache.getOrElse( key ) { - // increment miss counter - accumulator.incrementAndGet() - // return the value to store into the cache - "value" - } - - /** invokes internal getOrElse but it accumulate invocations of orElse clause in the accumulator */ - def getOrFutureCounting( key: String )( accumulator: Accumulator ) = cache.getOrFuture[ String ]( key ) { - Future.successful { - // increment miss counter - accumulator.incrementAndGet() - // return the value to store into the cache - "value" - } - } - } - -} diff --git a/src/test/scala/play/api/cache/redis/util/ExpirationSpec.scala b/src/test/scala/play/api/cache/redis/util/ExpirationSpec.scala deleted file mode 100644 index 3f2c1886..00000000 --- a/src/test/scala/play/api/cache/redis/util/ExpirationSpec.scala +++ /dev/null @@ -1,58 +0,0 @@ -package play.api.cache.redis.util - -import java.util.Date - -import play.api.cache.redis.{CacheApi, Expiration, ExpirationImplicits, Redis} - -import org.joda.time.DateTime -import org.specs2.mutable.Specification - -/** - *This specification tests expiration conversion
- */ -class ExpirationSpec extends Specification with Redis with ExpirationImplicits { - - private val Cache = injector.instanceOf[ CacheApi ] - - private def nowInJoda = new DateTime( ) - - private def nowInJava = new Date( ) - - private def in2seconds = nowInJoda.plusSeconds( 2 ) - - private val prefix = "expiration" - - "Expiration" should { - - "properly compute positive duration for org.joda.time.DateTime" in { - val expiration = nowInJoda.plusSeconds( 5 ).asExpiration - expiration.toSeconds >= 4 must beTrue - expiration.toSeconds <= 6 must beTrue - } - - "properly compute positive duration for java.util.Date" in { - val expiration = new Date( nowInJava.getTime + 5 * 1000 ).asExpiration - expiration.toSeconds >= 4 must beTrue - expiration.toSeconds <= 6 must beTrue - } - - "properly compute negative duration" in { - val expiration = nowInJoda.minusSeconds( 5 ).asExpiration - expiration.toSeconds <= -4 must beTrue - expiration.toSeconds >= -6 must beTrue - } - - "hit after set" in { - Cache.set( s"$prefix-test-1", "value", in2seconds.asExpiration ) - Cache.get[ String ]( s"$prefix-test-1" ) must beSome( "value" ) - } - - "miss after expiration" in { - Cache.set( s"$prefix-test-2", "value", in2seconds.asExpiration ) - Cache.get[ String ]( s"$prefix-test-2" ) must beSome( "value" ) - // wait until the duration expires - Thread.sleep( 2500 ) - Cache.get[ String ]( s"$prefix-test-2" ) must beNone - } - } -}