diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f95d0ef..c9fd17a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,9 @@ name: Continuous Integration on: pull_request: - branches: ['*'] + branches: ['**'] push: - branches: ['*'] + branches: ['**'] tags: [v*] env: @@ -26,7 +26,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.5] + scala: [2.13.6, 3.0.1] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -53,12 +53,12 @@ jobs: key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - name: Check that workflows are up to date - run: sbt ++${{ matrix.scala }} githubWorkflowCheck + run: sbt --client '++${{ matrix.scala }}; githubWorkflowCheck' - - run: sbt ++${{ matrix.scala }} ci + - run: sbt --client '++${{ matrix.scala }}; ci' - name: Compress target directories - run: tar cf targets.tar target db-testkit/target db-test-data/target project/target + run: tar cf targets.tar target db-test-data/target db-test-data-ce2/target db-testkit-ce2/target db-testkit/target project/target - name: Upload target directories uses: actions/upload-artifact@v2 @@ -73,7 +73,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - scala: [2.13.5] + scala: [2.13.6] java: [adopt@1.8] runs-on: ${{ matrix.os }} steps: @@ -99,12 +99,22 @@ jobs: ~/Library/Caches/Coursier/v1 key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} - - name: Download target directories (2.13.5) + - name: Download target directories (2.13.6) uses: actions/download-artifact@v2 with: - name: target-${{ matrix.os }}-2.13.5-${{ matrix.java }} + name: target-${{ matrix.os }}-2.13.6-${{ matrix.java }} - - name: Inflate target directories (2.13.5) + - name: Inflate target directories (2.13.6) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3.0.1) + uses: actions/download-artifact@v2 + with: + name: target-${{ matrix.os }}-3.0.1-${{ matrix.java }} + + - name: Inflate target directories (3.0.1) run: | tar xf targets.tar rm targets.tar @@ -112,4 +122,4 @@ jobs: - name: Import signing key run: echo $PGP_SECRET | base64 -d | gpg --import - - run: sbt ++${{ matrix.scala }} release \ No newline at end of file + - run: sbt --client '++${{ matrix.scala }}; release' \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf index a8ead2d..dff99c2 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -16,58 +16,68 @@ # limitations under the License. # -# See https://scalameta.org/scalafmt/docs/configuration.html for details, -# mostly done by trial and error. But I guess this formatting is "good enough" +# See https://scalameta.org/scalafmt/docs/configuration.html for details # +# https://github.com/scalameta/scalafmt/releases +version = 3.0.0-RC6 -#https://github.com/scalameta/scalafmt/releases -version = 2.7.5 - -project { - #if you don't specify that files ending in .scala .sbt with $, - # .scalafmt.conf is included in the formatting attempt - includeFilters = [ - ".*.\\.scala$" - ".*\\..sbt$" - ] +fileOverride { + "glob:**/src/main/scala/**" { + runner.dialect = scala213source3 + } + "glob:**/src/test/scala/**" { + runner.dialect = scala213source3 + } + "glob:**/src/main/scala-2/**" { + runner.dialect = scala213source3 + } + "glob:**/src/test/scala-2/**" { + runner.dialect = scala213source3 + } + "glob:**/src/main/scala-3/**" { + runner.dialect = scala3 + } + "glob:**/src/test/scala-3/**" { + runner.dialect = scala3 + } + "glob:**/project/*.scala" { + runner.dialect = scala212 + } + "glob:**/*.sbt" { + runner.dialect = Sbt1 + } } maxColumn = 120 -# Note. Only for the truest vertical aligners. This is a new option, -# feel free to open PR enabling more crazy vertical alignment here. -# Expect changes. align = most align.openParenCallSite = false align.openParenDefnSite = false align.multiline = true +align.tokens."+" = [ -align.tokens.add = [ - - {code = "<-", owner = "Enumerator.Generator"} - //Everything before the Term.Assign was the default regex. To find the - //default value, look through the code here: - //https://github.com/scalameta/scalafmt/blob/master/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Align.scala - {code = "=", owner = "(Enumerator.Generator|Val|Defn.(Va(l|r)|Def|Type))|Term.Assign"} + {code = "<-", owner = "Enumerator.Generator"}, - //used to align comments - "//" + # Everything before the Term.Assign was the default regex. To find the + # default value, look through the code here: + # https://github.com/scalameta/scalafmt/blob/master/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Align.scala + {code = "=", owner = "(Enumerator.Generator|Val|Defn.(Va(l|r)|Def|Type))|Term.Assign|Param"}, - //used for aligning type definition - ":" - - //used when creating tuples - {code = "->", owner = "Term.ApplyInfix"}, + "//", # used to align comments + ":", # used for aligning type definition + {code = "->", owner = "Term.ApplyInfix"}, # used to align tuples 1 -> 2 //sbt specific {code = "%", owner = "Term.ApplyInfix"}, - {code = "%%", owner = "Term.ApplyInfix"} - {code = "%%%", owner = "Term.ApplyInfix"} - {code = ":=", owner = "Term.ApplyInfix"} - {code = "withSources", owner = "Term.ApplyInfix"} + {code = "%%", owner = "Term.ApplyInfix"}, + {code = "%%%", owner = "Term.ApplyInfix"}, + {code = ":=", owner = "Term.ApplyInfix"}, + {code = "withSources", owner = "Term.ApplyInfix"}, "extends" ] +############################################################################### + continuationIndent { callSite = 2 defnSite = 2 @@ -76,12 +86,16 @@ continuationIndent { withSiteRelativeToExtends = 0 } +############################################################################### + verticalMultiline.atDefnSite = false verticalMultiline.newlineAfterOpenParen = true verticalMultiline.arityThreshold = 3 +############################################################################### + newlines { - alwaysBeforeTopLevelStatements = true + topLevelStatements = [before] sometimesBeforeColonInMethodReturnType = true penalizeSingleSelectMultiArgList = false alwaysBeforeElseAfterCurlyIf = true @@ -91,30 +105,35 @@ newlines { afterCurlyLambda = squash } +############################################################################### + spaces { afterKeywordBeforeParen = true } +############################################################################### + binPack { parentConstructors = true literalArgumentLists = true literalsMinArgCount = 5 } +############################################################################### optIn { breaksInsideChains = false - //preserves existing newlines in . chain calls. - //See: optIn.breakChainOnFirstMethodDot = true + # preserves existing newlines in . chain calls. + # See: optIn.breakChainOnFirstMethodDot = true breakChainOnFirstMethodDot = true blankLineBeforeDocstring = true } +############################################################################### + rewrite { rules = [ - SortImports SortModifiers - # if your for has more than one single <- then it gets transformed into a multit-line curly brace one PreferCurlyFors AvoidInfix RedundantBraces @@ -127,40 +146,21 @@ rewrite.redundantBraces.methodBodies = true rewrite.redundantBraces.includeUnitMethods = false rewrite.redundantBraces.stringInterpolation = true rewrite.redundantBraces.parensForOneLineApply = true +rewrite.neverInfix.excludeFilters = [withSources] -#we only really want to disable infix notation -# for map and flatMap and the like, because it -# kills IDE performance and type inference -rewrite.neverInfix.excludeFilters = [ - until - to - by - eq - ne - "should.*" - "contain.*" - "must.*" - in - be - of # behaviour of ("X") - taggedAs - thrownBy - synchronized - have - when - size - theSameElementsAs - withSources -] +############################################################################### preset = default danglingParentheses.preset = true +############################################################################### + assumeStandardLibraryStripMargin = true includeNoParensInSelectChains = false includeCurlyBraceInSelectChains = true trailingCommas = multiple +############################################################################### runner { optimizer { @@ -171,4 +171,4 @@ runner { # minimum number of func arguments before config-style (look at top of file) is enabled forceConfigStyleMinArgCount = 2 } -} \ No newline at end of file +} diff --git a/CHANGELOG.md b/CHANGELOG.md index bfcf3e5..992cc2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 # unreleased +# 0.3.0 + +This is the first release for a stable Scala 3 version, and with cats-effect 3 support! + +### :warning: breaking +- `pureharm-db-testkit` now depends on cats-effect `3.2.1`, and the corresponding dependencies for it. +- `pureharm-db-testkit-ce2` is binary, and source compatible with `pureharm-db-testkit` from version `0.2.0`, so if you haven't migrated to CE3 yet, use the former module. + +### Dependency upgrades +- [pureharm-core](https://github.com/busymachines/pureharm-core/releases) `0.3.0` +- [pureharm-effects-cats](https://github.com/busymachines/pureharm-effects-cats/releases) `0.5.0` +- [pureharm-testkit](https://github.com/busymachines/pureharm-testkit/releases) `0.4.0` +- [pureharm-db-core](https://github.com/busymachines/pureharm-db-core/releases) `0.5.0` +- [pureharm-db-flyway](https://github.com/busymachines/pureharm-db-flyway/releases) `0.6.0` + +### New Scala versions: +- `2.13.6` +- `3.0.1` for JVM + JS platforms +- drop `3.0.0-RC2`, `3.0.0-RC3` + +### internals +- bump scalafmt to `3.0.0-RC6` — from `2.7.5` +- bump sbt to `1.5.5` +- bump sbt-spiewak to `0.21.0` +- bump sbt-scalafmt to `2.4.3` +- bump sbt-scalajs-crossproject to `1.1.0` +- bump sbt-scalajs to `1.6.0` + # 0.2.0 ### breaking changes: diff --git a/README.md b/README.md index 2060699..56dd348 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,21 @@ See [changelog](./CHANGELOG.md). We do not even pretend to support anything other than Postgresql. +## Scala versions +- `2.13`: JVM +- `3`: JVM + ## modules -- `"com.busymachines" %% "pureharm-db-testkit" % "0.2.0"`. Which has these as its main dependencies: - - [pureharm-core-anomaly](https://github.com/busymachines/pureharm-core/releases) `0.2.0` - - [pureharm-core-sprout](https://github.com/busymachines/pureharm-core/releases) `0.2.0` - - [pureharm-core-identifiable](https://github.com/busymachines/pureharm-core/releases) `0.2.0` - - [pureharm-effects-cats](https://github.com/busymachines/pureharm-effects-cats/releases) `0.4.0` - - [pureharm-testkit](https://github.com/busymachines/pureharm-testkit/releases) `0.3.0` - - [pureharm-db-core](https://github.com/busymachines/pureharm-db-core/releases) `0.4.0` - - [pureharm-db-flyway](https://github.com/busymachines/pureharm-db-flyway/releases) `0.4.0` -- `"com.busymachines" %% "pureharm-db-test-data" % "0.2.0"` +- `"com.busymachines" %% "pureharm-db-testkit" % "0.3.0"`. Which has these as its main dependencies: + - [pureharm-core-anomaly](https://github.com/busymachines/pureharm-core/releases) `0.3.0` + - [pureharm-core-sprout](https://github.com/busymachines/pureharm-core/releases) `0.3.0` + - [pureharm-core-identifiable](https://github.com/busymachines/pureharm-core/releases) `0.3.0` + - [pureharm-effects-cats](https://github.com/busymachines/pureharm-effects-cats/releases) `0.5.0` + - [pureharm-testkit](https://github.com/busymachines/pureharm-testkit/releases) `0.4.0` + - [pureharm-db-core](https://github.com/busymachines/pureharm-db-core/releases) `0.5.0` + - [pureharm-db-flyway](https://github.com/busymachines/pureharm-db-flyway/releases) `0.6.0` +- `"com.busymachines" %% "pureharm-db-test-data" % "0.3.0"` - used only for common tests between different DB modules ## usage diff --git a/build.sbt b/build.sbt index 12eec85..957038b 100644 --- a/build.sbt +++ b/build.sbt @@ -18,12 +18,12 @@ //============================== build details ================================ //============================================================================= -addCommandAlias("github-gen", "githubWorkflowGenerate") -addCommandAlias("github-check", "githubWorkflowCheck") Global / onChangedBuildSource := ReloadOnSourceChanges -val Scala213 = "2.13.5" -val Scala3RC1 = "3.0.0-RC1" +// format: off +val Scala213 = "2.13.6" +val Scala3 = "3.0.1" +// format: on //============================================================================= //============================ publishing details ============================= @@ -32,10 +32,10 @@ val Scala3RC1 = "3.0.0-RC1" //see: https://github.com/xerial/sbt-sonatype#buildsbt ThisBuild / sonatypeCredentialHost := "s01.oss.sonatype.org" -ThisBuild / baseVersion := "0.2" -ThisBuild / organization := "com.busymachines" +ThisBuild / baseVersion := "0.3" +ThisBuild / organization := "com.busymachines" ThisBuild / organizationName := "BusyMachines" -ThisBuild / homepage := Option(url("https://github.com/busymachines/pureharm-db-testkit")) +ThisBuild / homepage := Option(url("https://github.com/busymachines/pureharm-db-testkit")) ThisBuild / scmInfo := Option( ScmInfo( @@ -44,8 +44,8 @@ ThisBuild / scmInfo := Option( ) ) -/** I want my email. So I put this here. To reduce a few lines of code, - * the sbt-spiewak plugin generates this (except email) from these two settings: +/** I want my email. So I put this here. To reduce a few lines of code, the sbt-spiewak plugin generates this (except + * email) from these two settings: * {{{ * ThisBuild / publishFullName := "Loránd Szakács" * ThisBuild / publishGithubUser := "lorandszakacs" @@ -60,7 +60,7 @@ ThisBuild / developers := List( ) ) -ThisBuild / startYear := Some(2019) +ThisBuild / startYear := Some(2019) ThisBuild / licenses := List("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) //until we get to 1.0.0, we keep strictSemVer false @@ -70,12 +70,12 @@ ThisBuild / spiewakMainBranches := List("main") ThisBuild / Test / publishArtifact := false ThisBuild / scalaVersion := Scala213 -ThisBuild / crossScalaVersions := List(Scala213) //List(Scala213, Scala3RC1) +ThisBuild / crossScalaVersions := List(Scala213, Scala3) //required for binary compat checks ThisBuild / versionIntroduced := Map( - Scala213 -> "0.1.0", - Scala3RC1 -> "0.1.0", + Scala213 -> "0.1.0", + Scala3 -> "0.3.0", ) //============================================================================= @@ -85,11 +85,11 @@ ThisBuild / resolvers += Resolver.sonatypeRepo("releases") ThisBuild / resolvers += Resolver.sonatypeRepo("snapshots") // format: off -val pureharmCoreV = "0.2.0" //https://github.com/busymachines/pureharm-core/releases -val pureharmEffectsV = "0.4.0" //https://github.com/busymachines/pureharm-effects-cats/releases -val pureharmDBCoreV = "0.4.0" //https://github.com/busymachines/pureharm-db-core/releases -val pureharmDBFlywayV = "0.4.0" //https://github.com/busymachines/pureharm-db-flyway/releases -val pureharmTestkitV = "0.3.0" //https://github.com/busymachines/pureharm-testkit/releases +val pureharmCoreV = "0.3.0" //https://github.com/busymachines/pureharm-core/releases +val pureharmEffectsV = "0.5.0" //https://github.com/busymachines/pureharm-effects-cats/releases +val pureharmDBCoreV = "0.5.0" //https://github.com/busymachines/pureharm-db-core/releases +val pureharmDBFlywayV = "0.6.0" //https://github.com/busymachines/pureharm-db-flyway/releases +val pureharmTestkitV = "0.4.0" //https://github.com/busymachines/pureharm-testkit/releases // format: on //============================================================================= @@ -101,6 +101,8 @@ lazy val root = project .aggregate( `db-testkit`, `db-test-data`, + `db-testkit-ce2`, + `db-test-data-ce2`, ) .enablePlugins(NoPublishPlugin) .enablePlugins(SonatypeCiReleasePlugin) @@ -112,44 +114,77 @@ lazy val `db-testkit` = project name := "pureharm-db-testkit", libraryDependencies ++= Seq( // format: off - "com.busymachines" %% "pureharm-core-identifiable" % pureharmCoreV withSources(), - "com.busymachines" %% "pureharm-core-anomaly" % pureharmCoreV withSources(), - "com.busymachines" %% "pureharm-core-sprout" % pureharmCoreV withSources(), - "com.busymachines" %% "pureharm-effects-cats" % pureharmEffectsV withSources(), - "com.busymachines" %% "pureharm-testkit" % pureharmTestkitV withSources(), - "com.busymachines" %% "pureharm-db-core" % pureharmDBCoreV withSources(), - "com.busymachines" %% "pureharm-db-flyway" % pureharmDBFlywayV withSources(), + "com.busymachines" %% "pureharm-core-identifiable" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-core-anomaly" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-core-sprout" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-effects-cats" % pureharmEffectsV withSources(), + "com.busymachines" %% "pureharm-testkit" % pureharmTestkitV withSources(), + "com.busymachines" %% "pureharm-db-core" % pureharmDBCoreV withSources(), + "com.busymachines" %% "pureharm-db-flyway" % pureharmDBFlywayV withSources(), // format: on ), - ).settings( + ) + .settings( javaOptions ++= Seq("-source", "1.8", "-target", "1.8") ) lazy val `db-test-data` = project .settings(commonSettings) .settings( - name := "pureharm-db-test-data", - ).settings( + name := "pureharm-db-test-data" + ) + .settings( javaOptions ++= Seq("-source", "1.8", "-target", "1.8") - ).dependsOn( + ) + .dependsOn( `db-testkit` ) +lazy val `db-testkit-ce2` = project + .settings(commonSettings) + .settings( + name := "pureharm-db-testkit-ce2", + libraryDependencies ++= Seq( + // format: off + "com.busymachines" %% "pureharm-core-identifiable" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-core-anomaly" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-core-sprout" % pureharmCoreV withSources(), + "com.busymachines" %% "pureharm-effects-cats-2" % pureharmEffectsV withSources(), + "com.busymachines" %% "pureharm-testkit-ce2" % pureharmTestkitV withSources(), + "com.busymachines" %% "pureharm-db-core" % pureharmDBCoreV withSources(), + "com.busymachines" %% "pureharm-db-flyway-ce2" % pureharmDBFlywayV withSources(), + // format: on + ), + ) + .settings( + javaOptions ++= Seq("-source", "1.8", "-target", "1.8") + ) + +lazy val `db-test-data-ce2` = project + .settings(commonSettings) + .settings( + name := "pureharm-db-test-data-ce2" + ) + .settings( + javaOptions ++= Seq("-source", "1.8", "-target", "1.8") + ) + .dependsOn( + `db-testkit-ce2` + ) + //============================================================================= //================================= Settings ================================== //============================================================================= lazy val commonSettings = Seq( - Compile / unmanagedSourceDirectories ++= { - val major = if (isDotty.value) "-3" else "-2" - List(CrossType.Pure, CrossType.Full).flatMap( - _.sharedSrcDir(baseDirectory.value, "main").toList.map(f => file(f.getPath + major)) - ) - }, - Test / unmanagedSourceDirectories ++= { - val major = if (isDotty.value) "-3" else "-2" - List(CrossType.Pure, CrossType.Full).flatMap( - _.sharedSrcDir(baseDirectory.value, "test").toList.map(f => file(f.getPath + major)) - ) - }, + scalacOptions ++= scalaCompilerOptions(scalaVersion.value) ) + +def scalaCompilerOptions(scalaVersion: String): Seq[String] = + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, _)) => + Seq[String]( + //"-Xsource:3" + ) + case _ => Seq.empty[String] + } diff --git a/db-test-data-ce2/src/main/resources/db/migration/V0.0.1__PureharmRows.sql b/db-test-data-ce2/src/main/resources/db/migration/V0.0.1__PureharmRows.sql new file mode 100644 index 0000000..643d30e --- /dev/null +++ b/db-test-data-ce2/src/main/resources/db/migration/V0.0.1__PureharmRows.sql @@ -0,0 +1,35 @@ +-- +-- Copyright (c) 2019 BusyMachines +-- +-- See company homepage at: https://www.busymachines.com/ +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. +-- +create TABLE pureharm_rows( + "id" VARCHAR NOT NULL PRIMARY KEY, + "byte" SMALLINT NOT NULL, + "int" INT NOT NULL, + "long" BIGINT NOT NULL, + "big_decimal" DOUBLE PRECISION NOT NULL, + "string" VARCHAR NOT NULL, + "jsonb_col" JSONB NOT NULL, + "opt_col" VARCHAR NULL, + "unique_string" VARCHAR NOT NULL UNIQUE, + "unique_int" INT NOT NULL UNIQUE, + "unique_json" JSONB NOT NULL UNIQUE +); + +CREATE TABLE "pureharm_external_rows"( + "id" UUID NOT NULL PRIMARY KEY, + "row_id" VARCHAR NOT NULL REFERENCES "pureharm_rows"("id") +) \ No newline at end of file diff --git a/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala new file mode 100644 index 0000000..982f507 --- /dev/null +++ b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala @@ -0,0 +1,39 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testdata + +import busymachines.pureharm.db._ + +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 27 + * Jan 2020 + */ +object PHRTestDBConfig { + + val dbConfig: DBConnectionConfig = DBConnectionConfig( + host = DBHost("localhost"), + port = DBPort(20010), + dbName = DatabaseName("pureharm_test"), + username = DBUsername("pureharmony"), + password = DBPassword("pureharmony"), + schema = Option.empty, //Modify in each test accordingly before using + ) + + def schemaName(s: String): Option[SchemaName] = Option(SchemaName(s"pureharm_test_$s")) + +} diff --git a/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala new file mode 100644 index 0000000..ad0be59 --- /dev/null +++ b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala @@ -0,0 +1,28 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testdata + +import busymachines.pureharm.db._ + +/** To be then implemented in the concrete slick, or doobie modules + * + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 13 + * Jun 2019 + */ +private[pureharm] trait PHRowRepo[F[_]] extends Repo[F, PHRow, SproutPK] diff --git a/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala new file mode 100644 index 0000000..3272ef9 --- /dev/null +++ b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala @@ -0,0 +1,109 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testdata + +import busymachines.pureharm.db.testkit._ +import busymachines.pureharm.effects._ +import busymachines.pureharm.identifiable._ + +/** Common class to enforce a minimal contract for out of the box implementations of busymachines.pureharm.db.Repo for + * various backends. For now only for doobie, and slick, soon, hopefully for skunk too :D + * + * In your own production code you probably create something similar that inherits from RepoTest to get a bunch of free + * tests for your Repos <3 to ensure that at least it typechecks. + * + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 + */ +abstract private[pureharm] class PHRowRepoTest[Trans] extends RepoTest[PHRow, SproutPK, Trans] { + override type ResourceType <: PHRowRepo[IO] + + override def data: PHRowRepoTest.pureharmRows.type = PHRowRepoTest.pureharmRows +} + +object PHRowRepoTest { + + object pureharmRows extends RepoTestData[PHRow, SproutPK] { + override def iden: Identifiable[PHRow, SproutPK] = PHRow.identifiable + + override def row1: PHRow = PHRow( + id = SproutPK("row1_id"), + byte = SproutByte(245.toByte), + int = SproutInt(41), + long = SproutLong(0.toLong), + bigDecimal = SproutBigDecimal(BigDecimal("1390749832749238")), + string = SproutString("row1_string"), + jsonbCol = PHJSONCol(42, "row1_json_column"), + optionalCol = Option(SproutString("row1_optional_value")), + uniqueString = UniqueString("unique_string_value_1"), + uniqueInt = UniqueInt(490), + uniqueJSON = UniqueJSON(PHJSONCol(421, "unique_json_value_1")), + ) + + override val row2: PHRow = PHRow( + id = SproutPK("row2_id"), + byte = SproutByte(123.toByte), + int = SproutInt(4321), + long = SproutLong(12L), + bigDecimal = SproutBigDecimal(BigDecimal("23414")), + string = SproutString("row2_string"), + jsonbCol = PHJSONCol(44, "row2_json_column"), + optionalCol = Option(SproutString("row2_optional_value")), + uniqueString = UniqueString("unique_string_value_2"), + uniqueInt = UniqueInt(491), + uniqueJSON = UniqueJSON(PHJSONCol(421, "unique_json_value_2")), + ) + + override def nonExistentPK: SproutPK = SproutPK("120-3921-039213") + + override val row1Update1: PHRow = row1.copy( + byte = SproutByte(111.toByte), + int = SproutInt(42), + long = SproutLong(6.toLong), + bigDecimal = SproutBigDecimal(BigDecimal("328572")), + string = SproutString("updated_string"), + jsonbCol = PHJSONCol(79, "new_json_col"), + optionalCol = Option(SproutString("new opt_value")), + uniqueString = UniqueString("unique_string_value_1_update"), + uniqueInt = UniqueInt(49012), + uniqueJSON = UniqueJSON(PHJSONCol(4121, "unique_json_value_1_update")), + ) + + override val row1Update2: PHRow = row1.copy( + byte = SproutByte(5.toByte), + int = SproutInt(31), + long = SproutLong(988888.toLong), + bigDecimal = SproutBigDecimal(BigDecimal("89276")), + string = SproutString("updated_string_2"), + jsonbCol = PHJSONCol(1, "new_json_col_2"), + optionalCol = Option.empty, + ) + + val ext1: ExtPHRow = ExtPHRow( + id = SproutUUID.unsafeFromString("9320e4a5-a736-4623-96d4-92c61ce1c5cf"), + rowID = row1.id, + ) + + val extNoFPK: ExtPHRow = ExtPHRow( + id = SproutUUID.unsafeFromString("014ef766-232a-4952-87b8-f6b051dbf7d1"), + rowID = nonExistentPK, + ) + } + +} diff --git a/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/package.scala b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/package.scala new file mode 100644 index 0000000..b48f582 --- /dev/null +++ b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/package.scala @@ -0,0 +1,76 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db + +import java.util.UUID +import busymachines.pureharm.effects._ +import busymachines.pureharm.effects.implicits._ +import busymachines.pureharm.sprout._ + +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 13 + * Jun 2019 + */ +package object testdata { + + object SproutByte extends Sprout[Byte] + type SproutByte = SproutByte.Type + + object SproutInt extends Sprout[Int] + type SproutInt = SproutInt.Type + + object SproutLong extends Sprout[Long] + type SproutLong = SproutLong.Type + + object SproutBigDecimal extends Sprout[BigDecimal] + type SproutBigDecimal = SproutBigDecimal.Type + + object SproutString extends Sprout[String] + type SproutString = SproutString.Type + + object SproutPK extends Sprout[String] { + implicit val showPK: Show[this.Type] = Show[String].contramap(oldType) + } + type SproutPK = SproutPK.Type + + object UniqueString extends Sprout[String] + type UniqueString = UniqueString.Type + + object UniqueInt extends Sprout[Int] + type UniqueInt = UniqueInt.Type + + object UniqueJSON extends Sprout[PHJSONCol] + type UniqueJSON = UniqueJSON.Type + + object SproutUUID extends Sprout[UUID] { + def unsafeFromString(s: String): SproutUUID = this(UUID.fromString(s)) + def unsafeFromBytes(a: Array[Byte]): SproutUUID = this(UUID.nameUUIDFromBytes(a)) + + def unsafeGenerate: SproutUUID = this(UUID.randomUUID()) + def generate[F[_]: Sync]: F[SproutUUID] = Sync[F].delay(unsafeGenerate) + + implicit val showUUID: Show[SproutUUID] = Show.fromToString[SproutUUID] + } + type SproutUUID = SproutUUID.Type + + object schema { + val PureharmRows: TableName = TableName("pureharm_rows") + val PureharmExternalRows: TableName = TableName("pureharm_external_rows") + } + +} diff --git a/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala new file mode 100644 index 0000000..3661a00 --- /dev/null +++ b/db-test-data-ce2/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testdata + +import busymachines.pureharm.identifiable._ + +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 12 + * Jun 2019 + */ +final private[pureharm] case class PHRow( + id: SproutPK, + byte: SproutByte, + int: SproutInt, + long: SproutLong, + bigDecimal: SproutBigDecimal, + string: SproutString, + jsonbCol: PHJSONCol, + optionalCol: Option[SproutString], + uniqueString: UniqueString, + uniqueInt: UniqueInt, + uniqueJSON: UniqueJSON, +) + +object PHRow { + + implicit val identifiable: Identifiable[PHRow, SproutPK] = new Identifiable[PHRow, SproutPK] { + override def id(t: PHRow): SproutPK = t.id + override def fieldName: FieldName = FieldName("id") + } +} + +final private[pureharm] case class PHJSONCol( + jsonInt: Int, + jsonString: String, +) + +final case class ExtPHRow( + id: SproutUUID, + rowID: SproutPK, +) + +object ExtPHRow { + + implicit val identifiable: Identifiable[ExtPHRow, SproutUUID] = new Identifiable[ExtPHRow, SproutUUID] { + override def id(t: ExtPHRow): SproutUUID = t.id + override def fieldName: FieldName = FieldName("id") + } +} diff --git a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala index 5c1ce01..982f507 100644 --- a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala +++ b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRTestDBConfig.scala @@ -18,8 +18,10 @@ package busymachines.pureharm.db.testdata import busymachines.pureharm.db._ -/** @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 27 Jan 2020 +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 27 + * Jan 2020 */ object PHRTestDBConfig { diff --git a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala index f42c91a..ad0be59 100644 --- a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala +++ b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepo.scala @@ -20,7 +20,9 @@ import busymachines.pureharm.db._ /** To be then implemented in the concrete slick, or doobie modules * - * @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 13 Jun 2019 + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 13 + * Jun 2019 */ private[pureharm] trait PHRowRepo[F[_]] extends Repo[F, PHRow, SproutPK] diff --git a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala index 386179b..3272ef9 100644 --- a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala +++ b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/PHRowRepoTest.scala @@ -20,16 +20,16 @@ import busymachines.pureharm.db.testkit._ import busymachines.pureharm.effects._ import busymachines.pureharm.identifiable._ -/** Common class to enforce a minimal contract for out of the box implementations - * of busymachines.pureharm.db.Repo for various backends. For now only for doobie, and slick, - * soon, hopefully for skunk too :D +/** Common class to enforce a minimal contract for out of the box implementations of busymachines.pureharm.db.Repo for + * various backends. For now only for doobie, and slick, soon, hopefully for skunk too :D * - * In your own production code you probably create something similar that - * inherits from RepoTest to get a bunch of free tests for your - * Repos <3 to ensure that at least it typechecks. + * In your own production code you probably create something similar that inherits from RepoTest to get a bunch of free + * tests for your Repos <3 to ensure that at least it typechecks. * - * @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 25 Jun 2020 + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 */ abstract private[pureharm] class PHRowRepoTest[Trans] extends RepoTest[PHRow, SproutPK, Trans] { override type ResourceType <: PHRowRepo[IO] diff --git a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/package.scala b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/package.scala index ef06d5d..b48f582 100644 --- a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/package.scala +++ b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/package.scala @@ -21,8 +21,10 @@ import busymachines.pureharm.effects._ import busymachines.pureharm.effects.implicits._ import busymachines.pureharm.sprout._ -/** @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 13 Jun 2019 +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 13 + * Jun 2019 */ package object testdata { diff --git a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala index b66bbb7..3661a00 100644 --- a/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala +++ b/db-test-data/src/main/scala/busymachines/pureharm/db/testdata/phRow.scala @@ -16,10 +16,12 @@ package busymachines.pureharm.db.testdata -import busymachines.pureharm.identifiable.Identifiable +import busymachines.pureharm.identifiable._ -/** @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 12 Jun 2019 +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 12 + * Jun 2019 */ final private[pureharm] case class PHRow( id: SproutPK, @@ -36,7 +38,11 @@ final private[pureharm] case class PHRow( ) object PHRow { - implicit val identifiable: Identifiable[PHRow, SproutPK] = Identifiable.mkIdentifiable + + implicit val identifiable: Identifiable[PHRow, SproutPK] = new Identifiable[PHRow, SproutPK] { + override def id(t: PHRow): SproutPK = t.id + override def fieldName: FieldName = FieldName("id") + } } final private[pureharm] case class PHJSONCol( @@ -50,5 +56,9 @@ final case class ExtPHRow( ) object ExtPHRow { - implicit val identifiable: Identifiable[ExtPHRow, SproutUUID] = Identifiable.mkIdentifiable + + implicit val identifiable: Identifiable[ExtPHRow, SproutUUID] = new Identifiable[ExtPHRow, SproutUUID] { + override def id(t: ExtPHRow): SproutUUID = t.id + override def fieldName: FieldName = FieldName("id") + } } diff --git a/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTest.scala b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTest.scala new file mode 100644 index 0000000..5a92a7c --- /dev/null +++ b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTest.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testkit + +import busymachines.pureharm.effects._ +import busymachines.pureharm.testkit._ + +abstract class DBTest[Trans] extends PureharmTest { + type ResourceType + + def setup: DBTestSetup[Trans] + + def resource(meta: TestOptions, trans: Trans): Resource[IO, ResourceType] + + def testResource: SyncIO[FunFixture[ResourceType]] = ResourceFixture { testOptions => + for { + trans <- setup.transactor(testOptions) + fix <- resource(testOptions, trans) + } yield fix + } + +} diff --git a/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala new file mode 100644 index 0000000..874110b --- /dev/null +++ b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala @@ -0,0 +1,135 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testkit + +import busymachines.pureharm.anomaly.InconsistentStateCatastrophe +import busymachines.pureharm.db._ +import busymachines.pureharm.db.flyway.FlywayConfig +import busymachines.pureharm.effects._ +import busymachines.pureharm.effects.implicits._ +import busymachines.pureharm.testkit._ +import busymachines.pureharm.testkit.util._ + +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 + */ +trait DBTestSetup[DBTransactor] extends PureharmTestRuntimeLazyConversions { + final type RT = PureharmTestRuntime + + implicit class TestSetupClassName(config: DBConnectionConfig) { + + /** @see + * schemaName + */ + def withSchemaFromClassAndTest(testOptions: TestOptions): DBConnectionConfig = + config.copy(schema = Option(schemaName(testOptions))) + + /** @see + * schemaName + */ + def withSchemaFromClassAndTest(prefix: String, testOptions: TestOptions): DBConnectionConfig = + config.copy(schema = Option(schemaName(prefix, testOptions))) + } + + /** Should be overridden to create a connection config appropriate for the test + * + * To ensure unique schema names for test cases use the extension methods: + * TestSetupClassName.withSchemaFromClassAndTest or the explicit variants schemaName + */ + def dbConfig(testOptions: TestOptions)(implicit logger: TestLogger): DBConnectionConfig + + def flywayConfig(testOptions: TestOptions)(implicit logger: TestLogger): Option[FlywayConfig] = + dbConfig(testOptions).schema.map(dbSchema => FlywayConfig.defaultConfig.copy(schemas = List(dbSchema))) + + protected def dbTransactorInstance( + testOptions: TestOptions + )(implicit rt: RT, logger: TestLogger): Resource[IO, DBTransactor] + + def transactor(testOptions: TestOptions)(implicit rt: RT, logger: TestLogger): Resource[IO, DBTransactor] = + for { + _ <- logger.info(MDCKeys.testSetup(testOptions))("SETUP — init").to[Resource[IO, *]] + schema = dbConfig(testOptions).schema.getOrElse("public") + _ <- + logger + .info(MDCKeys.testSetup(testOptions) ++ Map("schema" -> schema))(s"SETUP — schema name for test: $schema") + .to[Resource[IO, *]] + _ <- _cleanDB(testOptions) + _ <- _initDB(testOptions) + fixture <- dbTransactorInstance(testOptions) + } yield fixture + + protected def _initDB(testOptions: TestOptions)(implicit rt: RT, logger: TestLogger): Resource[IO, Unit] = + for { + //just to eliminate unused warning, implementors of this class most likely to need this parameter + _ <- Resource.pure[IO, RT](rt).void + _ <- logger.info(MDCKeys.testSetup(testOptions))("SETUP — preparing DB").to[Resource[IO, *]] + t <- Resource.eval( + flyway.Flyway + .migrate[IO](dbConfig = dbConfig(testOptions), flywayConfig(testOptions)) + .timedIn() + ) + (duration, migs) = t + _ <- (migs <= 0).ifTrueRaise[Resource[IO, *]]( + InconsistentStateCatastrophe( + """ + |Number of migrations run is 0. + | + |Meaning that the migrations are not in the proper folder. + | + |Please make sure to move them to the appropriate location corresponding to + |where your test is. This is a common mistake... in Intellij it doesn't matter + |in which module the migrations are... but it matters for SBT + | + |That's why you probably didn't encounter this damn bug so soon. + |""".stripMargin + ) + ) + + _ <- logger.info(MDCKeys.testSetup(testOptions, duration))("SETUP — done preparing DB").to[Resource[IO, *]] + } yield () + + protected def _cleanDB(meta: TestOptions)(implicit rt: RT, logger: TestLogger): Resource[IO, Unit] = + for { + //just to eliminate unused warning, implementors of this class most likely to need this parameter + _ <- Resource.pure[IO, RT](rt).void + _ <- logger.info(MDCKeys.testSetup(meta))("SETUP — cleaning DB for a clean slate").to[Resource[IO, *]] + _ <- flyway.Flyway.clean[IO](dbConfig(meta)).to[Resource[IO, *]] + _ <- logger.info(MDCKeys.testSetup(meta))("SETUP — done cleaning DB").to[Resource[IO, *]] + } yield () + + /** @return + * The schema name in the format of: $${getClass.SimpleName()_$${testLineNumber}} + */ + def schemaName(testOptions: TestOptions): SchemaName = + truncateSchemaName(SchemaName(s"${schemaNameFromClassAndLineNumber(testOptions)}")) + + /** @return + * The schema name in the format of: prefix_{getClass.SimpleName()_{testLineNumber}} + */ + def schemaName(prefix: String, testOptions: TestOptions): SchemaName = + truncateSchemaName(SchemaName(s"${prefix}_${schemaNameFromClassAndLineNumber(testOptions)}")) + + protected def truncateSchemaName(s: SchemaName): SchemaName = SchemaName(s.takeRight(63)) + + protected def schemaNameFromClassAndLineNumber(meta: TestOptions): SchemaName = + SchemaName(s"${schemaNameFromClass}_${meta.location.line.toString}") + + protected def schemaNameFromClass: String = + getClass.getSimpleName.replace("$", "").toLowerCase +} diff --git a/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala new file mode 100644 index 0000000..944f1d0 --- /dev/null +++ b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala @@ -0,0 +1,162 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testkit + +import busymachines.pureharm.db._ +import busymachines.pureharm.effects._ +import busymachines.pureharm.effects.implicits._ + +/** Extend this class to get a baseline "free" test for the methods in Repo, add any additional tests here. + */ +abstract class RepoTest[E, PK, Trans](implicit show: Show[PK]) extends DBTest[Trans] { + override type ResourceType <: Repo[IO, E, PK] + + def data: RepoTestData[E, PK] + + protected def dataAssumptionCheck: Resource[IO, Unit] = Resource.eval[IO, Unit] { + IO.delay { + assume(data.pk1 != data.pk2, "Incorrect test data. primary keys have to be different for the two rows") + assume(data.nonExistentPK != data.pk1, "Incorrect test data. nonExistentPK has to be different from PK1") + assume(data.nonExistentPK != data.pk2, "Incorrect test data. nonExistentPK has to be different from PK2") + } + } + + override def testResource = ResourceFixture { testOptions => + for { + _ <- dataAssumptionCheck + trans <- setup.transactor(testOptions) + fix <- resource(testOptions, trans) + } yield fix + } + + testResource.test("find nonExistentPK -> none") { implicit repo: ResourceType => + for { + att <- repo.find(data.nonExistentPK) + } yield assert(att.isEmpty) + } + + testResource.test("retrieve nonExistentPK —> failed") { implicit repo: ResourceType => + for { + att <- repo.retrieve(data.nonExistentPK).attempt + _ = interceptFailure[DBEntryNotFoundAnomaly](att) + } yield () + } + + testResource.test("exists in empty DB -> false") { implicit repo: ResourceType => + for { + exists <- repo.exists(data.pk1) + } yield assert(!exists) + } + + testResource.test("insert row1 + find -> some") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + fetchedRow <- repo.find(data.pk1).flattenOption(fail(s"PK=${data.pk1} row was not in database")) + } yield assertEquals(obtained = fetchedRow, expected = data.row1) + } + + testResource.test("insert row1 + row -> duplicate primary key") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + fetchedRow <- repo.find(data.pk1).flattenOption(fail(s"PK=${data.pk1} row was not in database")) + _ = assertEquals(obtained = fetchedRow, expected = data.row1) + attempt <- repo.insert(data.row1).attempt + failure = interceptFailure[DBUniqueConstraintViolationAnomaly](attempt) + } yield { + assert(failure.column == data.iden.fieldName, "pk column in error") + assert(failure.value == data.pk1.show, "duplicate value in error") + } + } + + testResource.test("insert row1 + retrieve -> success") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = data.row1) + } + + testResource.test("insert row1 + exists -> true") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + exists <- repo.exists(data.pk1) + } yield assert(exists) + } + + testResource.test("insert row1 + delete + find -> none") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + _ <- repo.delete(data.pk1) + deleted <- repo.find(data.pk1) + _ = assert(deleted.isEmpty) + _ <- repo.insert(data.row1) + r <- repo.find(data.pk1) + _ = assertSome(r)(data.row1) + + } yield () + } + + testResource.test("insert row + idempotent update") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + _ <- repo.update(data.row1) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = data.row1) + } + + testResource.test("insert row1 + row1Update1") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + _ = assume(data.row1 != data.row2, "Incorrect test data. We need at least one row that's different from row1") + toUpdate = data.row1Update1 + _ <- repo.update(toUpdate) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = toUpdate) + } + + if (data.isUniqueUpdate2) { + testResource.test("insert row1 + row1Update2") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + toUpdate = data.row1Update2 + _ <- repo.update(toUpdate) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = toUpdate) + } + } + + if (data.isUniqueUpdate3) { + testResource.test("insert row1 + row1Update3") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + toUpdate = data.row1Update3 + _ <- repo.update(toUpdate) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = toUpdate) + } + } + + if (data.isUniqueUpdate4) { + testResource.test("insert row1 + row1Update4") { implicit repo: ResourceType => + for { + _ <- repo.insert(data.row1) + toUpdate = data.row1Update4 + _ <- repo.update(toUpdate) + fetchedRow <- repo.retrieve(data.pk1) + } yield assertEquals(obtained = fetchedRow, expected = toUpdate) + } + } +} diff --git a/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala new file mode 100644 index 0000000..7017852 --- /dev/null +++ b/db-testkit-ce2/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2019 BusyMachines + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package busymachines.pureharm.db.testkit + +import busymachines.pureharm.identifiable.Identifiable + +/** Basic representation of data that the user ought to provide for a full test of the busymachines.pureharm.db.Repo + * + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 + */ +trait RepoTestData[E, PK] { + + def iden: Identifiable[E, PK] + + def nonExistentPK: PK + + def row1: E + def row2: E + + def row1Update1: E + def row1Update2: E = row1Update1 + def row1Update3: E = row1Update1 + def row1Update4: E = row1Update1 + + def pk1: PK = pk(row1) + def pk2: PK = pk(row2) + + def isUniqueUpdate2: Boolean = isUniqueUpdate(row1Update2) + def isUniqueUpdate3: Boolean = isUniqueUpdate(row1Update3) + def isUniqueUpdate4: Boolean = isUniqueUpdate(row1Update4) + + private def pk(e: E): PK = iden.id(e) + + private def isUniqueUpdate(e: E): Boolean = updates.count(_ == e) == 1 + + private lazy val updates = List(row1Update1, row1Update2, row1Update3, row1Update4) + +} diff --git a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala index 1616b62..15db83e 100644 --- a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala +++ b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/DBTestSetup.scala @@ -24,20 +24,24 @@ import busymachines.pureharm.effects.implicits._ import busymachines.pureharm.testkit._ import busymachines.pureharm.testkit.util._ -/** @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 25 Jun 2020 +/** @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 */ trait DBTestSetup[DBTransactor] extends PureharmTestRuntimeLazyConversions { final type RT = PureharmTestRuntime implicit class TestSetupClassName(config: DBConnectionConfig) { - /** @see schemaName + /** @see + * schemaName */ def withSchemaFromClassAndTest(testOptions: TestOptions): DBConnectionConfig = config.copy(schema = Option(schemaName(testOptions))) - /** @see schemaName + /** @see + * schemaName */ def withSchemaFromClassAndTest(prefix: String, testOptions: TestOptions): DBConnectionConfig = config.copy(schema = Option(schemaName(prefix, testOptions))) @@ -46,8 +50,7 @@ trait DBTestSetup[DBTransactor] extends PureharmTestRuntimeLazyConversions { /** Should be overridden to create a connection config appropriate for the test * * To ensure unique schema names for test cases use the extension methods: - * TestSetupClassName.withSchemaFromClassAndTest - * or the explicit variants schemaName + * TestSetupClassName.withSchemaFromClassAndTest or the explicit variants schemaName */ def dbConfig(testOptions: TestOptions)(implicit logger: TestLogger): DBConnectionConfig @@ -73,14 +76,15 @@ trait DBTestSetup[DBTransactor] extends PureharmTestRuntimeLazyConversions { protected def _initDB(testOptions: TestOptions)(implicit rt: RT, logger: TestLogger): Resource[IO, Unit] = for { - _ <- logger.info(MDCKeys.testSetup(testOptions))("SETUP — preparing DB").to[Resource[IO, *]] - att <- flyway.Flyway + //just to eliminate unused warning, implementors of this class most likely to need this parameter + _ <- Resource.pure[IO, RT](rt).void + _ <- logger.info(MDCKeys.testSetup(testOptions))("SETUP — preparing DB").to[Resource[IO, *]] + t <- flyway.Flyway .migrate[IO](dbConfig = dbConfig(testOptions), flywayConfig(testOptions)) - .timedAttempt() - .to[Resource[IO, *]] - (duration, migsAtt) = att - migs <- migsAtt.liftTo[Resource[IO, *]] - _ <- (migs <= 0).ifTrueRaise[Resource[IO, *]]( + .timedIn() + .toResource + (duration, migs) = t + _ <- (migs <= 0).ifTrueRaise[Resource[IO, *]]( InconsistentStateCatastrophe( """ |Number of migrations run is 0. @@ -101,21 +105,21 @@ trait DBTestSetup[DBTransactor] extends PureharmTestRuntimeLazyConversions { protected def _cleanDB(meta: TestOptions)(implicit rt: RT, logger: TestLogger): Resource[IO, Unit] = for { + //just to eliminate unused warning, implementors of this class most likely to need this parameter + _ <- Resource.pure[IO, RT](rt).void _ <- logger.info(MDCKeys.testSetup(meta))("SETUP — cleaning DB for a clean slate").to[Resource[IO, *]] _ <- flyway.Flyway.clean[IO](dbConfig(meta)).to[Resource[IO, *]] _ <- logger.info(MDCKeys.testSetup(meta))("SETUP — done cleaning DB").to[Resource[IO, *]] } yield () /** @return - * The schema name in the format of: - * $${getClass.SimpleName()_$${testLineNumber}} + * The schema name in the format of: $${getClass.SimpleName()_$${testLineNumber}} */ def schemaName(testOptions: TestOptions): SchemaName = truncateSchemaName(SchemaName(s"${schemaNameFromClassAndLineNumber(testOptions)}")) /** @return - * The schema name in the format of: - * prefix_{getClass.SimpleName()_{testLineNumber}} + * The schema name in the format of: prefix_{getClass.SimpleName()_{testLineNumber}} */ def schemaName(prefix: String, testOptions: TestOptions): SchemaName = truncateSchemaName(SchemaName(s"${prefix}_${schemaNameFromClassAndLineNumber(testOptions)}")) diff --git a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala index 0f2aa3f..fbe5b0b 100644 --- a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala +++ b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTest.scala @@ -20,10 +20,8 @@ import busymachines.pureharm.db._ import busymachines.pureharm.effects._ import busymachines.pureharm.effects.implicits._ -/** @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 25 Jun 2020 +/** Extend this class to get a baseline "free" test for the methods in Repo, add any additional tests here. */ -@scala.annotation.nowarn // remove once we remove deprecated methods from Repo. abstract class RepoTest[E, PK, Trans](implicit show: Show[PK]) extends DBTest[Trans] { override type ResourceType <: Repo[IO, E, PK] @@ -122,7 +120,7 @@ abstract class RepoTest[E, PK, Trans](implicit show: Show[PK]) extends DBTest[Tr testResource.test("insert row1 + row1Update1") { implicit repo: ResourceType => for { _ <- repo.insert(data.row1) - _ = assume(data.row1 != data.row2, "Incorrect test data. We need at least one row that's different from row1") + _ = assume(data.row1 != data.row2, "Incorrect test data. We need at least one row that's different from row1") toUpdate = data.row1Update1 _ <- repo.update(toUpdate) fetchedRow <- repo.retrieve(data.pk1) @@ -162,84 +160,4 @@ abstract class RepoTest[E, PK, Trans](implicit show: Show[PK]) extends DBTest[Tr } } - testResource.test("insertMany row1, row2 + find") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - r1 <- repo.find(data.pk1) - r2 <- repo.find(data.pk2) - } yield { - assert(r1.nonEmpty, "data.row1") - assert(r2.nonEmpty, "data.row2") - } - } - - testResource.test("insertMany row1, row1 -> duplicate key on row1") { implicit repo: ResourceType => - for { - att <- repo.insertMany(List(data.row1, data.row1)).attempt - e = interceptFailure[DBBatchInsertFailedAnomaly](att) - _ = assert(Option(e.getCause).nonEmpty, "insert should have cause") - _ = interceptFailure[DBUniqueConstraintViolationAnomaly](e.getCause.asLeft) - row1 <- repo.find(data.pk1) - } yield assert(row1.isEmpty, "... on batch insert one failure, means all fail, but row1 was still added") - } - - testResource.test("insertMany row1, row2 — deleteMany row1, row2 -> no rows should exist") { - implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - _ <- repo.deleteMany(List(data.pk1, data.pk2)) - r1 <- repo.find(data.pk1) - r2 <- repo.find(data.pk2) - } yield { - assert(r1.isEmpty, "r1 should have been deleted") - assert(r2.isEmpty, "r2 should have been deleted") - } - } - - testResource.test("insertMany row1, row2 — deleteMany row1 -> row2 should exist") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - _ <- repo.deleteMany(List(data.pk1)) - r1 <- repo.find(data.pk1) - r2 <- repo.find(data.pk2) - } yield { - assert(r1.isEmpty, "r1 should have been deleted") - assert(r2.nonEmpty, "r2 should NOT have been deleted") - } - } - - testResource.test("insert row1, row2 — existsAtLeastOne row1, row2 -> true") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - exists <- repo.existsAtLeastOne(List(data.pk1)) - } yield assert(exists) - } - - testResource.test("insert row1 — existsAtLeastOne row1, row2 -> true") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row2)) - exists <- repo.existsAtLeastOne(List(data.pk1, data.pk2)) - } yield assert(exists) - } - - testResource.test("insert row1, row2 — existsAll row1, row2 -> true") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - existsAll <- repo.existAll(List(data.pk1, data.pk2)) - } yield assert(existsAll) - } - - testResource.test("insert row1, row2 — existsAll row1 -> true") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - existsAll <- repo.existAll(List(data.pk1)) - } yield assert(existsAll) - } - - testResource.test("insert row1, row2 — existsAll row1, row2, nonExistent -> false") { implicit repo: ResourceType => - for { - _ <- repo.insertMany(List(data.row1, data.row2)) - existsAll <- repo.existAll(List(data.pk1, data.pk2, data.nonExistentPK)) - } yield assert(!existsAll) - } } diff --git a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala index ce9ae64..7017852 100644 --- a/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala +++ b/db-testkit/src/main/scala/busymachines/pureharm/db/testkit/RepoTestData.scala @@ -18,11 +18,12 @@ package busymachines.pureharm.db.testkit import busymachines.pureharm.identifiable.Identifiable -/** Basic representation of data that the user ought to provide - * for a full test of the busymachines.pureharm.db.Repo +/** Basic representation of data that the user ought to provide for a full test of the busymachines.pureharm.db.Repo * - * @author Lorand Szakacs, https://github.com/lorandszakacs - * @since 25 Jun 2020 + * @author + * Lorand Szakacs, https://github.com/lorandszakacs + * @since 25 + * Jun 2020 */ trait RepoTestData[E, PK] { diff --git a/project/build.properties b/project/build.properties index 9170632..f4aa9f7 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,2 +1,2 @@ // https://github.com/sbt/sbt/releases -sbt.version=1.5.0 +sbt.version=1.5.5 diff --git a/project/plugins.sbt b/project/plugins.sbt index 94e31c0..a6cc5c5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ // format: off -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.2" ) //https://github.com/scalameta/sbt-scalafmt/releases -addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.20.4") //https://github.com/djspiewak/sbt-spiewak/releases/ -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0" ) //https://github.com/sbt/sbt-header/releases +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.3" ) // https://github.com/scalameta/sbt-scalafmt/releases +addSbtPlugin("com.codecommit" % "sbt-spiewak-sonatype" % "0.21.0") // https://github.com/djspiewak/sbt-spiewak/releases/ +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.6.0" ) // https://github.com/sbt/sbt-header/releases