diff --git a/README.md b/README.md index b9647b4..655dc77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # sbt-redis-plugin [![Build Status](https://travis-ci.org/fmonniot/sbt-redis-plugin.svg?branch=master)](https://travis-ci.org/fmonniot/sbt-redis-plugin) -_Coming Soon_ +Launch redis before your test and bring it down after. No more waiting for it on each test or test suite ! ## Usage @@ -9,14 +9,12 @@ Add this snippet to your `project/plugins.sbt` ```scala resolvers += Resolver.url("fmonniot", url("https://dl.bintray.com/fmonniot/sbt-plugins"))(Resolver.ivyStylePatterns) -addSbtPlugin("eu.monniot.redis" % "redis-plugin" % "0.1.1") +addSbtPlugin("eu.monniot.redis" % "redis-plugin" % "0.5.0") ``` You can now configure the plugin: ```scala -import redis.embedded.util.{Architecture, OS} - // [Optional] Configure a redis binary matrix redisBinaries := Seq( (("3.0.7", OS.UNIX, Architecture.x86_64), "/path/to/the/correct/redis-server") @@ -24,7 +22,7 @@ redisBinaries := Seq( // Launch a server and a cluster redisInstances := Seq( - RedisInstance("3.0.7", RedisInstance.SERVER, Seq(7894)), + RedisInstance("3.0.7", RedisInstance.SERVER, 7894), RedisInstance("3.0.7", RedisInstance.CLUSTER, Seq(7895, 7896, 7897)) ) ``` \ No newline at end of file diff --git a/build.sbt b/build.sbt index a41ca82..5d7def7 100644 --- a/build.sbt +++ b/build.sbt @@ -6,11 +6,11 @@ sbtPlugin := true scalaVersion in Global := "2.10.6" -version := "0.2.4" +version := "0.5.0" resolvers += Resolver.jcenterRepo -libraryDependencies += "eu.monniot.redis" % "embedded-redis" % "1.2.2" +libraryDependencies += "eu.monniot.redis" % "embedded-redis" % "1.4.0" // Scripted - sbt plugin tests scriptedSettings diff --git a/sample/build.sbt b/sample/build.sbt index d762e0e..0fa968e 100644 --- a/sample/build.sbt +++ b/sample/build.sbt @@ -1,5 +1,3 @@ -import redis.embedded.util.{Architecture, OS} - name := "sample-project" version := "1.0" diff --git a/sample/project/plugin.sbt b/sample/project/plugin.sbt index a1fd0fc..23d5bc7 100644 --- a/sample/project/plugin.sbt +++ b/sample/project/plugin.sbt @@ -1,3 +1,4 @@ +resolvers += Resolver.jcenterRepo resolvers += Resolver.url("fmonniot", url("https://dl.bintray.com/fmonniot/sbt-plugins"))(Resolver.ivyStylePatterns) -addSbtPlugin("eu.monniot.redis" % "redis-plugin" % "0.1.1") \ No newline at end of file +addSbtPlugin("eu.monniot.redis" % "redis-plugin" % "0.5.0") \ No newline at end of file diff --git a/src/main/scala/RedisKeys.scala b/src/main/scala/RedisKeys.scala deleted file mode 100644 index 15a9e18..0000000 --- a/src/main/scala/RedisKeys.scala +++ /dev/null @@ -1,18 +0,0 @@ -import redis.embedded.util.{Architecture, OS} -import sbt.{SettingKey, TaskKey, _} - - -object RedisKeys { - lazy val redisBinaries = SettingKey[Seq[((String, OS, Architecture), String)]]( - "redis-binaries", - "A list of redis path associated with a version, os and arch" - ) - - lazy val redisInstances = SettingKey[Seq[RedisInstance]]( - "redis-instances", - "A list of redis instances to start for tests" - ) - - lazy val startRedis = TaskKey[Unit]("start-redis", "Start the different redis as per the defined configuration") - lazy val stopRedis = TaskKey[Unit]("stop-redis", "Stop the different redis as per the defined configuration") -} diff --git a/src/main/scala/RedisPlugin.scala b/src/main/scala/RedisPlugin.scala index 28c2338..4afa10f 100644 --- a/src/main/scala/RedisPlugin.scala +++ b/src/main/scala/RedisPlugin.scala @@ -1,5 +1,4 @@ -import redis.embedded.RedisExecProvider -import redis.embedded.util.{Architecture, OS} +import eu.monniot.redis.plugin.{Architecture, OS, RedisKeys, RedisTestsListener} import sbt.Keys._ import sbt._ @@ -7,66 +6,27 @@ object RedisPlugin extends AutoPlugin { import RedisKeys._ - override def trigger = allRequirements + override def trigger: PluginTrigger = allRequirements override def requires = plugins.JvmPlugin - val autoImport = RedisKeys - - override lazy val projectSettings = defaultSettings + object autoImport extends RedisKeys { + val RedisInstance = eu.monniot.redis.plugin.RedisInstance + val OS = eu.monniot.redis.plugin.OS + val Architecture = eu.monniot.redis.plugin.Architecture + } - def defaultSettings: Seq[Setting[_]] = Seq( + override lazy val projectSettings = Seq( redisInstances := Seq.empty, redisBinaries := Seq( ("3.0.7", OS.MAC_OS_X, Architecture.x86_64) -> "redis-server-3.0.7-darwin", ("3.0.7", OS.UNIX, Architecture.x86_64) -> "redis-server-3.0.7" ), - startRedis := effectivelyStartRedis(redisBinaries.value, redisInstances.value, streams.value.log), - stopRedis := effectivelyStopRedis(streams.value.log), - - (test in Test) <<= { - val t = (test in Test).dependsOn(startRedis) - - t.andFinally(RedisUtils.stopRedisInstances()) - }, - (testOnly in Test) <<= { - val t = (testOnly in Test).dependsOn(startRedis) - - t.andFinally(RedisUtils.stopRedisInstances()) - } + testListeners += new RedisTestsListener( + streams.value.log, + redisBinaries.value, + redisInstances.value + ) ) - - def buildProvider(redisBinaries: Seq[((String, OS, Architecture), String)]) = { - redisBinaries - .map { case ((v, os, arch), path) => - (v, os, arch, path) - } - .groupBy(_._1) - .map { case (v, list) => - val provider = RedisExecProvider.defaultProvider() - - list.foreach { case (_, os, arch, path) => - provider.`override`(os, arch, path) - } - - (v, provider) - } - } - - def effectivelyStartRedis(redisBinaries: Seq[((String, OS, Architecture), String)], redis: Seq[RedisInstance], logger: Logger): Unit = { - val redisExecProviders = buildProvider(redisBinaries) - - logger.debug(s"Redis configuration: ${redisBinaries.toMap}") - logger.debug(s"Redis servers defined: $redis") - - RedisUtils.startRedisCluster(logger, redisExecProviders, redis.filter(m => m.isRedisCluster)) - RedisUtils.startRedisServer(logger, redisExecProviders, redis.filter(m => m.isRedisServer)) - } - - def effectivelyStopRedis(logger: Logger): Unit = { - logger.info("Stopping redis instances") - - RedisUtils.stopRedisInstances() - } } diff --git a/src/main/scala/RedisUtils.scala b/src/main/scala/RedisUtils.scala deleted file mode 100644 index 0938039..0000000 --- a/src/main/scala/RedisUtils.scala +++ /dev/null @@ -1,95 +0,0 @@ -import java.io.{BufferedReader, InputStreamReader} - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global -import redis.embedded.{Redis, RedisExecProvider} -import redis.embedded.cluster.RedisCluster -import sbt._ - -object RedisUtils { - - import redis.embedded.RedisServer - - private var redisServers: Seq[RedisServer] = null - - private var redisClusters: Seq[RedisCluster] = null - - def startRedisServer(logger: Logger, providers: Map[String, RedisExecProvider], redisList: Seq[RedisInstance]) = { - redisServers = redisList.map { config => - - val port = config.ports.next() - - ensureFileExecutable(providers(config.version).get(), logger) - - val redisServer = new RedisServer.Builder() - .redisExecProvider(providers(config.version)) - .port(port) - .build() - - startAndCaptureErrors(redisServer, logger) - - logger.info(s"Redis Server started on port $port") - - redisServer - } - } - - def startRedisCluster(logger: Logger, providers: Map[String, RedisExecProvider], redis: Seq[RedisInstance]) = { - redisClusters = redis.map { config => - - logger.info("Starting Redis Cluster") - - ensureFileExecutable(providers(config.version).get(), logger) - - val redisCluster = new RedisCluster.Builder() - .serverPorts(config.ports) - .numOfMasters(config.numOfMaster) - .withServerBuilder(new RedisServer.Builder().redisExecProvider(providers(config.version))) - .build() - - startAndCaptureErrors(redisCluster, logger) - - logger.info(s"Redis Cluster started on ports ${redisCluster.ports()}") - - redisCluster - } - } - - def stopRedisInstances(): Unit = { - // TODO Get access to a Logger here -// logger.debug("Stopping redis instances") - - if (redisServers != null) { - redisServers.foreach(_.stop()) - } - if (redisClusters != null) { - redisClusters.foreach(_.stop()) - } - } - - private def ensureFileExecutable(file: File, logger: Logger) = { - if (!file.canExecute) { - logger.debug(s"Making ${file.getAbsolutePath} executable.") - file.setExecutable(true, true) - } - } - - private def startAndCaptureErrors(redis: Redis, logger: Logger): Unit = { - val reader = new BufferedReader(new InputStreamReader(redis.errors())) - - try { - redis.start() - } catch { - case e: RuntimeException => - val ports = redis.ports() - val error = Stream.continually(reader.readLine()).takeWhile(_ != null).foldLeft(false) { case (err, line) => - if (line.contains("Address already in use")) { - logger.error(s"[${redis.getClass.getSimpleName}@$ports] $line") - true - } else false - } - - if (error) throw e - } - } -} diff --git a/src/main/scala/eu/monniot/redis/plugin/Architecture.scala b/src/main/scala/eu/monniot/redis/plugin/Architecture.scala new file mode 100644 index 0000000..a21976d --- /dev/null +++ b/src/main/scala/eu/monniot/redis/plugin/Architecture.scala @@ -0,0 +1,23 @@ +package eu.monniot.redis.plugin + +import redis.embedded.util.{Architecture => jArch} + +/** + * Correspond to the redis.embedded.util.Architecture enum, but scalaified to be automatically + * imported in a sbt config file + */ +sealed trait Architecture { + def toJava: jArch +} + +object Architecture { + + object x86 extends Architecture { + def toJava: jArch = jArch.x86 + } + + object x86_64 extends Architecture { + def toJava: jArch = jArch.x86_64 + } + +} diff --git a/src/main/scala/eu/monniot/redis/plugin/OS.scala b/src/main/scala/eu/monniot/redis/plugin/OS.scala new file mode 100644 index 0000000..d9d7c9f --- /dev/null +++ b/src/main/scala/eu/monniot/redis/plugin/OS.scala @@ -0,0 +1,27 @@ +package eu.monniot.redis.plugin + +import redis.embedded.util.{OS => jOS} + +/** + * Correspond to the redis.embedded.util.OS enum, but scalaified to be automatically + * imported in a sbt config file + */ +sealed trait OS { + def toJava: jOS +} + +object OS { + + object WINDOWS extends OS { + def toJava: jOS = jOS.WINDOWS + } + + object UNIX extends OS { + def toJava: jOS = jOS.UNIX + } + + object MAC_OS_X extends OS { + def toJava: jOS = jOS.MAC_OS_X + } + +} \ No newline at end of file diff --git a/src/main/scala/RedisInstance.scala b/src/main/scala/eu/monniot/redis/plugin/RedisInstance.scala similarity index 72% rename from src/main/scala/RedisInstance.scala rename to src/main/scala/eu/monniot/redis/plugin/RedisInstance.scala index ef0d19a..624a8ea 100644 --- a/src/main/scala/RedisInstance.scala +++ b/src/main/scala/eu/monniot/redis/plugin/RedisInstance.scala @@ -1,3 +1,5 @@ +package eu.monniot.redis.plugin + import java.util import redis.embedded.PortProvider @@ -8,6 +10,9 @@ object RedisInstance { def apply(version: String, kind: String, ports: Seq[Int]) = new RedisInstance(version, kind, new PredefinedPortProvider(scalaToJava(ports))) + def apply(version: String, kind: String, port: Int) = + new RedisInstance(version, kind, new PredefinedPortProvider(scalaToJava(Seq(port)))) + private def scalaToJava(list: Seq[Int]): util.List[Integer] = { seqAsJavaList(list).asInstanceOf[util.List[Integer]] } @@ -23,7 +28,7 @@ case class RedisInstance(version: String, import RedisInstance._ - def isRedisCluster = kind == CLUSTER + def isRedisCluster: Boolean = kind == CLUSTER - def isRedisServer = kind == SERVER + def isRedisServer: Boolean = kind == SERVER } \ No newline at end of file diff --git a/src/main/scala/eu/monniot/redis/plugin/RedisKeys.scala b/src/main/scala/eu/monniot/redis/plugin/RedisKeys.scala new file mode 100644 index 0000000..39eb38f --- /dev/null +++ b/src/main/scala/eu/monniot/redis/plugin/RedisKeys.scala @@ -0,0 +1,20 @@ +package eu.monniot.redis.plugin + +import sbt.SettingKey + +/** + * Created by francois on 27/01/17. + */ +trait RedisKeys { + lazy val redisBinaries = SettingKey[Seq[((String, OS, Architecture), String)]]( + "redis-binaries", + "A list of redis path associated with a version, os and arch" + ) + + lazy val redisInstances = SettingKey[Seq[RedisInstance]]( + "redis-instances", + "A list of redis instances to start for tests" + ) +} + +object RedisKeys extends RedisKeys \ No newline at end of file diff --git a/src/main/scala/eu/monniot/redis/plugin/RedisTestsListener.scala b/src/main/scala/eu/monniot/redis/plugin/RedisTestsListener.scala new file mode 100644 index 0000000..210cab8 --- /dev/null +++ b/src/main/scala/eu/monniot/redis/plugin/RedisTestsListener.scala @@ -0,0 +1,138 @@ +package eu.monniot.redis.plugin + +import java.io.{BufferedReader, InputStreamReader} + +import redis.embedded.{Redis, RedisExecProvider, RedisServer} +import redis.embedded.cluster.RedisCluster +import sbt.{File, Logger, TestEvent, TestResult, TestsListener} + + +class RedisTestsListener(logger: Logger, + binaries: Seq[((String, OS, Architecture), String)], + instances: Seq[RedisInstance]) extends TestsListener { + + private var redisServers: Seq[RedisServer] = _ + + private var redisClusters: Seq[RedisCluster] = _ + + override def doInit(): Unit = { + val redisExecProviders = buildProvider(binaries) + + logger.debug(s"Redis configuration: ${binaries.toMap}") + logger.debug(s"Redis servers defined: $instances") + + startRedisCluster(logger, redisExecProviders, instances.filter(m => m.isRedisCluster)) + startRedisServer(logger, redisExecProviders, instances.filter(m => m.isRedisServer)) + } + + override def doComplete(finalResult: TestResult.Value): Unit = { + logger.info("Stopping redis instances") + + if (redisServers != null) { + redisServers.foreach(_.stop()) + } + + if (redisClusters != null) { + redisClusters.foreach(_.stop()) + } + } + + private def buildProvider(redisBinaries: Seq[((String, OS, Architecture), String)]) = { + redisBinaries + .map { case ((v, os, arch), path) => + (v, os, arch, path) + } + .groupBy(_._1) + .map { case (v, list) => + val provider = RedisExecProvider.build() + + list.foreach { case (_, os, arch, path) => + provider.`override`(os.toJava, arch.toJava, path) + } + + (v, provider) + } + } + + private def startRedisServer(logger: Logger, providers: Map[String, RedisExecProvider], redisList: Seq[RedisInstance]) = { + redisServers = redisList.map { config => + + val port = config.ports.copy().next() + + ensureFileExecutable(providers(config.version).get(), logger) + + val redisServer = new RedisServer.Builder() + .redisExecProvider(providers(config.version)) + .port(port) + .build() + + startAndCaptureErrors(redisServer, logger) + + logger.info(s"Redis Server started on port $port") + + redisServer + } + } + + private def startRedisCluster(logger: Logger, providers: Map[String, RedisExecProvider], redis: Seq[RedisInstance]) = { + redisClusters = redis.map { config => + + logger.info("Starting Redis Cluster") + + ensureFileExecutable(providers(config.version).get(), logger) + + val redisCluster = new RedisCluster.Builder() + .serverPorts(config.ports.copy()) + .numOfMasters(config.numOfMaster) + .withServerBuilder( + new RedisServer.Builder() + .setting("bind 127.0.0.1") + .redisExecProvider(providers(config.version)) + ) + .build() + + startAndCaptureErrors(redisCluster, logger) + + logger.info(s"Redis Cluster started on ports ${redisCluster.ports()}") + + redisCluster + } + } + + private def ensureFileExecutable(file: File, logger: Logger) = { + if (!file.canExecute) { + logger.debug(s"Making ${file.getAbsolutePath} executable.") + file.setExecutable(true, true) + } + } + + private def startAndCaptureErrors(redis: Redis, logger: Logger): Unit = { + val reader = new BufferedReader(new InputStreamReader(redis.errors())) + + try { + redis.start() + } catch { + case e: RuntimeException => + val ports = redis.ports() + val error = Stream.continually(reader.readLine()).takeWhile(_ != null).foldLeft(false) { case (_, line) => + if (line.contains("Address already in use")) { + logger.error(s"[${redis.getClass.getSimpleName}@$ports] $line") + true + } else false + } + + if (error) throw e + } + } + + + // TestReportListener interface, not used but necessary + + override def startGroup(name: String): Unit = {} + + override def testEvent(event: TestEvent): Unit = {} + + override def endGroup(name: String, t: Throwable): Unit = {} + + override def endGroup(name: String, result: TestResult.Value): Unit = {} +}