From 1f1ab55221dd61ca72ce3f11daf5f937dc39c7ec Mon Sep 17 00:00:00 2001 From: Karel Cemus Date: Fri, 10 Nov 2023 01:14:48 +0100 Subject: [PATCH 1/2] Update of release process --- .github/workflows/build-test.yml | 45 ++++++++++++++ .github/workflows/publish.yml | 45 ++++++++++++++ .travis.yml | 67 --------------------- build.sbt | 38 +++--------- project/CustomReleasePlugin.scala | 98 ++++++++++++++++--------------- project/DocumentationUpdate.scala | 89 ++++++++++++++++++---------- project/ReleaseUtilities.scala | 51 ++++++++++++++++ project/Utilities.scala | 46 --------------- project/build.properties | 2 +- project/plugins.sbt | 20 +++---- project/sonatype.sbt | 9 --- version.sbt | 1 - 12 files changed, 270 insertions(+), 241 deletions(-) create mode 100644 .github/workflows/build-test.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .travis.yml create mode 100644 project/ReleaseUtilities.scala delete mode 100644 project/Utilities.scala delete mode 100644 project/sonatype.sbt delete mode 100644 version.sbt diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml new file mode 100644 index 00000000..f94f501e --- /dev/null +++ b/.github/workflows/build-test.yml @@ -0,0 +1,45 @@ +name: Build and Test +on: + pull_request: + + push: + branches: + - master + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-22.04 + timeout-minutes: 15 + env: + SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + + - name: Cache Coursier + uses: coursier/cache-action@v6.4 + + - name: Setup JDK + uses: coursier/setup-action@v1.3.4 + with: + jvm: adoptium:1.8 + + - name: Test Code Style + run: | + sbt --client "+scalariformFormat; +test:scalariformFormat" + git diff --exit-code || ( + echo "ERROR: Scalariform check failed, see differences above." + echo "To fix, format your sources using sbt scalariformFormat test:scalariformFormat before submitting a pull request." + false + ) + + - name: Build + timeout-minutes: 10 + run: | + sbt --client "+clean; +compile; +Test/compile; +test;" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..0875ff16 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish +on: + push: + tags: ["*"] + +concurrency: + group: "${{ github.workflow }}-${{ github.ref }}" + cancel-in-progress: true + +jobs: + build-and-test: + runs-on: ubuntu-22.04 + timeout-minutes: 15 + environment: "Generally Available" + env: + SBT_OPTS: -Dfile.encoding=UTF-8 -Duser.timezone=UTC + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.ref_name }} + + - name: Cache Coursier + uses: coursier/cache-action@v6.4 + + - name: Setup JDK + uses: coursier/setup-action@v1.3.4 + with: + jvm: adoptium:1.17 + + - name: Import GPG Key + timeout-minutes: 10 + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + run: | + echo $PGP_SECRET | base64 --decode | gpg --batch --import + + - name: Test and Publish JARs + timeout-minutes: 10 + env: + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + run: | + sbt --client "+test; +publishSigned; sonatypeBundleRelease;" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 9156c2db..00000000 --- a/.travis.yml +++ /dev/null @@ -1,67 +0,0 @@ -language: scala - -dist: bionic - -branches: - only: - - master - -cache: - directories: - - "$HOME/.coursier" - - "$HOME/.ivy2/cache" - - "$HOME/.ivy2/local" - - "$HOME/.jabba/jdk" - - "$HOME/.sbt" - -env: - global: - # Sonatype credentials - - secure: ozghyo5Yp3enkBx53MzQaD0LeqhBNbGFC3vkkZAcQbele4tR5GrOzR4xjTH0oHhVj7NUHq7WGu4nUDj1h43fJW7uCViepUARrBMPLaUc8ba036IX1OXTU7e2ilGKnOsFeOGfgOo9tb5HnisgfhSXAfMBCgigc+oCtL0HTBdxn9CqjqsUo23c/fnG2UKyLk0Hv5vp91Z/3+kMNkxQTMdJs+ywpKRbMHdIwkghJIqSVfezGxmqhi1wSfoh0xMjOQjaHC0NpTWUs+gmD0hENzV/THwtkeLNcIsdkMIJPoPKruOlfGeTnCFHF3ojNgrUEEDwulrwpLTLoQJeDU6P60JSwUdtzu9hSxwTZLgndwmaE77r2cVsiD5xM5AGB4jVQbd9oOFMW7TIErUemg53qFAiix1W5KtgGv6vW9qT7Z/CRpYC2LG99BUnNzdTKu9e2oBsCY1QbYrXHORSqefnzihAlwJ/6lAGDLnAFw4pgYb779oo8GIhQQdCDMqELSunhP8uJ6G3dpY5p5ubrsSA7yS6tRcaQ+iB1410iTNfMAoD0tKIjgbpR4Zs8lg/5bpiO914f3Xf0KKwFM7lXdajMdN5z5iDnOGgAgtwXB1a/QT2phHqKQ/DFLUbqVUdE2tmuycJ9iAUtVVsyOXZggtOun6o04hfBG1NK/tc1ifGUPigjXw= - - secure: b1gPGCnIdQPqvztIFzM1JkVqYhgDSx5KC273sg9/I2f9zAP+YqPgJ9WjdUBj2Wp2pzQdggakdBgAj36cpEhpylr4W2t4aMzHitvmu/BvHW9+vpjzMhqpLP/6GG8N+Szbhm3nj4BDA/BjelTnN2SyvKrxWGrD6QJuU33AakWM3v/b/Og5L2Sb2E63X80i2iZk9qTdOHCoTxizkTtB/DEgt9PpzUfMJmn8Ww1Gs1mbou3fPUPOVO88iBjn5JmF6iJ+sIRY0MQuPHhDnk98z6BvITg8GycQBc27OWzJFxK0Zu78MVV3t8WRjZ0sxyJNwdGSBBdz5L1jEzP5J5GtgRZiCcCOVZ4j04kdd2/2/4RaZX2jdZAyr1uzIEuBY+xwKhg6J0386HGrQVUu+IvgPQbpFOtmDeyIBRRAXW11KXR2+cNH6RFF8wA274G8l3SAfvvpWKUwussj9j3h1T/37xdCL2igaOmbOEIQYL1Mpo5cnN8lI080LoCc5vL9XHqCVlE7Hr+VUhoagmuiMMUzrTW0Rg81fI+I8dswyy6QTaqY8EYg8KdxQhDoFnkRtHAdaJyjWrL7IuM5G1Poto9UkW8Gg4YwK5HHeg9xDaLMD/OBnup2DVIqwsi2walsM6OlLB0WkEaDa2VG8WVKFOrTOLTAmfWFCscg+qutgTrfasF12EM= - # set version of JDK - - TRAVIS_JDK_MIN=1.8.0-0 - - TRAVIS_JDK_MAX=1.9.0-0 - - SCALA_VERSION=2.13.0 - - COURSIER_CACHE=$HOME/.coursier - -jobs: - include: - - name: "Test Scala 2.12" - env: - - SCALA_VERSION=2.12.10 - - SCRIPT=test - - name: "Test Scala 2.13" - env: - - SCALA_VERSION=2.13.0 - - SCRIPT=test - - name: "Test Scala Code Style" - env: - - SCRIPT=test-code-style - -script: - - bin/$SCRIPT $SCALA_VERSION - -services: - - redis-server - - docker - -before_install: - - curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh - -install: - - TRAVIS_JDK=`jabba ls-remote "adopt@>=$TRAVIS_JDK_MIN < $TRAVIS_JDK_MAX" --latest=minor` - - jabba install "$TRAVIS_JDK" && jabba use "$TRAVIS_JDK" && java -Xmx32m -version - - javac -version - - docker pull karelcemus/redis-cluster:latest - - | - docker run -d -e "SENTINEL=true" -e "IP=0.0.0.0" \ - -p 5000:5000 -p 5001:5001 -p 5002:5002 \ - -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 \ - -p 7006:7006 -p 7007:7007 -p 7008:7008 \ - --name redis-cluster \ - karelcemus/redis-cluster:latest - -after_script: - - docker stop redis-cluster - - docker rm redis-cluster diff --git a/build.sbt b/build.sbt index ca475ff2..3f67bd71 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,3 @@ -import com.jsuereth.sbtpgp.PgpKeys import sbt.Keys._ import sbt._ @@ -12,23 +11,17 @@ organization := "com.github.karelcemus" scalaVersion := "2.13.8" -crossScalaVersions := Seq( "2.12.15", scalaVersion.value ) +crossScalaVersions := Seq("2.12.15", scalaVersion.value) playVersion := "2.8.13" -connectorVersion := "1.9.1" - -specs2Version := "4.13.2" - libraryDependencies ++= Seq( // play framework cache API "com.typesafe.play" %% "play-cache" % playVersion.value % Provided, // redis connector - "com.github.karelcemus" %% "rediscala" % connectorVersion.value, - // test framework - "org.specs2" %% "specs2-core" % specs2Version.value % Test, - // with mockito extension - "org.specs2" %% "specs2-mock" % specs2Version.value % Test, + "com.github.karelcemus" %% "rediscala" % "1.9.1", + // test framework with mockito extension + "org.specs2" %% "specs2-mock" % "4.13.2" % Test, // test module for play framework "com.typesafe.play" %% "play-specs2" % playVersion.value % Test ) @@ -37,28 +30,15 @@ resolvers ++= Seq( "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/" ) -javacOptions ++= Seq( "-source", "1.8", "-target", "1.8", "-Xlint:unchecked", "-encoding", "UTF-8" ) - -scalacOptions ++= Seq( "-deprecation", "-feature", "-unchecked" ) - -homepage := Some( url( "https://github.com/karelcemus/play-redis" ) ) - -licenses := Seq( "Apache 2" -> url( "https://www.apache.org/licenses/LICENSE-2.0" ) ) +javacOptions ++= Seq("-Xlint:unchecked", "-encoding", "UTF-8") -vcsScm := "git@github.com:KarelCemus/play-redis.git" +scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked") -authors := Seq( "Karel Čemus" ) +homepage := Some(url("https://github.com/karelcemus/play-redis")) -// Release plugin settings -releaseCrossBuild := true -releaseTagName := ( ThisBuild / version ).value -releasePublishArtifactsAction := PgpKeys.publishSigned.value +licenses := Seq("Apache 2" -> url("https://www.apache.org/licenses/LICENSE-2.0")) -// Publish settings -publishTo := { - if (isSnapshot.value) Some(Opts.resolver.sonatypeSnapshots) - else Some( Opts.resolver.sonatypeStaging ) -} +enablePlugins(CustomReleasePlugin) // exclude from tests coverage coverageExcludedFiles := ".*exceptions.*" diff --git a/project/CustomReleasePlugin.scala b/project/CustomReleasePlugin.scala index aed5a2da..0c077d08 100644 --- a/project/CustomReleasePlugin.scala +++ b/project/CustomReleasePlugin.scala @@ -1,65 +1,71 @@ -import sbt.Keys._ -import sbt._ -import sbtrelease._ +import com.github.sbt.git.{GitVersioning, SbtGit} +import sbt.* +import sbt.Keys.* +import sbtrelease.* +import xerial.sbt.Sonatype object CustomReleasePlugin extends AutoPlugin { - import ReleasePlugin.autoImport._ - import ReleaseStateTransformations._ + import ReleasePlugin.autoImport.* + import ReleaseStateTransformations.* + import ReleaseUtilities.* + import Sonatype.autoImport.* object autoImport { - - val playVersion = settingKey[ String ]( "Version of Play framework" ) - val connectorVersion = settingKey[ String ]( "Version redis connector" ) - val specs2Version = settingKey[ String ]( "Version of specs2 testing framework" ) - - val authors = settingKey[ Seq[ String ] ]( "List of authors of the library" ) - val vcsScm = settingKey[ String ]( "URL of the GIT repository" ) + val playVersion = settingKey[String]("Version of Play framework") } override def trigger = allRequirements - override def requires = ReleasePlugin + override def requires: Plugins = ReleasePlugin && GitVersioning && Sonatype - private def customizedReleaseProcess = { - Seq[ ReleaseStep ]( + private def customizedReleaseProcess: Seq[ReleaseStep] = { + Seq[ReleaseStep]( checkSnapshotDependencies, inquireVersions, - runClean, - runTest, - DocumentationUpdate.bumpVersionInDoc, - DocumentationUpdate.bumpLatestVersionInReadme, - setReleaseVersion, - commitReleaseVersion, - tagRelease, - publishArtifacts, - setNextVersion, - commitNextVersion, - pushChanges + DocumentationUpdate.updateDocumentation, ) } - import autoImport._ - - override def projectSettings = Seq[ Setting[ _ ] ]( + override def projectSettings = Seq[Setting[_]]( publishMavenStyle := true, - pomIncludeRepository := { _ => false}, - pomExtra := { - - {vcsScm.value} - scm:{vcsScm.value} - - - { - authors.value.map { author => - - {author} - - } - } - - }, + pomIncludeRepository := { _ => false }, // customized release process - releaseProcess := customizedReleaseProcess + releaseProcess := customizedReleaseProcess, + // + scmInfo := Some( + ScmInfo( + url("https://github.com/KarelCemus/play-i18n.git"), + "scm:git@github.com:KarelCemus/play-i18n.git" + ) + ), + developers := List( + Developer(id = "karel.cemus", name = "Karel Cemus", email = "", url = url("https://github.com/KarelCemus/")) + ), + // Publish settings + publishTo := sonatypePublishToBundle.value, + // git tags without "v" prefix + SbtGit.git.gitTagToVersionNumber := { tag: String => + if (tag matches "[0-9]+\\..*") Some(tag) + else None + } ) + + private lazy val inquireVersions: ReleaseStep = { implicit st: State => + val extracted = Project.extract(st) + + val useDefs = false + val currentV = vcs.latest + val nextVersion = st.extracted.runTask(releaseVersion, st)._2(currentV) + val bump = Version.Bump.Minor + + val suggestedReleaseV: String = Version(nextVersion).map(_.bump(bump).string).getOrElse(versionFormatError(currentV)) + + st.log.info("Press enter to use the default value") + + //flatten the Option[Option[String]] as the get returns an Option, and the value inside is an Option + val releaseV = readVersion(suggestedReleaseV, "Release version [%s] : ", useDefs, st.get(ReleaseKeys.commandLineReleaseVersion).flatten) + + st.put(ReleaseKeys.versions, (releaseV, releaseV)) + } } diff --git a/project/DocumentationUpdate.scala b/project/DocumentationUpdate.scala index f67fde08..85f2b572 100644 --- a/project/DocumentationUpdate.scala +++ b/project/DocumentationUpdate.scala @@ -7,69 +7,96 @@ object DocumentationUpdate { import CustomReleasePlugin.autoImport._ import ReleasePlugin.autoImport._ import ReleaseKeys._ - import Utilities._ + import ReleaseUtilities._ /** version to be released */ - private def next( implicit st: State ) = st.get( versions ).get._1 - - /** latest version extracted from git tag on the current branch */ - private def latest( implicit st: State ) = vcs.cmd( "describe", "--abbrev=0", "--tags" ).!!.trim + private def next(implicit st: State) = st.get(versions).get._1 /** directory with documentation */ - private def documentationDirectory( implicit st: State ) = st.extracted.get( baseDirectory ) / "doc" + private def documentationDirectory(implicit st: State) = st.extracted.get(baseDirectory) / "doc" /** all documentation files */ - private def documentationSources( implicit st: State ) = { - documentationDirectory ** ( -DirectoryFilter && "*.md" ) + private def documentationSources(implicit st: State) = { + documentationDirectory ** (-DirectoryFilter && "*.md") }.get /** readme file */ - private def readme( implicit st: State ) = st.extracted.get( baseDirectory ) / "README.md" + private def readme(implicit st: State) = st.extracted.get(baseDirectory) / "README.md" /** groupId used for construction of SBT dependency statement */ - private def groupId( implicit st: State ) = st.extracted.get( organization ) + private def groupId(implicit st: State) = st.extracted.get(organization) /** artifactId used for construction of SBT dependency statement */ - private def artifactId( implicit st: State ) = st.extracted.get( normalizedName ) + private def artifactId(implicit st: State) = st.extracted.get(normalizedName) /** SBT dependency definition */ - private def sbtDependency( version: String )( implicit st: State ) = { + private def sbtDependency(version: String)(implicit st: State) = { s""" "$groupId" %% "$artifactId" % "$version" """.trim } /** Major and minor version of Play framework */ - private def playMinorVersion( implicit st: State ) = { - val version = st.extracted.get( playVersion ) - version.take( version.lastIndexOf( "." ) ) + private def playMinorVersion(implicit st: State) = { + val version = st.extracted.get(playVersion) + version.take(version.lastIndexOf(".")) } private object version { - def placeholder( implicit st: State ) = s".*" - def replacement( version: String )( implicit st: State ) = s"$version" + def placeholder(implicit st: State) = s".*" + def replacement(version: String)(implicit st: State) = s"$version" } - def bumpVersionInDoc: ReleaseStep = ReleaseStep( { implicit st: State => + def updateDocumentation: ReleaseStep = ReleaseStep({ implicit st: State => + val commitMessage = s"Documentation updated to version $next" + val program = List[State => State]( + bumpVersionInDoc(_), + bumpLatestVersionInReadme(_), + commitVersion(commitMessage)(_) + ).reduce(_ andThen _) + + program(st) + }) + + private def bumpVersionInDoc(implicit st: State): State = { + val latest = vcs.latest // update versions in documentation - ( readme +: documentationSources ).transform { _ - .replace( s"blob/$latest", s"blob/$next" ) - .replaceAll( sbtDependency( latest ), sbtDependency( next ) ) + (readme +: documentationSources).transform { + _ + .replace(s"blob/$latest", s"blob/$next") + .replaceAll(sbtDependency(latest), sbtDependency(next)) } // stage the changes - vcs.stage( readme +: documentationSources: _* ) - + vcs.stage(readme +: documentationSources: _*) st - } ) + } /** Update the line with SBT dependency definition */ - def bumpLatestVersionInReadme: ReleaseStep = ReleaseStep( { implicit st: State => + private def bumpLatestVersionInReadme(implicit st: State): State = { // update the README file - readme.transform { _ - .replaceAll( sbtDependency( latest ), sbtDependency( next ) ) - .replaceAll( version.placeholder, version.replacement( next ) ) + readme.transform { + _ + .replaceAll(sbtDependency(vcs.latest), sbtDependency(next)) + .replaceAll(version.placeholder, version.replacement(next)) } // stage the changes - vcs.stage( readme ) - + vcs.stage(readme) st - } ) + } + + private def commitVersion(commitMessage: String)(implicit st: State): State = { + val log = processLogger + val base = vcs(st).baseDir.getCanonicalFile + val sign = st.extracted.get(releaseVcsSign) + val signOff = st.extracted.get(releaseVcsSignOff) + + val status = vcs(st).status.!!.trim + + val newState = if (status.nonEmpty) { + vcs(st).commit(commitMessage, sign, signOff) ! log + st + } else { + // nothing to commit + st + } + newState + } } diff --git a/project/ReleaseUtilities.scala b/project/ReleaseUtilities.scala new file mode 100644 index 00000000..f5e66740 --- /dev/null +++ b/project/ReleaseUtilities.scala @@ -0,0 +1,51 @@ +import scala.sys.process.ProcessLogger + +import sbt._ +import sbtrelease._ + +object ReleaseUtilities { + + import ReleasePlugin.autoImport._ + + implicit class StateExtraction(val st: State) extends AnyVal { + def extracted = Project.extract(st) + } + + def vcs(implicit st: State): Vcs = { + Project.extract(st).get(releaseVcs).getOrElse { + sys.error("Aborting release. Working directory is not a repository of a recognized VCS.") + } + } + + def processLogger(implicit st: State): ProcessLogger = new ProcessLogger { + override def err(s: => String): Unit = st.log.info(s) + override def out(s: => String): Unit = st.log.info(s) + override def buffer[T](f: => T): T = st.log.buffer(f) + } + + implicit class ExecutableVcs(val vcs: Vcs) extends AnyVal { + def stage(files: File*)(implicit st: State): Unit = vcs.add(files.getAbsolutePaths: _*) !! processLogger + } + + /** + * Helper class implementing a transform operation over a file. + * The file is opened, transformed, and saved. + */ + implicit class FilesUpdater(val files: Seq[File]) extends AnyVal { + + def transform(transform: String => String): Unit = { + files.foreach { + file => IO.write(file, transform(IO.read(file))) + } + } + + def getAbsolutePaths: Seq[String] = files.map(_.getAbsolutePath) + } + + implicit def file2updater(file: File): FilesUpdater = new FilesUpdater(Seq(file)) + + implicit class RichVcs(private val thiz: Vcs) extends AnyVal { + /** latest version extracted from git tag on the current branch */ + def latest(implicit st: State) = vcs.cmd("describe", "--abbrev=0", "--tags").!!.trim + } +} diff --git a/project/Utilities.scala b/project/Utilities.scala deleted file mode 100644 index d87beba4..00000000 --- a/project/Utilities.scala +++ /dev/null @@ -1,46 +0,0 @@ -import scala.sys.process.ProcessLogger - -import sbt._ -import sbtrelease._ - -object Utilities { - - import ReleasePlugin.autoImport._ - - implicit class StateExtraction( val st: State ) extends AnyVal { - def extracted = Project.extract( st ) - } - - def vcs( implicit st: State ): Vcs = { - Project.extract( st ).get( releaseVcs ).getOrElse { - sys.error( "Aborting release. Working directory is not a repository of a recognized VCS." ) - } - } - - private def processLogger( implicit st: State ): ProcessLogger = new ProcessLogger { - override def err( s: => String ): Unit = st.log.info( s ) - override def out( s: => String ): Unit = st.log.info( s ) - override def buffer[ T ]( f: => T ): T = st.log.buffer( f ) - } - - implicit class ExecutableVcs( val vcs: Vcs ) extends AnyVal { - def stage( files: File* )( implicit st: State ): Unit = vcs.add( files.getAbsolutePaths: _* ) !! processLogger - } - - /** - * Helper class implementing a transform operation over a file. - * The file is opened, transformed, and saved. - */ - implicit class FilesUpdater( val files: Seq[ File ] ) extends AnyVal { - - def transform( transform: String => String ): Unit = { - files.foreach { - file => IO.write( file, transform( IO.read( file ) ) ) - } - } - - def getAbsolutePaths: Seq[ String ] = files.map( _.getAbsolutePath ) - } - - implicit def file2updater( file: File ): FilesUpdater = new FilesUpdater( Seq( file ) ) -} diff --git a/project/build.properties b/project/build.properties index c8fcab54..e8a1e246 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.6.2 +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index 303172d2..0ebf6af2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,19 +1,17 @@ -resolvers += Resolver.url( "scoverage-bintray", url( "https://dl.bintray.com/sksamuel/sbt-plugins/" ) )( Resolver.ivyStylePatterns ) - -// library release -addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") - -// PGP signature -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") +resolvers += Resolver.url("scoverage-bintray", url("https://dl.bintray.com/sksamuel/sbt-plugins/"))(Resolver.ivyStylePatterns) // checks for updates -addSbtPlugin( "com.timushev.sbt" % "sbt-updates" % "0.6.1" ) +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.1") -// code coverage +// code coverage and uploader of the coverage results into the coveralls.io addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2") - -// uploads the coverage results into the coveralls.io addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") // code linter addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.3") + +// library release +addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") +addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") diff --git a/project/sonatype.sbt b/project/sonatype.sbt deleted file mode 100644 index 44fa4a25..00000000 --- a/project/sonatype.sbt +++ /dev/null @@ -1,9 +0,0 @@ -credentials ++= ( - for { - user <- Option(System.getenv() get "SONATYPE_USERNAME") - pass <- Option(System.getenv() get "SONATYPE_PASSWORD") - } yield Seq( - Credentials( "Sonatype Nexus Repository Manager", "oss.sonatype.org", user, pass ), - Credentials( "Nexus Repository Manager", "oss.sonatype.org", user, pass ) - ) -).getOrElse( Seq() ) diff --git a/version.sbt b/version.sbt deleted file mode 100644 index ce5683f0..00000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -ThisBuild / version := "2.7.1-SNAPSHOT" From a93d6c3f5bcf89e9e975b5d2c8f2502a613bd480 Mon Sep 17 00:00:00 2001 From: Karel Cemus Date: Fri, 10 Nov 2023 18:33:13 +0100 Subject: [PATCH 2/2] Update to Play 2.9, fixed integration tests --- .github/workflows/build-test.yml | 15 ++--- .scalariform.conf | 30 ---------- bin/config | 23 -------- bin/test | 13 ----- bin/test-code-style | 12 ---- build.sbt | 18 +++--- project/CustomReleasePlugin.scala | 4 +- project/plugins.sbt | 9 +-- .../cache/redis/JavaCompatibilityBase.scala | 8 --- .../cache/redis/JavaCompatibilityBase.scala | 0 .../cache/redis/configuration/RedisHost.scala | 49 +++++++++------- .../redis/configuration/RedisInstance.scala | 32 +++++----- .../cache/redis/connector/RedisCommands.scala | 10 ++-- .../api/cache/redis/impl/RedisCaches.scala | 21 ++++--- .../cache/redis/impl/RedisListJavaImpl.scala | 4 +- .../cache/redis/impl/RedisSetJavaImpl.scala | 16 ++--- .../ScalaSpecificSerializerSpec.scala | 58 ------------------- .../ScalaSpecificSerializerSpec.scala | 58 ------------------- .../cache/redis/ClusterRedisContainer.scala | 20 +++++++ .../play/api/cache/redis/ExpirationSpec.scala | 13 ++--- .../api/cache/redis/ForAllTestContainer.scala | 19 ++++++ .../play/api/cache/redis/Implicits.scala | 21 ++++--- .../api/cache/redis/RecoveryPolicySpec.scala | 5 +- .../redis/RedisCacheComponentsSpec.scala | 19 +++--- .../cache/redis/RedisCacheModuleSpec.scala | 23 ++++---- .../play/api/cache/redis/RedisContainer.scala | 19 ++++++ .../cache/redis/RedisContainerConfig.scala | 7 +++ .../redis/StandaloneRedisContainer.scala | 11 ++++ .../redis/configuration/RedisHostSpec.scala | 6 +- .../RedisInstanceManagerTest.scala | 3 +- .../RedisInstanceProviderSpec.scala | 2 +- .../redis/connector/RedisClusterSpec.scala | 34 ++++++++--- .../connector/RedisConnectorFailureSpec.scala | 2 +- .../redis/connector/RedisConnectorSpec.scala | 54 +++++++++-------- .../redis/connector/RedisSentinelSpec.scala | 12 ++-- .../redis/connector/SerializerSpec.scala | 27 +-------- .../api/cache/redis/impl/AsyncRedisSpec.scala | 2 +- .../api/cache/redis/impl/BuildersSpec.scala | 2 +- .../redis/impl/RedisCacheImplicits.scala | 4 +- .../api/cache/redis/impl/RedisCacheSpec.scala | 4 +- .../cache/redis/impl/RedisRuntimeSpec.scala | 2 +- 41 files changed, 284 insertions(+), 407 deletions(-) delete mode 100644 .scalariform.conf delete mode 100644 bin/config delete mode 100755 bin/test delete mode 100755 bin/test-code-style delete mode 100644 src/main/scala-2.12/play/api/cache/redis/JavaCompatibilityBase.scala rename src/main/{scala-2.13 => scala}/play/api/cache/redis/JavaCompatibilityBase.scala (100%) delete mode 100644 src/test/scala-2.11/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala delete mode 100644 src/test/scala-2.12/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala create mode 100644 src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala create mode 100644 src/test/scala/play/api/cache/redis/ForAllTestContainer.scala create mode 100644 src/test/scala/play/api/cache/redis/RedisContainer.scala create mode 100644 src/test/scala/play/api/cache/redis/RedisContainerConfig.scala create mode 100644 src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index f94f501e..047ffdd9 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,18 +28,11 @@ jobs: - name: Setup JDK uses: coursier/setup-action@v1.3.4 with: - jvm: adoptium:1.8 - - - name: Test Code Style - run: | - sbt --client "+scalariformFormat; +test:scalariformFormat" - git diff --exit-code || ( - echo "ERROR: Scalariform check failed, see differences above." - echo "To fix, format your sources using sbt scalariformFormat test:scalariformFormat before submitting a pull request." - false - ) + jvm: adoptium:1.17 - name: Build timeout-minutes: 10 + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - sbt --client "+clean; +compile; +Test/compile; +test;" + sbt --client "+clean; +compile; +Test/compile; +coverage; +test; +coverageReport; +coveralls;" diff --git a/.scalariform.conf b/.scalariform.conf deleted file mode 100644 index adbb97d4..00000000 --- a/.scalariform.conf +++ /dev/null @@ -1,30 +0,0 @@ -#alignArguments=false -#alignParameters=false -alignSingleLineCaseStatements=true -#alignSingleLineCaseStatements.maxArrowIndent=40 -#allowParamGroupsOnNewlines=false -#compactControlReadability=false -#compactStringConcatenation=false -danglingCloseParenthesis=Force -#doubleIndentClassDeclaration=false -#doubleIndentConstructorArguments=false -doubleIndentMethodDeclaration=true -#firstArgumentOnNewline=Force -#firstParameterOnNewline=Force -#formatXml=true -#indentLocalDefs=false -#indentPackageBlocks=true -#indentSpaces=2 -#indentWithTabs=false -#multilineScaladocCommentsStartOnFirstLine=false -newlineAtEndOfFile=true -placeScaladocAsterisksBeneathSecondAsterisk=true -#preserveSpaceBeforeArguments=false -#rewriteArrowSymbols=false -#singleCasePatternOnNewline=true -#spaceBeforeColon=false -#spaceBeforeContextColon=false -#spaceInsideBrackets=false -#spaceInsideParentheses=false -spacesAroundMultiImports=false -#spacesWithinPatternBinders=true diff --git a/bin/config b/bin/config deleted file mode 100644 index dbc113f3..00000000 --- a/bin/config +++ /dev/null @@ -1,23 +0,0 @@ -#! /bin/bash - -# config for CI scripts - -set -o pipefail - -# Travis uses a detached HEAD during builds -# CURRENT_BRANCH is used when updating Whitesource to determine the correct project name -export CURRENT_BRANCH=${TRAVIS_BRANCH} - -# Command to run tests based on current branch -[[ "$TRAVIS_BRANCH" == "master" && "$TRAVIS_PULL_REQUEST" == false ]] && SBT_CMD="coverageOn test coverageOff publish" || SBT_CMD="coverage test" -export SBT_CMD - -runSbt() { - sbt "$@" -} - -doAfterSuccess() { - if [ $1 -eq 0 ]; then - sbt ++$2 coverageReport coveralls - fi -} diff --git a/bin/test b/bin/test deleted file mode 100755 index ac166e2d..00000000 --- a/bin/test +++ /dev/null @@ -1,13 +0,0 @@ -#! /bin/bash - -. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" - -SCALA_VERSION="$1" - -runSbt ++$SCALA_VERSION $SBT_CMD - -result=$? - -doAfterSuccess $result $SCALA_VERSION - -exit $result diff --git a/bin/test-code-style b/bin/test-code-style deleted file mode 100755 index b2a0debf..00000000 --- a/bin/test-code-style +++ /dev/null @@ -1,12 +0,0 @@ -#! /bin/bash - -. "$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )/config" - -runSbt +scalariformFormat \ - +test:scalariformFormat - -git diff --exit-code || ( - echo "ERROR: Scalariform check failed, see differences above." - echo "To fix, format your sources using sbt scalariformFormat test:scalariformFormat before submitting a pull request." - false -) diff --git a/build.sbt b/build.sbt index 3f67bd71..d25a5030 100644 --- a/build.sbt +++ b/build.sbt @@ -9,21 +9,23 @@ description := "Redis cache plugin for the Play framework 2" organization := "com.github.karelcemus" -scalaVersion := "2.13.8" +crossScalaVersions := Seq("2.13.12") //, "3.3.0" -crossScalaVersions := Seq("2.12.15", scalaVersion.value) +scalaVersion := crossScalaVersions.value.head -playVersion := "2.8.13" +playVersion := "2.9.0" libraryDependencies ++= Seq( // play framework cache API "com.typesafe.play" %% "play-cache" % playVersion.value % Provided, // redis connector - "com.github.karelcemus" %% "rediscala" % "1.9.1", + "io.github.rediscala" %% "rediscala" % "1.14.0-akka", // test framework with mockito extension - "org.specs2" %% "specs2-mock" % "4.13.2" % Test, + "org.specs2" %% "specs2-mock" % "4.20.3" % Test, // test module for play framework - "com.typesafe.play" %% "play-specs2" % playVersion.value % Test + "com.typesafe.play" %% "play-test" % playVersion.value % Test, + // to run integration tests + "com.dimafeng" %% "testcontainers-scala-core" % "0.41.0" % Test ) resolvers ++= Seq( @@ -34,10 +36,6 @@ javacOptions ++= Seq("-Xlint:unchecked", "-encoding", "UTF-8") scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked") -homepage := Some(url("https://github.com/karelcemus/play-redis")) - -licenses := Seq("Apache 2" -> url("https://www.apache.org/licenses/LICENSE-2.0")) - enablePlugins(CustomReleasePlugin) // exclude from tests coverage diff --git a/project/CustomReleasePlugin.scala b/project/CustomReleasePlugin.scala index 0c077d08..d73b395e 100644 --- a/project/CustomReleasePlugin.scala +++ b/project/CustomReleasePlugin.scala @@ -32,7 +32,9 @@ object CustomReleasePlugin extends AutoPlugin { pomIncludeRepository := { _ => false }, // customized release process releaseProcess := customizedReleaseProcess, - // + // release details + homepage := Some(url("https://github.com/karelcemus/play-redis")), + licenses := Seq("Apache 2" -> url("https://www.apache.org/licenses/LICENSE-2.0")), scmInfo := Some( ScmInfo( url("https://github.com/KarelCemus/play-i18n.git"), diff --git a/project/plugins.sbt b/project/plugins.sbt index 0ebf6af2..81b0b226 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,14 +1,11 @@ resolvers += Resolver.url("scoverage-bintray", url("https://dl.bintray.com/sksamuel/sbt-plugins/"))(Resolver.ivyStylePatterns) // checks for updates -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.1") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") // code coverage and uploader of the coverage results into the coveralls.io -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.8.2") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.1") - -// code linter -addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.3") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") +addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.11") // library release addSbtPlugin("com.github.sbt" % "sbt-git" % "2.0.1") diff --git a/src/main/scala-2.12/play/api/cache/redis/JavaCompatibilityBase.scala b/src/main/scala-2.12/play/api/cache/redis/JavaCompatibilityBase.scala deleted file mode 100644 index c333601f..00000000 --- a/src/main/scala-2.12/play/api/cache/redis/JavaCompatibilityBase.scala +++ /dev/null @@ -1,8 +0,0 @@ -package play.api.cache.redis - -import scala.collection.convert.{DecorateAsJava, DecorateAsScala} - -private[redis] trait JavaCompatibilityBase extends DecorateAsScala with DecorateAsJava - -private[redis] object JavaCompatibilityBase extends JavaCompatibilityBase - diff --git a/src/main/scala-2.13/play/api/cache/redis/JavaCompatibilityBase.scala b/src/main/scala/play/api/cache/redis/JavaCompatibilityBase.scala similarity index 100% rename from src/main/scala-2.13/play/api/cache/redis/JavaCompatibilityBase.scala rename to src/main/scala/play/api/cache/redis/JavaCompatibilityBase.scala 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 60928212..dfde44e9 100644 --- a/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala +++ b/src/main/scala/play/api/cache/redis/configuration/RedisHost.scala @@ -14,6 +14,8 @@ trait RedisHost { def port: Int /** redis database identifier to work with */ def database: Option[Int] + /** when enabled security, this returns username for the AUTH command */ + def username: Option[String] /** when enabled security, this returns password for the AUTH command */ def password: Option[String] // $COVERAGE-OFF$ @@ -21,13 +23,13 @@ trait RedisHost { 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 => Equals.check(this, that)(_.host, _.port, _.database, _.password) + case that: RedisHost => Equals.check(this, that)(_.host, _.port, _.username, _.database, _.password) case _ => false } /** to string */ - override def toString = (password, database) match { - case (Some(password), Some(database)) => s"redis://redis:$password@$host:$port?db=$database" - case (Some(password), None) => s"redis://redis:$password@$host:$port" + override def toString: String = (password, database) match { + case (Some(password), Some(database)) => s"redis://${username.getOrElse("redis")}:$password@$host:$port?db=$database" + case (Some(password), None) => s"redis://${username.getOrElse("redis")}:$password@$host:$port" case (None, Some(database)) => s"redis://$host:$port?db=$database" case (None, None) => s"redis://$host:$port" } @@ -38,7 +40,7 @@ object RedisHost extends ConfigLoader[RedisHost] { import RedisConfigLoader._ /** expected format of the environment variable */ - private val ConnectionString = "redis://((?[^:]+):(?[^@]+)@)?(?[^:]+):(?[0-9]+)".r("auth", "user", "password", "host", "port") + private val ConnectionString = "redis://((?[^:]+):(?[^@]+)@)?(?[^:]+):(?[0-9]+)".r("auth", "username", "password", "host", "port") def load(config: Config, path: String): RedisHost = apply( host = config.getString(path / "host"), @@ -51,29 +53,31 @@ object RedisHost extends ConfigLoader[RedisHost] { def fromConnectionString(connectionString: String): RedisHost = ConnectionString findFirstMatchIn connectionString match { // read the environment variable and fill missing information from the local configuration file case Some(matcher) => new RedisHost { - val host = matcher.group("host") - val port = matcher.group("port").toInt - val database = None - val password = Option(matcher.group("password")) + override val host: String = matcher.group("host") + override val port: Int = matcher.group("port").toInt + override val database: Option[Nothing] = None + override val username: Option[String] = Option(matcher.group("username")) + override val password: Option[String] = Option(matcher.group("password")) } // unexpected format case None => throw new IllegalArgumentException(s"Unexpected format of the connection string: '$connectionString'. Expected format is 'redis://[user:password@]host:port'.") } - def apply(host: String, port: Int, database: Option[Int] = None, password: Option[String] = None): RedisHost = - create(host, port, database, password) + def apply(host: String, port: Int, database: Option[Int] = None, username: Option[String] = None, password: Option[String] = None): RedisHost = + create(host, port, database, username, password) /** hackish method to preserve nice names of parameters in apply */ - @inline private def create(_host: String, _port: Int, _database: Option[Int], _password: Option[String]) = new RedisHost { - val host = _host - val port = _port - val database = _database - val password = _password + @inline private def create(_host: String, _port: Int, _database: Option[Int], _username: Option[String], _password: Option[String]) = new RedisHost { + override val host: String = _host + override val port: Int = _port + override val database: Option[Int] = _database + override val username: Option[String] = _username + override val password: Option[String] = _password } // $COVERAGE-OFF$ - def unapply(host: RedisHost): Option[(String, Int, Option[Int], Option[String])] = { - Some((host.host, host.port, host.database, host.password)) + def unapply(host: RedisHost): Option[(String, Int, Option[Int],Option[String], Option[String])] = { + Some((host.host, host.port, host.database, host.username, host.password)) } // $COVERAGE-ON$ } @@ -84,8 +88,9 @@ object RedisHost extends ConfigLoader[RedisHost] { */ trait RedisDelegatingHost extends RedisHost { def innerHost: RedisHost - def host = innerHost.host - def port = innerHost.port - def database = innerHost.database - def password = innerHost.password + override def host: String = innerHost.host + override def port: Int = innerHost.port + override def database: Option[Int] = innerHost.database + override def username: Option[String] = innerHost.username + override def password: Option[String] = innerHost.password } 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 0df7c272..03c2f334 100644 --- a/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala +++ b/src/main/scala/play/api/cache/redis/configuration/RedisInstance.scala @@ -91,6 +91,7 @@ trait RedisSentinel extends RedisInstance { def sentinels: List[RedisHost] def masterGroup: String + def username: Option[String] def password: Option[String] def database: Option[Int] @@ -103,23 +104,28 @@ trait RedisSentinel extends RedisInstance { object RedisSentinel { - def apply(name: String, masterGroup: String, - sentinels: List[RedisHost], - settings: RedisSettings, - password: Option[String] = None, - database: Option[Int] = None): RedisSentinel with RedisDelegatingSettings = - create(name, masterGroup, password, database, sentinels, settings) + def apply( + name: String, + masterGroup: String, + sentinels: List[RedisHost], + settings: RedisSettings, + username: Option[String] = None, + password: Option[String] = None, + database: Option[Int] = None + ): RedisSentinel with RedisDelegatingSettings = + create(name, masterGroup, username, password, database, sentinels, settings) @inline - private def create(_name: String, _masterGroup: String, _password: Option[String], _database: Option[Int], + private def create(_name: String, _masterGroup: String, _username: Option[String], _password: Option[String], _database: Option[Int], _sentinels: List[RedisHost], _settings: RedisSettings) = new RedisSentinel with RedisDelegatingSettings { - val name = _name - val masterGroup = _masterGroup - val password = _password - val database = _database - val sentinels = _sentinels - val settings = _settings + override val name: String = _name + override val masterGroup: String = _masterGroup + override val username: Option[String] = _username + override val password: Option[String] = _password + override val database: Option[Int] = _database + override val sentinels: List[RedisHost] = _sentinels + override val settings: RedisSettings = _settings } } 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 eaad4a90..4c2c0bc5 100644 --- a/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala +++ b/src/main/scala/play/api/cache/redis/connector/RedisCommands.scala @@ -62,6 +62,7 @@ private[connector] class RedisCommandsStandalone(configuration: RedisStandalone) host = host, port = port, db = database, + username = username, password = password ) with FailEagerly with RedisRequestTimeout { @@ -105,7 +106,7 @@ private[connector] class RedisCommandsCluster(configuration: RedisCluster)(impli val client: RedisClusterClient = new RedisClusterClient( nodes.map { - case RedisHost(host, port, database, password) => RedisServer(host.resolvedIpAddress, port, password, database) + case RedisHost(host, port, database, username, password) => RedisServer(host.resolvedIpAddress, port, username, password, database) } ) with RedisRequestTimeout { protected val timeout = configuration.timeout.redis @@ -116,8 +117,8 @@ private[connector] class RedisCommandsCluster(configuration: RedisCluster)(impli // $COVERAGE-OFF$ def start() = { def servers = nodes.map { - case RedisHost(host, port, Some(database), _) => s" $host:$port?database=$database" - case RedisHost(host, port, None, _) => s" $host:$port" + case RedisHost(host, port, Some(database), _, _) => s" $host:$port?database=$database" + case RedisHost(host, port, None, _, _) => s" $host:$port" } log.info(s"Redis cluster cache actor started. It is connected to ${servers mkString ", "}") @@ -143,9 +144,10 @@ private[connector] class RedisCommandsSentinel(configuration: RedisSentinel)(imp val client: SentinelMonitoredRedisClient with RedisRequestTimeout = new SentinelMonitoredRedisClient( configuration.sentinels.map { - case RedisHost(host, port, _, _) => (host.resolvedIpAddress, port) + case RedisHost(host, port, _, _, _) => (host.resolvedIpAddress, port) }, master = configuration.masterGroup, + username = configuration.username, password = configuration.password, db = configuration.database ) with RedisRequestTimeout { diff --git a/src/main/scala/play/api/cache/redis/impl/RedisCaches.scala b/src/main/scala/play/api/cache/redis/impl/RedisCaches.scala index ca1b9ce4..e1989f9b 100644 --- a/src/main/scala/play/api/cache/redis/impl/RedisCaches.scala +++ b/src/main/scala/play/api/cache/redis/impl/RedisCaches.scala @@ -1,12 +1,11 @@ package play.api.cache.redis.impl import javax.inject.Provider - import play.api.Environment import play.api.cache.redis._ import play.api.inject.ApplicationLifecycle - import akka.actor.ActorSystem +import play.api.cache.{AsyncCacheApi, DefaultSyncCacheApi} /** * Aggregates all available redis APIs into a single handler. This simplifies @@ -27,16 +26,16 @@ private[redis] class RedisCachesProvider(instance: RedisInstance, serializer: co private implicit lazy val runtime: RedisRuntime = RedisRuntime(instance, instance.recovery, instance.invocationPolicy, instance.prefix)(system) - private implicit def implicitEnvironment = environment + private implicit def implicitEnvironment: Environment = environment lazy val get = new RedisCaches { - lazy val redisConnector = new connector.RedisConnectorProvider(instance, serializer).get - lazy val async = new AsyncRedisImpl(redisConnector) - lazy val sync = new SyncRedis(redisConnector) - lazy val scalaSync = new play.api.cache.DefaultSyncCacheApi(async) - lazy val scalaAsync = async - lazy val java = new AsyncJavaRedis(async) - lazy val javaAsync = java - lazy val javaSync = new play.cache.DefaultSyncCacheApi(java) + lazy val redisConnector: RedisConnector = new connector.RedisConnectorProvider(instance, serializer).get + lazy val async: AsyncRedis = new AsyncRedisImpl(redisConnector) + lazy val sync: CacheApi = new SyncRedis(redisConnector) + lazy val scalaSync: play.api.cache.SyncCacheApi = new play.api.cache.DefaultSyncCacheApi(async) + lazy val scalaAsync: play.api.cache.AsyncCacheApi = async + lazy val java: AsyncJavaRedis = new AsyncJavaRedis(async) + lazy val javaAsync: play.cache.redis.AsyncCacheApi = java + lazy val javaSync: play.cache.SyncCacheApi = new play.cache.DefaultSyncCacheApi(java) } } diff --git a/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala b/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala index dab7f8bd..f4a9b68a 100644 --- a/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala +++ b/src/main/scala/play/api/cache/redis/impl/RedisListJavaImpl.scala @@ -78,7 +78,7 @@ class RedisListJavaImpl[Elem](internal: RedisList[Elem, Future])(implicit runtim new AsyncRedisListModificationJavaImpl(internal.modify) } - class AsyncRedisListViewJavaImpl(view: internal.RedisListView) extends AsyncRedisList.AsyncRedisListView[Elem] { + private class AsyncRedisListViewJavaImpl(view: internal.RedisListView) extends AsyncRedisList.AsyncRedisListView[Elem] { def slice(from: Int, end: Int): CompletionStage[JavaList[Elem]] = { async { implicit context => @@ -87,7 +87,7 @@ class RedisListJavaImpl[Elem](internal: RedisList[Elem, Future])(implicit runtim } } - class AsyncRedisListModificationJavaImpl(modification: internal.RedisListModification) extends AsyncRedisList.AsyncRedisListModification[Elem] { + private class AsyncRedisListModificationJavaImpl(modification: internal.RedisListModification) extends AsyncRedisList.AsyncRedisListModification[Elem] { def collection(): AsyncRedisList[Elem] = This diff --git a/src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala b/src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala index 7304bb1c..f474311e 100644 --- a/src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala +++ b/src/main/scala/play/api/cache/redis/impl/RedisSetJavaImpl.scala @@ -1,32 +1,32 @@ package play.api.cache.redis.impl -import scala.concurrent.Future - import play.api.cache.redis.RedisSet import play.cache.redis.AsyncRedisSet +import scala.concurrent.Future + class RedisSetJavaImpl[Elem](internal: RedisSet[Elem, Future])(implicit runtime: RedisRuntime) extends AsyncRedisSet[Elem] { import JavaCompatibility._ - def add(element: Elem*): CompletionStage[AsyncRedisSet[Elem]] = { + override def add(elements: Elem*): CompletionStage[AsyncRedisSet[Elem]] = { async { implicit context => - internal.add(element: _*).map(_ => this) + internal.add(elements: _*).map(_ => this) } } - def contains(element: Elem): CompletionStage[java.lang.Boolean] = { + override def contains(element: Elem): CompletionStage[java.lang.Boolean] = { async { implicit context => internal.contains(element).map(Boolean.box) } } - def remove(element: Elem*): CompletionStage[AsyncRedisSet[Elem]] = { + override def remove(elements: Elem*): CompletionStage[AsyncRedisSet[Elem]] = { async { implicit context => - internal.remove(element: _*).map(_ => this) + internal.remove(elements: _*).map(_ => this) } } - def toSet: CompletionStage[JavaSet[Elem]] = { + override def toSet: CompletionStage[JavaSet[Elem]] = { async { implicit context => internal.toSet.map(_.asJava) } diff --git a/src/test/scala-2.11/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala b/src/test/scala-2.11/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala deleted file mode 100644 index 949012dc..00000000 --- a/src/test/scala-2.11/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala +++ /dev/null @@ -1,58 +0,0 @@ -package play.api.cache.redis.connector - -import java.util.Date - -import scala.reflect.ClassTag - -import play.api.inject.guice.GuiceApplicationBuilder -import play.api.cache.redis._ - -import org.joda.time.{DateTime, DateTimeZone} -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification - -class ScalaSpecificSerializerSpec extends Specification with Mockito { - import SerializerImplicits._ - - private val system = GuiceApplicationBuilder().build().actorSystem - - private implicit val serializer: AkkaSerializer = new AkkaSerializerImpl(system) - - "AkkaEncoder" should "encode" >> { - - "custom classes" in { - SimpleObject("B", 3).encoded mustEqual """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0 - |cyRTaW1wbGVPYmplY3TbTGkeqUDNdwIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJp - |bmc7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces - } - - "list" in { - List("A", "B", "C").encoded mustEqual """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 - |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu - |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== - """.stripMargin.removeAllWhitespaces - } - } - - "AkkaDecoder" should "decode" >> { - - "custom classes" in { - """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0 - |cyRTaW1wbGVPYmplY3TbTGkeqUDNdwIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJp - |bmc7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces.decoded[SimpleObject] mustEqual SimpleObject("B", 3) - } - - "list" in { - """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 - |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu - |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== - """.stripMargin.removeAllWhitespaces.decoded[List[String]] mustEqual List("A", "B", "C") - } - } -} diff --git a/src/test/scala-2.12/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala b/src/test/scala-2.12/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala deleted file mode 100644 index 0671b42c..00000000 --- a/src/test/scala-2.12/play/api/cache/redis/connector/ScalaSpecificSerializerSpec.scala +++ /dev/null @@ -1,58 +0,0 @@ -package play.api.cache.redis.connector - -import java.util.Date - -import scala.reflect.ClassTag - -import play.api.inject.guice.GuiceApplicationBuilder -import play.api.cache.redis._ - -import org.joda.time.{DateTime, DateTimeZone} -import org.specs2.mock.Mockito -import org.specs2.mutable.Specification - -class ScalaSpecificSerializerSpec extends Specification with Mockito { - import SerializerImplicits._ - - private val system = GuiceApplicationBuilder().build().actorSystem - - private implicit val serializer: AkkaSerializer = new AkkaSerializerImpl(system) - - "AkkaEncoder" should "encode" >> { - - "custom classes" in { - SimpleObject("B", 3).encoded mustEqual """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0c - |yRTaW1wbGVPYmplY3TbTGkeqUDNdwIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJpbm - |c7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces - } - - "list" in { - List("A", "B", "C").encoded mustEqual """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 - |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu - |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== - """.stripMargin.removeAllWhitespaces - } - } - - "AkkaDecoder" should "decode" >> { - - "custom classes" in { - """ - |rO0ABXNyAD9wbGF5LmFwaS5jYWNoZS5yZWRpcy5jb25uZWN0b3IuU2VyaWFsaXplckltcGxpY2l0c - |yRTaW1wbGVPYmplY3TbTGkeqUDNdwIAAkkABXZhbHVlTAADa2V5dAASTGphdmEvbGFuZy9TdHJpbm - |c7eHAAAAADdAABQg== - """.stripMargin.removeAllWhitespaces.decoded[SimpleObject] mustEqual SimpleObject("B", 3) - } - - "list" in { - """ - |rO0ABXNyADJzY2FsYS5jb2xsZWN0aW9uLmltbXV0YWJsZS5MaXN0JFNlcmlhbGl6YXRpb25Qcm94 - |eQAAAAAAAAABAwAAeHB0AAFBdAABQnQAAUNzcgAsc2NhbGEuY29sbGVjdGlvbi5pbW11dGFibGUu - |TGlzdFNlcmlhbGl6ZUVuZCSKXGNb91MLbQIAAHhweA== - """.stripMargin.removeAllWhitespaces.decoded[List[String]] mustEqual List("A", "B", "C") - } - } -} diff --git a/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala b/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala new file mode 100644 index 00000000..da359612 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/ClusterRedisContainer.scala @@ -0,0 +1,20 @@ +package play.api.cache.redis + +trait ClusterRedisContainer extends RedisContainer { + + protected def redisMaster = 4 + + protected def redisSlaves = 1 + + override protected lazy val redisConfig: RedisContainerConfig = + RedisContainerConfig( + "grokzen/redis-cluster:latest", + 0.until(redisMaster * (redisSlaves + 1)).map(7000 + _), + Map( + "IP" -> "0.0.0.0", + "INITIAL_PORT" -> "7000", + "MASTERS" -> s"$redisMaster", + "SLAVES_PER_MASTER" -> s"$redisSlaves", + ), + ) +} diff --git a/src/test/scala/play/api/cache/redis/ExpirationSpec.scala b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala index b546c6f1..8f160d79 100644 --- a/src/test/scala/play/api/cache/redis/ExpirationSpec.scala +++ b/src/test/scala/play/api/cache/redis/ExpirationSpec.scala @@ -1,12 +1,11 @@ package play.api.cache.redis -import java.util.Date +import org.specs2.mutable.Specification +import java.time.Instant +import java.util.Date import scala.concurrent.duration._ -import org.joda.time._ -import org.specs2.mutable.Specification - /** *

This specification tests expiration conversion

*/ @@ -14,19 +13,19 @@ class ExpirationSpec extends Specification { "Expiration" should { - def expireAt = DateTime.now().plusMinutes(5).plusSeconds(30) + val expireAt: Instant = Instant.now().plusSeconds(5.minutes.toSeconds).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) + new Date(expireAt.toEpochMilli).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) + LocalDateTime.ofInstant(expireAt, ZoneId.systemDefault()).asExpiration must beBetween(expirationFrom, expirationTo) } } } diff --git a/src/test/scala/play/api/cache/redis/ForAllTestContainer.scala b/src/test/scala/play/api/cache/redis/ForAllTestContainer.scala new file mode 100644 index 00000000..224f0a7c --- /dev/null +++ b/src/test/scala/play/api/cache/redis/ForAllTestContainer.scala @@ -0,0 +1,19 @@ +package play.api.cache.redis + +import com.dimafeng.testcontainers.SingleContainer +import org.specs2.specification.BeforeAfterAll + +trait ForAllTestContainer extends BeforeAfterAll { + + def newContainer: SingleContainer[_] + + final protected lazy val container = newContainer + + override def beforeAll(): Unit = { + container.start() + } + + override def afterAll(): Unit = { + container.stop() + } +} diff --git a/src/test/scala/play/api/cache/redis/Implicits.scala b/src/test/scala/play/api/cache/redis/Implicits.scala index 671c37b3..abd74bf5 100644 --- a/src/test/scala/play/api/cache/redis/Implicits.scala +++ b/src/test/scala/play/api/cache/redis/Implicits.scala @@ -42,7 +42,7 @@ object Implicits { 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) + def awaitForFuture = Await.result(future, 2.minutes) } implicit def implicitlyAny2Callable[T](f: => T): Callable[T] = new Callable[T] { @@ -73,9 +73,12 @@ trait ReducedMockito extends MocksCreation with MockitoStubs with CapturedArgument // with MockitoMatchers - with ArgThat + // with ArgThat with Expectations - with MockitoFunctions + with MockitoFunctions { + + override def argThat[T, U <: T](m: org.specs2.matcher.Matcher[U]): T = super.argThat(m) +} object MockitoImplicits extends ReducedMockito @@ -85,13 +88,13 @@ trait WithApplication { protected def builder = new GuiceApplicationBuilder() - private val theBuilder = builder + private lazy val theBuilder = builder - protected val injector = theBuilder.injector + protected lazy val injector = theBuilder.injector() - protected val application: Application = injector.instanceOf[Application] + protected lazy val application: Application = injector.instanceOf[Application] - implicit protected val system = injector.instanceOf[ActorSystem] + implicit protected lazy val system: ActorSystem = injector.instanceOf[ActorSystem] } trait WithHocon { @@ -101,13 +104,13 @@ trait WithHocon { protected def hocon: String - protected val config = { + protected lazy val config = { val reference = ConfigFactory.load() val local = ConfigFactory.parseString(hocon.stripMargin) local.withFallback(reference) } - protected val configuration = Configuration(config) + protected lazy val configuration = Configuration(config) } abstract class WithConfiguration(val hocon: String) extends WithHocon with Around with Scope { diff --git a/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala b/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala index cd748a9a..172f089a 100644 --- a/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala +++ b/src/test/scala/play/api/cache/redis/RecoveryPolicySpec.scala @@ -5,10 +5,9 @@ import scala.concurrent.Future import play.api.Logger import org.specs2.concurrent.ExecutionEnv -import org.specs2.mock.Mockito import org.specs2.mutable.Specification -class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with Mockito { +class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito { class BasicPolicy extends RecoveryPolicy { def recoverFrom[T](rerun: => Future[T], default: => Future[T], failure: RedisException) = default @@ -25,7 +24,7 @@ class RecoveryPolicySpec(implicit ee: ExecutionEnv) extends Specification with M 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 + def any: UnexpectedResponseException = unexpectedAny } "Recovery Policy" should { diff --git a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala index 07258ed1..f8ca9019 100644 --- a/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala +++ b/src/test/scala/play/api/cache/redis/RedisCacheComponentsSpec.scala @@ -2,23 +2,23 @@ package play.api.cache.redis import play.api._ import play.api.inject.ApplicationLifecycle - import org.specs2.mutable.Specification -import org.specs2.specification.AfterAll -class RedisCacheComponentsSpec extends Specification with WithApplication with AfterAll { +class RedisCacheComponentsSpec extends Specification with WithApplication with StandaloneRedisContainer { + import Implicits._ - object components extends RedisCacheComponents { + object components extends RedisCacheComponents with WithHocon { def actorSystem = system def applicationLifecycle = injector.instanceOf[ApplicationLifecycle] def environment = injector.instanceOf[Environment] - def configuration = injector.instanceOf[Configuration] - val syncRedis = cacheApi("play").sync + lazy val syncRedis = cacheApi("play").sync + override lazy val configuration = Configuration(config) + override protected def hocon: String = s"play.cache.redis.port: ${container.mappedPort(defaultPort)}" } private type Cache = CacheApi - private val cache = components.syncRedis + private lazy val cache = components.syncRedis val prefix = "components-sync" @@ -42,7 +42,8 @@ class RedisCacheComponentsSpec extends Specification with WithApplication with A } } - def afterAll() = { - Shutdown.run + override def afterAll() = { + Shutdown.run.awaitForFuture + super.afterAll() } } diff --git a/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala index 13db7d7a..8735aa78 100644 --- a/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala +++ b/src/test/scala/play/api/cache/redis/RedisCacheModuleSpec.scala @@ -163,16 +163,17 @@ object RedisCacheModuleSpec { 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 + override def name = defaultCacheName + override def invocationContext = "akka.actor.default-dispatcher" + override def invocationPolicy = "lazy" + override def timeout = RedisTimeouts(1.second) + override def recovery = "log-and-default" + override def source = "my-instance" + override def prefix = None + override def host = localhost + override def port = defaultPort + override def database = None + override def username = None + override def password = None } } diff --git a/src/test/scala/play/api/cache/redis/RedisContainer.scala b/src/test/scala/play/api/cache/redis/RedisContainer.scala new file mode 100644 index 00000000..b0682d46 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/RedisContainer.scala @@ -0,0 +1,19 @@ +package play.api.cache.redis + +import com.dimafeng.testcontainers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait + +trait RedisContainer extends ForAllTestContainer { + + protected def redisConfig: RedisContainerConfig + + private lazy val config = redisConfig + + override val newContainer = GenericContainer( + dockerImage = config.redisDockerImage, + exposedPorts = config.redisPorts, + env = config.redisEnvironment, + waitStrategy = Wait.forListeningPorts(config.redisPorts: _*), + ) +} + diff --git a/src/test/scala/play/api/cache/redis/RedisContainerConfig.scala b/src/test/scala/play/api/cache/redis/RedisContainerConfig.scala new file mode 100644 index 00000000..f5cdb544 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/RedisContainerConfig.scala @@ -0,0 +1,7 @@ +package play.api.cache.redis + +final case class RedisContainerConfig( + redisDockerImage: String, + redisPorts: Seq[Int], + redisEnvironment: Map[String, String], +) diff --git a/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala b/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala new file mode 100644 index 00000000..505e0276 --- /dev/null +++ b/src/test/scala/play/api/cache/redis/StandaloneRedisContainer.scala @@ -0,0 +1,11 @@ +package play.api.cache.redis + +trait StandaloneRedisContainer extends RedisContainer { + + override protected lazy val redisConfig: RedisContainerConfig = + RedisContainerConfig( + redisDockerImage = "redis:latest", + redisPorts = Seq(6379), + redisEnvironment = Map.empty + ) +} diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala index eebca540..312f7120 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisHostSpec.scala @@ -1,13 +1,13 @@ package play.api.cache.redis.configuration import play.api.cache.redis._ - import org.specs2.mutable.Specification +import play.api.ConfigLoader class RedisHostSpec extends Specification { import Implicits._ - private implicit val loader = RedisHost + private implicit val loader: ConfigLoader[RedisHost] = RedisHost "host with database and password" in new WithConfiguration( """ @@ -34,7 +34,7 @@ class RedisHostSpec extends Specification { } "host from connection string" in { - RedisHost.fromConnectionString("redis://redis:something@localhost:6378") mustEqual RedisHost("localhost", 6378, password = "something") + RedisHost.fromConnectionString("redis://redis:something@localhost:6378") mustEqual RedisHost("localhost", 6378, username = "redis", password = "something") RedisHost.fromConnectionString("redis://localhost:6378") mustEqual RedisHost("localhost", 6378) // test invalid string RedisHost.fromConnectionString("redis:/localhost:6378") must throwA[IllegalArgumentException] diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala index 8a6956ad..7c81c3dd 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceManagerTest.scala @@ -1,5 +1,6 @@ package play.api.cache.redis.configuration +import play.api.ConfigLoader import play.api.cache.redis._ /** @@ -18,7 +19,7 @@ case class RedisInstanceManagerTest(default: String)(providers: RedisInstancePro abstract class WithRedisInstanceManager(hocon: String) extends WithConfiguration(hocon) { - private implicit val loader = RedisInstanceManager + private implicit val loader: ConfigLoader[RedisInstanceManager] = RedisInstanceManager protected val manager = configuration.get[RedisInstanceManager]("play.cache.redis") } diff --git a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala index 71a47652..0a9e016e 100644 --- a/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala +++ b/src/test/scala/play/api/cache/redis/configuration/RedisInstanceProviderSpec.scala @@ -7,7 +7,7 @@ class RedisInstanceProviderSpec extends Specification { val defaultCache = RedisStandalone(defaultCacheName, RedisHost(localhost, defaultPort, database = 0), defaults) - implicit val resolver = new RedisInstanceResolver { + implicit val resolver: RedisInstanceResolver = new RedisInstanceResolver { def resolve = { case `defaultCacheName` => defaultCache } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala index 066afa59..3c3b8f79 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisClusterSpec.scala @@ -15,19 +15,31 @@ import org.specs2.specification.{AfterAll, BeforeAll} /** *

Specification of the low level connector implementing basic commands

*/ -class RedisClusterSpec(implicit ee: ExecutionEnv) extends Specification with BeforeAll with AfterAll with WithApplication { +class RedisClusterSpec(implicit ee: ExecutionEnv) extends Specification with WithApplication with ClusterRedisContainer { + + args(skipAll=true) import Implicits._ - implicit private val lifecycle = application.injector.instanceOf[ApplicationLifecycle] + implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] - implicit private val runtime = RedisRuntime("cluster", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit private val runtime: RedisRuntime = RedisRuntime("cluster", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) private val serializer = new AkkaSerializerImpl(system) - private val clusterInstance = RedisCluster(defaultCacheName, nodes = RedisHost(dockerIp, 7000) :: RedisHost(dockerIp, 7001) :: RedisHost(dockerIp, 7002) :: RedisHost(dockerIp, 7003) :: Nil, defaults) + private lazy val containerIpAddress = container.containerIpAddress + + private lazy val clusterInstance = RedisCluster( + name = defaultCacheName, + nodes = RedisHost(containerIpAddress, container.mappedPort(7000)) :: + RedisHost(containerIpAddress, container.mappedPort(7001)) :: + RedisHost(containerIpAddress, container.mappedPort(7002)) :: + RedisHost(containerIpAddress, container.mappedPort(7003)) :: + Nil, + settings = defaults + ) - private val connector: RedisConnector = new RedisConnectorProvider(clusterInstance, serializer).get + private lazy val connector: RedisConnector = new RedisConnectorProvider(clusterInstance, serializer).get val prefix = "cluster-test" @@ -71,12 +83,16 @@ class RedisClusterSpec(implicit ee: ExecutionEnv) extends Specification with Bef } } - def beforeAll() = { + override def beforeAll() = { + super.beforeAll() // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap(connector.remove).await + connector.matching(s"$prefix-*").flatMap { + keys => Future.sequence(keys.map(connector.remove(_))) + }.awaitForFuture } - def afterAll() = { - Shutdown.run + override def afterAll() = { + Shutdown.run.awaitForFuture + super.afterAll() } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala index a1c10fa4..6a2221d0 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorFailureSpec.scala @@ -40,7 +40,7 @@ class RedisConnectorFailureSpec(implicit ee: ExecutionEnv) extends Specification } "fail when decoder fails" in new MockedConnector { - serializer.decode(anyString)(any[ClassTag[_]]) returns simulatedFailure + serializer.decode(anyString)(any()) returns simulatedFailure commands.get[String](key) returns someValue // run the test connector.get[String](key) must throwA[SerializationException].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 48f2be72..dcbfc156 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisConnectorSpec.scala @@ -1,29 +1,31 @@ package play.api.cache.redis.connector -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} - +import org.specs2.concurrent.ExecutionEnv +import org.specs2.mutable.Specification import play.api.cache.redis._ +import play.api.cache.redis.configuration.{RedisHost, RedisStandalone} import play.api.cache.redis.impl._ import play.api.inject.ApplicationLifecycle -import org.specs2.concurrent.ExecutionEnv -import org.specs2.mutable.Specification -import org.specs2.specification.{AfterAll, BeforeAll} +import scala.concurrent.duration._ +import scala.concurrent.{ExecutionContext, Future} /** *

Specification of the low level connector implementing basic commands

*/ -class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with BeforeAll with AfterAll with WithApplication { +class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with WithApplication with StandaloneRedisContainer { import Implicits._ - implicit private val lifecycle = application.injector.instanceOf[ApplicationLifecycle] + implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] - implicit private val runtime = RedisRuntime("connector", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit private val runtime: RedisRuntime = RedisRuntime("connector", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) private val serializer = new AkkaSerializerImpl(system) - private val connector: RedisConnector = new RedisConnectorProvider(defaultInstance, serializer).get + private lazy val connector: RedisConnector = new RedisConnectorProvider( + RedisStandalone(defaultCacheName, RedisHost(container.containerIpAddress, container.mappedPort(defaultPort)), defaults), + serializer + ).get val prefix = "connector-test" @@ -67,11 +69,11 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B } "hit after mset" in new TestCase { - connector.mSet(s"$prefix-mset-$idx-1" -> "value-1", s"$prefix-mset-$idx-2" -> "value-2").await + connector.mSet(s"$prefix-mset-$idx-1" -> "value-1", s"$prefix-mset-$idx-2" -> "value-2").awaitForFuture connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), Some("value-2"), None)).await - connector.mSet(s"$prefix-mset-$idx-3" -> "value-3", s"$prefix-mset-$idx-2" -> null).await + connector.mSet(s"$prefix-mset-$idx-3" -> "value-3", s"$prefix-mset-$idx-2" -> null).awaitForFuture connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), None, Some("value-3"))).await - connector.mSet(s"$prefix-mset-$idx-3" -> null).await + connector.mSet(s"$prefix-mset-$idx-3" -> null).awaitForFuture connector.mGet[String](s"$prefix-mset-$idx-1", s"$prefix-mset-$idx-2", s"$prefix-mset-$idx-3") must beEqualTo(List(Some("value-1"), None, None)).await } @@ -84,7 +86,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B "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 + connector.expire(s"$prefix-$idx", 1.minute).awaitForFuture // wait until the first duration expires Future.after(3) must not(throwA[Throwable]).awaitFor(4.seconds) connector.get[String](s"$prefix-$idx") must beSome("value").await @@ -174,7 +176,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B 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.remove(s"$prefix-remove-multiple-1", s"$prefix-remove-multiple-2", s"$prefix-remove-multiple-3").awaitForFuture connector.get[String](s"$prefix-remove-multiple-1") must beNone.await connector.get[String](s"$prefix-remove-multiple-2") must beNone.await connector.get[String](s"$prefix-remove-multiple-3") must beNone.await @@ -187,7 +189,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B 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.remove(s"$prefix-remove-batch-1", s"$prefix-remove-batch-2", s"$prefix-remove-batch-3").awaitForFuture connector.get[String](s"$prefix-remove-batch-1") must beNone.await connector.get[String](s"$prefix-remove-batch-2") must beNone.await connector.get[String](s"$prefix-remove-batch-3") must beNone.await @@ -235,14 +237,14 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B "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.append(s"$prefix-append-to-null", "value").awaitForFuture connector.get[String](s"$prefix-append-to-null") must beSome("value").await } "append to existing string" in new TestCase { connector.set(s"$prefix-append-to-some", "some").await connector.get[String](s"$prefix-append-to-some") must beSome("some").await - connector.append(s"$prefix-append-to-some", " value").await + connector.append(s"$prefix-append-to-some", " value").awaitForFuture connector.get[String](s"$prefix-append-to-some") must beSome("some value").await } @@ -266,7 +268,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B "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.listSetAt(s"$prefix-list-set", 1, "D").awaitForFuture connector.listSlice[String](s"$prefix-list-set", 0, -1) must beEqualTo(List("A", "D", "C")).await connector.listSetAt(s"$prefix-list-set", 3, "D") must throwA[IndexOutOfBoundsException].await } @@ -297,7 +299,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B "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.listTrim(s"$prefix-list-trim", 1, 2).awaitForFuture connector.listSize(s"$prefix-list-trim") must beEqualTo(2).await connector.listSlice[String](s"$prefix-list-trim", 0, -1) must beEqualTo(List("B", "C")).await } @@ -380,7 +382,7 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B val key = s"$prefix-hash-set" connector.hashSize(key) must beEqualTo(0).await - connector.hashGetAll(key) must beEqualTo(Map.empty).await + connector.hashGetAll[String](key) must beEqualTo(Map.empty).await connector.hashKeys(key) must beEqualTo(Set.empty).await connector.hashValues[String](key) must beEqualTo(Set.empty).await @@ -496,12 +498,14 @@ class RedisConnectorSpec(implicit ee: ExecutionEnv) extends Specification with B } } - def beforeAll() = { + override def beforeAll(): Unit = { + super.beforeAll() // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap(connector.remove).await + connector.matching(s"$prefix-*").flatMap(connector.remove).awaitForFuture } - def afterAll() = { - Shutdown.run + override def afterAll(): Unit = { + Shutdown.run.awaitForFuture + super.afterAll() } } diff --git a/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala b/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala index 92471e05..6532f101 100644 --- a/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/RedisSentinelSpec.scala @@ -16,17 +16,19 @@ import scala.concurrent.{ExecutionContext, Future} */ class RedisSentinelSpec(implicit ee: ExecutionEnv) extends Specification with BeforeAll with AfterAll with WithApplication { + args(skipAll=true) + import Implicits._ - implicit private val lifecycle = application.injector.instanceOf[ApplicationLifecycle] + implicit private val lifecycle: ApplicationLifecycle = application.injector.instanceOf[ApplicationLifecycle] - implicit private val runtime = RedisRuntime("sentinel", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) + implicit private val runtime: RedisRuntime = RedisRuntime("sentinel", syncTimeout = 5.seconds, ExecutionContext.global, new LogAndFailPolicy, LazyInvocation) private val serializer = new AkkaSerializerImpl(system) - private val sentinelInstance = RedisSentinel(defaultCacheName, masterGroup = "sentinel5000", sentinels = RedisHost(dockerIp, 5000) :: RedisHost(dockerIp, 5001) :: RedisHost(dockerIp, 5002) :: Nil, defaults) + private lazy val sentinelInstance = RedisSentinel(defaultCacheName, masterGroup = "sentinel5000", sentinels = RedisHost(dockerIp, 5000) :: RedisHost(dockerIp, 5001) :: RedisHost(dockerIp, 5002) :: Nil, defaults) - private val connector: RedisConnector = new RedisConnectorProvider(sentinelInstance, serializer).get + private lazy val connector: RedisConnector = new RedisConnectorProvider(sentinelInstance, serializer).get val prefix = "sentinel-test" @@ -72,7 +74,7 @@ class RedisSentinelSpec(implicit ee: ExecutionEnv) extends Specification with Be def beforeAll(): Unit = { // initialize the connector by flushing the database - connector.matching(s"$prefix-*").flatMap(connector.remove).await + connector.matching(s"$prefix-*").flatMap(connector.remove).awaitForFuture } def afterAll(): Unit = { diff --git a/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala b/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala index 77cd047b..a148cf35 100644 --- a/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala +++ b/src/test/scala/play/api/cache/redis/connector/SerializerSpec.scala @@ -2,12 +2,9 @@ package play.api.cache.redis.connector import java.util.Date -import scala.reflect.ClassTag - import play.api.inject.guice.GuiceApplicationBuilder import play.api.cache.redis._ -import org.joda.time.{DateTime, DateTimeZone} import org.specs2.mock.Mockito import org.specs2.mutable.Specification @@ -67,16 +64,6 @@ class SerializerSpec extends Specification with Mockito { new Date(123).encoded mustEqual "rO0ABXNyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAAAAAAB7eA==" } - "datetime" in { - new DateTime(123456L, DateTimeZone.forID("UTC")).encoded mustEqual """ - |rO0ABXNyABZvcmcuam9kYS50aW1lLkRhdGVUaW1luDx4ZGpb3fkCAAB4cgAfb3JnLmpvZGEudGlt - |ZS5iYXNlLkJhc2VEYXRlVGltZf//+eFPXS6jAgACSgAHaU1pbGxpc0wAC2lDaHJvbm9sb2d5dAAa - |TG9yZy9qb2RhL3RpbWUvQ2hyb25vbG9neTt4cAAAAAAAAeJAc3IAJ29yZy5qb2RhLnRpbWUuY2hy - |b25vLklTT0Nocm9ub2xvZ3kkU3R1YqnIEWZxN1AnAwAAeHBzcgAfb3JnLmpvZGEudGltZS5EYXRl - |VGltZVpvbmUkU3R1YqYvAZp8MhrjAwAAeHB3BQADVVRDeHg= - """.stripMargin.removeAllWhitespaces - } - "null" in { new ValueEncoder(null).encoded must throwA[UnsupportedOperationException] } @@ -134,18 +121,8 @@ class SerializerSpec extends Specification with Mockito { "rO0ABXNyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAAAAAAB7eA==".decoded[Date] mustEqual new Date(123) } - "datetime" in { - """ - |rO0ABXNyABZvcmcuam9kYS50aW1lLkRhdGVUaW1luDx4ZGpb3fkCAAB4cgAfb3JnLmpvZGEudGlt - |ZS5iYXNlLkJhc2VEYXRlVGltZf//+eFPXS6jAgACSgAHaU1pbGxpc0wAC2lDaHJvbm9sb2d5dAAa - |TG9yZy9qb2RhL3RpbWUvQ2hyb25vbG9neTt4cAAAAAAAAeJAc3IAJ29yZy5qb2RhLnRpbWUuY2hy - |b25vLklTT0Nocm9ub2xvZ3kkU3R1YqnIEWZxN1AnAwAAeHBzcgAfb3JnLmpvZGEudGltZS5EYXRl - |VGltZVpvbmUkU3R1YqYvAZp8MhrjAwAAeHB3BQADVVRDeHg= - """.stripMargin.removeAllWhitespaces.decoded[DateTime] mustEqual new DateTime(123456L, DateTimeZone.forID("UTC")) - } - - "forgotten type" in { - def decoded: String = "something".decoded + "invalid type" in { + def decoded: Date = "something".decoded[Date] decoded must throwA[IllegalArgumentException] } } diff --git a/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala index add0174b..d98d4311 100644 --- a/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/AsyncRedisSpec.scala @@ -42,7 +42,7 @@ class AsyncRedisSpec(implicit ee: ExecutionEnv) extends Specification with Reduc "getOrElseUpdate (failing orElse)" in new MockedAsyncRedis with OrElse { connector.get[String](anyString)(anyClassTag) returns None - cache.getOrElseUpdate(key)(failedFuture) must throwA[TimeoutException].await + cache.getOrElseUpdate[String](key)(failedFuture) must throwA[TimeoutException].await orElse mustEqual 2 } diff --git a/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala b/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala index 829439c9..7ea241d5 100644 --- a/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/BuildersSpec.scala @@ -11,7 +11,7 @@ import org.specs2.mock.Mockito import org.specs2.mutable.Specification import org.specs2.specification._ -class BuildersSpec(implicit ee: ExecutionEnv) extends Specification with Mockito with WithApplication { +class BuildersSpec(implicit ee: ExecutionEnv) extends Specification with ReducedMockito with WithApplication { import Builders._ import BuildersSpec._ diff --git a/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala b/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala index fccf66aa..7db3b3c6 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisCacheImplicits.scala @@ -46,7 +46,7 @@ object RedisCacheImplicits { protected def policy: RecoveryPolicy = new RecoverWithDefault {} - protected implicit val runtime = mock[RedisRuntime] + protected implicit val runtime: RedisRuntime = mock[RedisRuntime] runtime.context returns ExecutionContext.global runtime.invocation returns invocation runtime.prefix returns RedisEmptyPrefix @@ -104,7 +104,7 @@ object RedisCacheImplicits { protected val classTagKey = s"classTag::$key" protected val classTagOther = s"classTag::$other" - protected implicit val environment = mock[Environment] + protected implicit val environment: Environment = mock[Environment] protected val async = mock[AsyncRedis] protected val cache: play.cache.redis.AsyncCacheApi = new AsyncJavaRedis(async) diff --git a/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala index 18bab084..2a6a0ac7 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisCacheSpec.scala @@ -13,8 +13,6 @@ class RedisCacheSpec(implicit ee: ExecutionEnv) extends Specification with Reduc import org.mockito.ArgumentMatchers._ - val key = "key" - val value = "value" val expiration = 1.second "Redis Cache" should { @@ -213,7 +211,7 @@ class RedisCacheSpec(implicit ee: ExecutionEnv) extends Specification with Reduc "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 + cache.getOrFuture[String](key)(failedFuture) must throwA[TimeoutException].await orElse mustEqual 2 } diff --git a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala index 31891f64..96390493 100644 --- a/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala +++ b/src/test/scala/play/api/cache/redis/impl/RedisRuntimeSpec.scala @@ -8,7 +8,7 @@ class RedisRuntimeSpec extends Specification with WithApplication { import Implicits._ - implicit val recoveryResolver = new RecoveryPolicyResolverImpl + implicit val recoveryResolver: RecoveryPolicyResolver = new RecoveryPolicyResolverImpl "RedisRuntime" should { import RedisRuntime._