From 922a6f5041dc60c9a61853a6289456f324841ebb Mon Sep 17 00:00:00 2001 From: plaflamme Date: Thu, 9 Apr 2020 18:02:23 -0400 Subject: [PATCH 1/2] Convert IntegrationSpec to use an embedded PostgreSQL database. --- build.sbt | 3 +- .../finagle/postgres/EmbeddedPgSqlSpec.scala | 64 ++ .../integration/IntegrationSpec.scala | 628 ++++++++---------- 3 files changed, 359 insertions(+), 336 deletions(-) create mode 100644 src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala diff --git a/build.sbt b/build.sbt index 90cd08df..1b9ef3c8 100644 --- a/build.sbt +++ b/build.sbt @@ -19,7 +19,8 @@ val baseSettings = Seq( "org.scalatest" %% "scalatest" % "3.0.8" % "test,it", "org.scalacheck" %% "scalacheck" % "1.14.3" % "test,it", "org.scalamock" %% "scalamock" % "4.4.0" % "test,it", - "io.circe" %% "circe-testing" % circeTestingVersion(scalaVersion.value) % "test,it" + "io.circe" %% "circe-testing" % circeTestingVersion(scalaVersion.value) % "test,it", + "com.opentable.components" % "otj-pg-embedded" % "0.13.3" % "test", ) ) diff --git a/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala b/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala new file mode 100644 index 00000000..f8cfbfc2 --- /dev/null +++ b/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala @@ -0,0 +1,64 @@ +package com.twitter.finagle.postgres + +import com.opentable.db.postgres.embedded.EmbeddedPostgres +import com.twitter.finagle.Postgres +import com.twitter.util.Try +import org.scalatest.BeforeAndAfterAll + +class EmbeddedPgSqlSpec extends Spec with BeforeAndAfterAll { + + var embeddedPgSql: Option[EmbeddedPostgres] = None + + final val TestDbUser = "postgres" + final val TestDbPassword: Option[String] = None + final val TestDbName = "finagle_postgres_test" + + def configure(b: EmbeddedPostgres.Builder): EmbeddedPostgres.Builder = b + + private[this] def using[C <: AutoCloseable, T](c: => C)(f: C => T): T = + Try(f(c)).ensure(c.close()).get + + // NOTE: this replicates what .travis.yml does. + private[this] def prep(psql: EmbeddedPostgres): EmbeddedPostgres = { + using(psql.getPostgresDatabase.getConnection()) { conn => + using(conn.createStatement()) { stmt => + stmt.execute(s"CREATE DATABASE $TestDbName") + } + } + using(psql.getDatabase(TestDbUser, TestDbName).getConnection()) { conn => + using(conn.createStatement()) { stmt => + stmt.execute("CREATE EXTENSION IF NOT EXISTS hstore") + stmt.execute("CREATE EXTENSION IF NOT EXISTS citext") + } + } + psql + } + + def getClient: PostgresClientImpl = embeddedPgSql match { + case None => sys.error("getClient invoked outside of test fragment") + case Some(pgsql) => + val client = Postgres.Client() + .withCredentials(TestDbUser, TestDbPassword) + .database(TestDbName) + .withSessionPool.maxSize(1) + // TODO +// .conditionally(useSsl, c => sslHost.fold(c.withTransport.tls)(c.withTransport.tls(_))) + .newRichClient(s"localhost:${pgsql.getPort}") + + while (!client.isAvailable) {} + client + } + + override def beforeAll(): Unit = { + val builder = + EmbeddedPostgres.builder() + .setCleanDataDirectory(true) + .setErrorRedirector(ProcessBuilder.Redirect.INHERIT) + .setOutputRedirector(ProcessBuilder.Redirect.INHERIT) + + embeddedPgSql = Some(prep(configure(builder).start())) + } + + override def afterAll(): Unit = embeddedPgSql.foreach(_.close()) + +} diff --git a/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala b/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala index 6f68f816..cf974c4d 100644 --- a/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala +++ b/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala @@ -7,440 +7,398 @@ import com.twitter.finagle.postgres._ import com.twitter.finagle.postgres.codec.ServerError import com.twitter.finagle.Postgres import com.twitter.finagle.Status -import com.twitter.util.Try import com.twitter.util.Await import com.twitter.util.Duration -import com.twitter.util.Future object IntegrationSpec { val pgTestTable = "finagle_test" } -/* - * Note: For these to work, you need to have: - * - * (1) An environment variable PG_HOST_PORT which specifies "host:port" of the test server - * (2) Environment variables PG_USER and optionally PG_PASSWORD which specify the username and password to the server - * (3) An environment variable PG_DBNAME which specifies the test database - * - * If these are conditions are met, the integration tests will be run. - * - * The tests can be run with SSL by also setting the USE_PG_SSL variable to "1", and hostname verification can be added - * by setting PG_SSL_HOST. - * - */ -class IntegrationSpec extends Spec { - - for { - hostPort <- sys.env.get("PG_HOST_PORT") - user <- sys.env.get("PG_USER") - password = sys.env.get("PG_PASSWORD") - dbname <- sys.env.get("PG_DBNAME") - useSsl = sys.env.getOrElse("USE_PG_SSL", "0") == "1" - sslHost = sys.env.get("PG_SSL_HOST") - } yield { - - - val queryTimeout = Duration.fromSeconds(2) - - def getClient: PostgresClientImpl = { - val client = Postgres.Client() - .withCredentials(user, password) - .database(dbname) - .withSessionPool.maxSize(1) - .conditionally(useSsl, c => sslHost.fold(c.withTransport.tls)(c.withTransport.tls(_))) - .newRichClient(hostPort) - - Await.result(Future[PostgresClientImpl] { - while (!client.isAvailable) {} - client - }) - } +class IntegrationSpec extends EmbeddedPgSqlSpec { - def getBadClient = { - Postgres.Client() - .withCredentials(user, password) - .database(dbname) - .withSessionPool.maxSize(1) - .conditionally(useSsl, c => sslHost.fold(c.withTransport.tls)(c.withTransport.tls(_))) - .newRichClient("badhost:5432") - } + val queryTimeout = Duration.fromSeconds(2) - def cleanDb(client: PostgresClient): Unit = { - val dropQuery = client.executeUpdate("DROP TABLE IF EXISTS %s".format(IntegrationSpec.pgTestTable)) - val response = Await.result(dropQuery, queryTimeout) + def getBadClient = { + Postgres.Client() + .withCredentials(TestDbUser, TestDbPassword) + .database(TestDbName) + .withSessionPool.maxSize(1) + // TODO +// .conditionally(useSsl, c => sslHost.fold(c.withTransport.tls)(c.withTransport.tls(_))) + .newRichClient("badhost:5432") + } - response must equal(OK(1)) + def cleanDb(client: PostgresClient): Unit = { + val dropQuery = client.executeUpdate("DROP TABLE IF EXISTS %s".format(IntegrationSpec.pgTestTable)) + val response = Await.result(dropQuery, queryTimeout) + + response must equal(OK(1)) + + val createTableQuery = client.executeUpdate( + """ + |CREATE TABLE %s ( + | str_field VARCHAR(40), + | int_field INT, + | double_field DOUBLE PRECISION, + | timestamp_field TIMESTAMP WITH TIME ZONE, + | bool_field BOOLEAN + |) + """.stripMargin.format(IntegrationSpec.pgTestTable)) + val response2 = Await.result(createTableQuery, queryTimeout) + response2 must equal(OK(1)) + } + + def insertSampleData(client: PostgresClient): Unit = { + val insertDataQuery = client.executeUpdate( + """ + |INSERT INTO %s VALUES + | ('hello', 1234, 10.5, '2015-01-08 11:55:12-0800', TRUE), + | ('hello', 5557, -4.51, '2015-01-08 12:55:12-0800', TRUE), + | ('hello', 7787, -42.51, '2013-12-24 07:01:00-0800', FALSE), + | ('goodbye', 4567, 15.8, '2015-01-09 16:55:12+0500', FALSE) + """.stripMargin.format(IntegrationSpec.pgTestTable)) + + val response = Await.result(insertDataQuery, queryTimeout) + + response must equal(OK(4)) + } + + "A postgres client" should { + "insert and select rows" in { + val client = getClient + cleanDb(client) + insertSampleData(client) + + val selectQuery = client.select( + "SELECT * FROM %s WHERE str_field='hello' ORDER BY timestamp_field".format(IntegrationSpec.pgTestTable) + )( + identity) + + val resultRows = Await.result(selectQuery, queryTimeout) + + resultRows.size must equal(3) + + // Spot check the first row + val firstRow = resultRows.head + + firstRow.getOption[String]("str_field") must equal(Some("hello")) + firstRow.getOption[Int]("int_field") must equal(Some(7787)) + firstRow.getOption[Double]("double_field") must equal(Some(-42.51)) + firstRow.getOption[Instant]("timestamp_field") must equal(Some( + new Timestamp(1387897260000L).toInstant + )) + firstRow.getOption[Boolean]("bool_field") must equal(Some(false)) + firstRow.getOption[String]("bad_column") must equal(None) - val createTableQuery = client.executeUpdate( - """ - |CREATE TABLE %s ( - | str_field VARCHAR(40), - | int_field INT, - | double_field DOUBLE PRECISION, - | timestamp_field TIMESTAMP WITH TIME ZONE, - | bool_field BOOLEAN - |) - """.stripMargin.format(IntegrationSpec.pgTestTable)) - val response2 = Await.result(createTableQuery, queryTimeout) - response2 must equal(OK(1)) - - Try(Await.result(client.execute("create extension hstore"))) // enable hstore type } - def insertSampleData(client: PostgresClient): Unit = { - val insertDataQuery = client.executeUpdate( - """ - |INSERT INTO %s VALUES - | ('hello', 1234, 10.5, '2015-01-08 11:55:12-0800', TRUE), - | ('hello', 5557, -4.51, '2015-01-08 12:55:12-0800', TRUE), - | ('hello', 7787, -42.51, '2013-12-24 07:01:00-0800', FALSE), - | ('goodbye', 4567, 15.8, '2015-01-09 16:55:12+0500', FALSE) - """.stripMargin.format(IntegrationSpec.pgTestTable)) + "execute a select that returns nothing" in { + val client = getClient + cleanDb(client) + + insertSampleData(client) + + val + selectQuery = client.select( + "SELECT * FROM %s WHERE str_field='xxxx' ORDER BY timestamp_field". + format( - val response = Await.result(insertDataQuery, queryTimeout) + IntegrationSpec.pgTestTable) + )(identity) - response must equal(OK(4)) + val + + resultRows = Await.result( + selectQuery, queryTimeout) + resultRows.size must equal(0) } - "A postgres client" should { - "insert and select rows" in { - val client = getClient - cleanDb(client) - insertSampleData(client) - val selectQuery = client.select( - "SELECT * FROM %s WHERE str_field='hello' ORDER BY timestamp_field".format(IntegrationSpec.pgTestTable) - )( - identity) + "update a row" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - val resultRows = Await.result(selectQuery, queryTimeout) + val updateQuery = client.executeUpdate( - resultRows.size must equal(3) + "UPDATE %s SET str_field='hello_updated' where int_field=4567".format(IntegrationSpec.pgTestTable) + ) - // Spot check the first row - val firstRow = resultRows.head + val response = Await. - firstRow.getOption[String]("str_field") must equal(Some("hello")) - firstRow.getOption[Int]("int_field") must equal(Some(7787)) - firstRow.getOption[Double]("double_field") must equal(Some(-42.51)) - firstRow.getOption[Instant]("timestamp_field") must equal(Some( - new Timestamp(1387897260000L).toInstant - )) - firstRow.getOption[Boolean]("bool_field") must equal(Some(false)) - firstRow.getOption[String]("bad_column") must equal(None) + result(updateQuery, queryTimeout) - } + response must equal(OK(1)) - "execute a select that returns nothing" in { - val client = getClient - cleanDb(client) + val selectQuery = client.select( - insertSampleData(client) + "SELECT * FROM %s WHERE str_field='hello_updated'".format(IntegrationSpec.pgTestTable) + )( - val - selectQuery = client.select( - "SELECT * FROM %s WHERE str_field='xxxx' ORDER BY timestamp_field". - format( + identity) - IntegrationSpec.pgTestTable) - )(identity) + val + resultRows = Await.result(selectQuery, queryTimeout) - val + resultRows.size must equal(1) + resultRows.head.getOption[String]("str_field") must equal(Some("hello_updated")) + } - resultRows = Await.result( - selectQuery, queryTimeout) - resultRows.size must equal(0) - } + "delete rows" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - "update a row" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val updateQuery = client.executeUpdate( + "DELETE FROM %s WHERE str_field='hello'" + .format(IntegrationSpec.pgTestTable) + ) - val updateQuery = client.executeUpdate( + val response = Await.result(updateQuery, queryTimeout) - "UPDATE %s SET str_field='hello_updated' where int_field=4567".format(IntegrationSpec.pgTestTable) - ) + response must equal(OK(3)) - val response = Await. + val selectQuery = client.select( + "SELECT * FROM %s".format(IntegrationSpec.pgTestTable) + )(identity) - result(updateQuery, queryTimeout) + val resultRows = Await.result(selectQuery, queryTimeout) - response must equal(OK(1)) + resultRows.size must equal (1) + resultRows.head.getOption[String]("str_field") must equal(Some("goodbye")) + } - val selectQuery = client.select( - "SELECT * FROM %s WHERE str_field='hello_updated'".format(IntegrationSpec.pgTestTable) - )( + "select rows via a prepared query" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - identity) + val preparedQuery = client.prepareAndQuery( + "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), + Param("hello"), + Param(true))(identity) - val - resultRows = Await.result(selectQuery, queryTimeout) + val resultRows = Await.result( + preparedQuery, + queryTimeout + ) - resultRows.size must equal(1) - resultRows.head.getOption[String]("str_field") must equal(Some("hello_updated")) + resultRows.size must equal(2) + resultRows.foreach { + row => + row.getOption[String]("str_field") must equal(Some("hello")) + row.getOption[Boolean]("bool_field") must equal(Some(true)) } + } + "execute an update via a prepared statement" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - "delete rows" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val preparedQuery = client.prepareAndExecute( + "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + Param("hello_updated") + ) - val updateQuery = client.executeUpdate( - "DELETE FROM %s WHERE str_field='hello'" - .format(IntegrationSpec.pgTestTable) - ) + val numRows = Await.result(preparedQuery) - val response = Await.result(updateQuery, queryTimeout) + val resultRows = Await.result(client.select( + "SELECT * from %s WHERE str_field = 'hello_updated' AND int_field = 4567".format(IntegrationSpec.pgTestTable) + )(identity)) - response must equal(OK(3)) + resultRows.size must equal(numRows) + } - val selectQuery = client.select( - "SELECT * FROM %s".format(IntegrationSpec.pgTestTable) - )(identity) + "execute an update via a prepared statement using a Some(value)" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - val resultRows = Await.result(selectQuery, queryTimeout) - resultRows.size must equal (1) - resultRows.head.getOption[String]("str_field") must equal(Some("goodbye")) - } + val preparedQuery = client.prepareAndExecute( + "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + Some("hello_updated_some") + ) + val numRows = Await.result(preparedQuery) - "select rows via a prepared query" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val resultRows = Await.result(client.select( + "SELECT * from %s WHERE str_field = 'hello_updated_some' AND int_field = 4567".format(IntegrationSpec.pgTestTable) + )(identity)) - val preparedQuery = client.prepareAndQuery( - "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), - Param("hello"), - Param(true))(identity) - - val resultRows = Await.result( - preparedQuery, - queryTimeout - ) - - resultRows.size must equal(2) - resultRows.foreach { - row => - row.getOption[String]("str_field") must equal(Some("hello")) - row.getOption[Boolean]("bool_field") must equal(Some(true)) - } - } + resultRows.size must equal(numRows) + } - "execute an update via a prepared statement" in { - val client = getClient - cleanDb(client) - insertSampleData(client) - - val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), - Param("hello_updated") - ) - - val numRows = Await.result(preparedQuery) - - val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field = 'hello_updated' AND int_field = 4567".format(IntegrationSpec.pgTestTable) - )(identity)) - - resultRows.size must equal(numRows) - } + "execute an update via a prepared statement using a None" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - "execute an update via a prepared statement using a Some(value)" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val preparedQuery = client.prepareAndExecute( + "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + None: Option[String] + ) - val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), - Some("hello_updated_some") - ) + val numRows = Await.result(preparedQuery) - val numRows = Await.result(preparedQuery) + val resultRows = Await.result(client.select( + "SELECT * from %s WHERE str_field IS NULL AND int_field = 4567".format(IntegrationSpec.pgTestTable) + )(identity)) - val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field = 'hello_updated_some' AND int_field = 4567".format(IntegrationSpec.pgTestTable) - )(identity)) + resultRows.size must equal(numRows) + } - resultRows.size must equal(numRows) - } + "return rows from UPDATE...RETURNING" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - "execute an update via a prepared statement using a None" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val preparedQuery = client.prepareAndQuery( + "UPDATE %s SET str_field = $1 where int_field = 4567 RETURNING *".format(IntegrationSpec.pgTestTable), + Param("hello_updated") + )(identity) - val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), - None: Option[String] - ) + val resultRows = Await.result(preparedQuery) - val numRows = Await.result(preparedQuery) + resultRows.size must equal(1) + resultRows.head.get[String]("str_field") must equal("hello_updated") + } - val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field IS NULL AND int_field = 4567".format(IntegrationSpec.pgTestTable) - )(identity)) + "return rows from DELETE...RETURNING" in { + val client = getClient + cleanDb(client) + insertSampleData(client) - resultRows.size must equal(numRows) - } + Await.result(client.prepareAndExecute( + s"""INSERT INTO ${IntegrationSpec.pgTestTable} + VALUES ('delete', 9012, 15.8, '2015-01-09 16:55:12+0500', FALSE)""" + )) - "return rows from UPDATE...RETURNING" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val preparedQuery = client.prepareAndQuery ( + "DELETE FROM %s where int_field = 9012 RETURNING *".format(IntegrationSpec.pgTestTable) + )(identity) + val resultRows = Await.result(preparedQuery) - val preparedQuery = client.prepareAndQuery( - "UPDATE %s SET str_field = $1 where int_field = 4567 RETURNING *".format(IntegrationSpec.pgTestTable), - Param("hello_updated") - )(identity) + resultRows.size must equal(1) + resultRows.head.get[String]("str_field") must equal("delete") + } - val resultRows = Await.result(preparedQuery) - resultRows.size must equal(1) - resultRows.head.get[String]("str_field") must equal("hello_updated") - } + "execute an UPDATE...RETURNING that updates nothing" in { + val client = getClient + cleanDb(client) + insertSampleData(client) + val preparedQuery = client.prepareAndQuery( + "UPDATE %s SET str_field = $1 where str_field = $2 RETURNING *".format(IntegrationSpec.pgTestTable), + Param("hello_updated"), + Param("xxxx") + )(identity) - "return rows from DELETE...RETURNING" in { - val client = getClient - cleanDb(client) - insertSampleData(client) + val resultRows = Await.result(preparedQuery) - Await.result(client.prepareAndExecute( - s"""INSERT INTO ${IntegrationSpec.pgTestTable} - VALUES ('delete', 9012, 15.8, '2015-01-09 16:55:12+0500', FALSE)""" - )) + resultRows.size must equal(0) + } - val preparedQuery = client.prepareAndQuery ( - "DELETE FROM %s where int_field = 9012 RETURNING *".format(IntegrationSpec.pgTestTable) - )(identity) - - val resultRows = Await.result(preparedQuery) + "execute a DELETE...RETURNING that deletes nothing" in { - resultRows.size must equal(1) - resultRows.head.get[String]("str_field") must equal("delete") - } + val client = getClient + cleanDb(client) + insertSampleData(client) + val preparedQuery = client.prepareAndQuery( + "DELETE FROM %s WHERE str_field=$1".format(IntegrationSpec.pgTestTable), + Param("xxxx") + )(identity) - "execute an UPDATE...RETURNING that updates nothing" in { - val client = getClient - cleanDb(client) - insertSampleData(client) - val preparedQuery = client.prepareAndQuery( - "UPDATE %s SET str_field = $1 where str_field = $2 RETURNING *".format(IntegrationSpec.pgTestTable), - Param("hello_updated"), - Param("xxxx") - )(identity) - - val resultRows = Await.result(preparedQuery) + val resultRows = Await.result(preparedQuery) + resultRows.size must equal(0) + } - resultRows.size must equal(0) - } + // this test will fail if the test DB user doesn't have permission + "create an extension using CREATE EXTENSION" in { + assume(TestDbUser == "postgres") + + val client = getClient + val result = client.prepareAndExecute("CREATE EXTENSION IF NOT EXISTS hstore") + Await.result(result) + } + + "support multi-statement DDL" in { + val client = getClient + val result = client.query(""" + |CREATE TABLE multi_one(id integer); + |CREATE TABLE multi_two(id integer); + |DROP TABLE multi_one; + |DROP TABLE multi_two; + """.stripMargin) + Await.result(result) + } - "execute a DELETE...RETURNING that deletes nothing" in { + "throw a ServerError" when { + "query has error" in { val client = getClient cleanDb(client) - insertSampleData(client) - val preparedQuery = client.prepareAndQuery( - "DELETE FROM %s WHERE str_field=$1".format(IntegrationSpec.pgTestTable), - Param("xxxx") + val selectQuery = client.select( + "SELECT * FROM %s WHERE unknown_column='hello_updated'".format(IntegrationSpec.pgTestTable) )(identity) - - val resultRows = Await.result(preparedQuery) - resultRows.size must equal(0) - } - // this test will fail if the test DB user doesn't have permission - "create an extension using CREATE EXTENSION" in { - if(user == "postgres") { - val client = getClient - val result = client.prepareAndExecute("CREATE EXTENSION IF NOT EXISTS hstore") - Await.result(result) + a[ServerError] must be thrownBy { + Await.result(selectQuery, queryTimeout) } } - "support multi-statement DDL" in { + "query in a prepared statement has an error" in { val client = getClient - val result = client.query(""" - |CREATE TABLE multi_one(id integer); - |CREATE TABLE multi_two(id integer); - |DROP TABLE multi_one; - |DROP TABLE multi_two; - """.stripMargin) - Await.result(result) + a [ServerError] must be thrownBy { + Await.result(client.prepareAndQuery("Garbage query")(identity)) + } } + "prepared query is missing parameters" in { - "throw a ServerError" when { - "query has error" in { - val client = getClient - cleanDb(client) - - val selectQuery = client.select( - "SELECT * FROM %s WHERE unknown_column='hello_updated'".format(IntegrationSpec.pgTestTable) - )(identity) + val client = getClient + cleanDb(client) - a[ServerError] must be thrownBy { - Await.result(selectQuery, queryTimeout) - } - } + val preparedQuery = client.prepareAndQuery( + "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), + Param("hello") + )(identity) - "query in a prepared statement has an error" in { - val client = getClient - a [ServerError] must be thrownBy { - Await.result(client.prepareAndQuery("Garbage query")(identity)) - } + a [ServerError] must be thrownBy { + Await.result( + preparedQuery, + queryTimeout + ) } - "prepared query is missing parameters" in { - - val client = getClient - cleanDb(client) - - val preparedQuery = client.prepareAndQuery( - "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), - Param("hello") - )(identity) - - a [ServerError] must be thrownBy { - Await.result( - preparedQuery, - queryTimeout - ) - } - - } } + } - "return correct availability information" when { - "client is good" in { - val client: PostgresClient = getClient - client.isAvailable must equal(true) - client.status must equal(Status.Open) - } - "client is bad" in { - val badClient: PostgresClient = getBadClient - badClient.isAvailable must equal(false) - Set(Status.Busy, Status.Closed) must contain (badClient.status) - } - "client is closed" in { - val client: PostgresClient = getClient - client.close() - client.isAvailable must equal(false) - client.status must equal(Status.Closed) - } + "return correct availability information" when { + "client is good" in { + val client: PostgresClient = getClient + client.isAvailable must equal(true) + client.status must equal(Status.Open) + } + "client is bad" in { + val badClient: PostgresClient = getBadClient + badClient.isAvailable must equal(false) + Set(Status.Busy, Status.Closed) must contain (badClient.status) + } + "client is closed" in { + val client: PostgresClient = getClient + client.close() + client.isAvailable must equal(false) + client.status must equal(Status.Closed) } } - } } From 796c1b1dc2d86cc0b97ff283dde53f46c2ece158 Mon Sep 17 00:00:00 2001 From: plaflamme Date: Fri, 10 Apr 2020 15:12:14 -0400 Subject: [PATCH 2/2] Use forked version to enable testing against multiple postgresql versions. --- build.sbt | 62 ++++++++++++++++++- .../finagle/postgres/EmbeddedPgSqlSpec.scala | 4 +- .../finagle/postgres}/IntegrationSpec.scala | 61 +++++++++--------- 3 files changed, 90 insertions(+), 37 deletions(-) rename {src => finagle-postgres-integration/src}/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala (93%) rename {src/test/scala/com/twitter/finagle/postgres/integration => finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres}/IntegrationSpec.scala (87%) diff --git a/build.sbt b/build.sbt index 1b9ef3c8..dd81f8e0 100644 --- a/build.sbt +++ b/build.sbt @@ -19,8 +19,7 @@ val baseSettings = Seq( "org.scalatest" %% "scalatest" % "3.0.8" % "test,it", "org.scalacheck" %% "scalacheck" % "1.14.3" % "test,it", "org.scalamock" %% "scalamock" % "4.4.0" % "test,it", - "io.circe" %% "circe-testing" % circeTestingVersion(scalaVersion.value) % "test,it", - "com.opentable.components" % "otj-pg-embedded" % "0.13.3" % "test", + "io.circe" %% "circe-testing" % circeTestingVersion(scalaVersion.value) % "test,it" ) ) @@ -80,13 +79,70 @@ lazy val publishSettings = Seq( lazy val allSettings = baseSettings ++ buildSettings ++ publishSettings lazy val shapelessRef = LocalProject("finagle-postgres-shapeless") +lazy val integrationTestRef = LocalProject("finagle-postgres-integration") lazy val `finagle-postgres` = project .in(file(".")) .settings(moduleName := "finagle-postgres") .settings(allSettings) .configs(IntegrationTest) - .aggregate(shapelessRef) + .aggregate( + shapelessRef, + integrationTestRef + ) + +lazy val `finagle-postgres-integration` = project + .settings(moduleName := "finagle-postgres-integration") + .settings(allSettings) + .configs(IntegrationTest) + .settings( + libraryDependencies ++= Seq( + "io.zonky.test" % "embedded-postgres" % "1.2.6" % "test" + ) + ) + .dependsOn(`finagle-postgres` % "test->test") + .aggregate( + test9, + test10, + test11 + ) + +lazy val test9 = integrationTests("9.6.17") +lazy val test10 = integrationTests("10.11.0") // 10.12.0 fails to find libz for some reason +lazy val test11 = integrationTests("11.6.0") + +def integrationTests(v: String) = { + val majorVersion = v.split('.') match { + case Array(major, _, _) => major + case _ => sys.error(s"unexpected version number. Expected major.minor.patch, got $v") + } + val id = s"finagle-postgres-test-$majorVersion" + Project(id = id, base = file(id)) + .settings(baseSettings ++ buildSettings) // don't publish + .settings( + libraryDependencies ++= Seq( + // TODO + "io.zonky.test.postgres" % "embedded-postgres-binaries-darwin-amd64" % v % "test", + ) + ) + .settings( + parallelExecution in Test := false, + javaOptions in Test += "-Duser.timezone=UTC" // TODO: investigate and submit a test to demonstrate that timezone handling is broken. + ) + .settings( + Test / sourceGenerators += Def.task { + val file = (Test / sourceManaged).value / "com" / "twitter" / "finagle" / "postgres" / "IntegrationSpec.scala" + IO.write(file, + s"""package com.twitter.finagle.postgres + | + |class IntegrationSpec extends BaseIntegrationSpec("$v") + |""".stripMargin) + Seq(file) + }.taskValue + ) + .configs(IntegrationTest) + .dependsOn(integrationTestRef % "compile->compile;test->test") +} lazy val `finagle-postgres-shapeless` = project .settings(moduleName := "finagle-postgres-shapeless") diff --git a/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala b/finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala similarity index 93% rename from src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala rename to finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala index f8cfbfc2..14959d39 100644 --- a/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala +++ b/finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/EmbeddedPgSqlSpec.scala @@ -1,11 +1,11 @@ package com.twitter.finagle.postgres -import com.opentable.db.postgres.embedded.EmbeddedPostgres import com.twitter.finagle.Postgres import com.twitter.util.Try +import io.zonky.test.db.postgres.embedded.EmbeddedPostgres import org.scalatest.BeforeAndAfterAll -class EmbeddedPgSqlSpec extends Spec with BeforeAndAfterAll { +abstract class EmbeddedPgSqlSpec extends Spec with BeforeAndAfterAll { var embeddedPgSql: Option[EmbeddedPostgres] = None diff --git a/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala b/finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/IntegrationSpec.scala similarity index 87% rename from src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala rename to finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/IntegrationSpec.scala index cf974c4d..def52983 100644 --- a/src/test/scala/com/twitter/finagle/postgres/integration/IntegrationSpec.scala +++ b/finagle-postgres-integration/src/test/scala/com/twitter/finagle/postgres/IntegrationSpec.scala @@ -1,20 +1,17 @@ -package com.twitter.finagle.postgres.integration +package com.twitter.finagle.postgres import java.sql.Timestamp import java.time.Instant -import com.twitter.finagle.postgres._ +import com.twitter.finagle.{Postgres, Status} import com.twitter.finagle.postgres.codec.ServerError -import com.twitter.finagle.Postgres -import com.twitter.finagle.Status -import com.twitter.util.Await -import com.twitter.util.Duration +import com.twitter.util.{Await, Duration} -object IntegrationSpec { +object BaseIntegrationSpec { val pgTestTable = "finagle_test" } -class IntegrationSpec extends EmbeddedPgSqlSpec { +abstract class BaseIntegrationSpec(version: String) extends EmbeddedPgSqlSpec { val queryTimeout = Duration.fromSeconds(2) @@ -29,7 +26,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { } def cleanDb(client: PostgresClient): Unit = { - val dropQuery = client.executeUpdate("DROP TABLE IF EXISTS %s".format(IntegrationSpec.pgTestTable)) + val dropQuery = client.executeUpdate("DROP TABLE IF EXISTS %s".format(BaseIntegrationSpec.pgTestTable)) val response = Await.result(dropQuery, queryTimeout) response must equal(OK(1)) @@ -43,7 +40,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { | timestamp_field TIMESTAMP WITH TIME ZONE, | bool_field BOOLEAN |) - """.stripMargin.format(IntegrationSpec.pgTestTable)) + """.stripMargin.format(BaseIntegrationSpec.pgTestTable)) val response2 = Await.result(createTableQuery, queryTimeout) response2 must equal(OK(1)) } @@ -56,21 +53,21 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { | ('hello', 5557, -4.51, '2015-01-08 12:55:12-0800', TRUE), | ('hello', 7787, -42.51, '2013-12-24 07:01:00-0800', FALSE), | ('goodbye', 4567, 15.8, '2015-01-09 16:55:12+0500', FALSE) - """.stripMargin.format(IntegrationSpec.pgTestTable)) + """.stripMargin.format(BaseIntegrationSpec.pgTestTable)) val response = Await.result(insertDataQuery, queryTimeout) response must equal(OK(4)) } - "A postgres client" should { + s"A postgres client against Postgresql v$version" should { "insert and select rows" in { val client = getClient cleanDb(client) insertSampleData(client) val selectQuery = client.select( - "SELECT * FROM %s WHERE str_field='hello' ORDER BY timestamp_field".format(IntegrationSpec.pgTestTable) + "SELECT * FROM %s WHERE str_field='hello' ORDER BY timestamp_field".format(BaseIntegrationSpec.pgTestTable) )( identity) @@ -103,7 +100,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { "SELECT * FROM %s WHERE str_field='xxxx' ORDER BY timestamp_field". format( - IntegrationSpec.pgTestTable) + BaseIntegrationSpec.pgTestTable) )(identity) val @@ -121,7 +118,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val updateQuery = client.executeUpdate( - "UPDATE %s SET str_field='hello_updated' where int_field=4567".format(IntegrationSpec.pgTestTable) + "UPDATE %s SET str_field='hello_updated' where int_field=4567".format(BaseIntegrationSpec.pgTestTable) ) val response = Await. @@ -132,7 +129,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val selectQuery = client.select( - "SELECT * FROM %s WHERE str_field='hello_updated'".format(IntegrationSpec.pgTestTable) + "SELECT * FROM %s WHERE str_field='hello_updated'".format(BaseIntegrationSpec.pgTestTable) )( identity) @@ -152,7 +149,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val updateQuery = client.executeUpdate( "DELETE FROM %s WHERE str_field='hello'" - .format(IntegrationSpec.pgTestTable) + .format(BaseIntegrationSpec.pgTestTable) ) val response = Await.result(updateQuery, queryTimeout) @@ -160,7 +157,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { response must equal(OK(3)) val selectQuery = client.select( - "SELECT * FROM %s".format(IntegrationSpec.pgTestTable) + "SELECT * FROM %s".format(BaseIntegrationSpec.pgTestTable) )(identity) val resultRows = Await.result(selectQuery, queryTimeout) @@ -176,7 +173,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { insertSampleData(client) val preparedQuery = client.prepareAndQuery( - "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), + "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(BaseIntegrationSpec.pgTestTable), Param("hello"), Param(true))(identity) @@ -199,14 +196,14 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { insertSampleData(client) val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + "UPDATE %s SET str_field = $1 where int_field = 4567".format(BaseIntegrationSpec.pgTestTable), Param("hello_updated") ) val numRows = Await.result(preparedQuery) val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field = 'hello_updated' AND int_field = 4567".format(IntegrationSpec.pgTestTable) + "SELECT * from %s WHERE str_field = 'hello_updated' AND int_field = 4567".format(BaseIntegrationSpec.pgTestTable) )(identity)) resultRows.size must equal(numRows) @@ -219,14 +216,14 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + "UPDATE %s SET str_field = $1 where int_field = 4567".format(BaseIntegrationSpec.pgTestTable), Some("hello_updated_some") ) val numRows = Await.result(preparedQuery) val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field = 'hello_updated_some' AND int_field = 4567".format(IntegrationSpec.pgTestTable) + "SELECT * from %s WHERE str_field = 'hello_updated_some' AND int_field = 4567".format(BaseIntegrationSpec.pgTestTable) )(identity)) resultRows.size must equal(numRows) @@ -239,14 +236,14 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val preparedQuery = client.prepareAndExecute( - "UPDATE %s SET str_field = $1 where int_field = 4567".format(IntegrationSpec.pgTestTable), + "UPDATE %s SET str_field = $1 where int_field = 4567".format(BaseIntegrationSpec.pgTestTable), None: Option[String] ) val numRows = Await.result(preparedQuery) val resultRows = Await.result(client.select( - "SELECT * from %s WHERE str_field IS NULL AND int_field = 4567".format(IntegrationSpec.pgTestTable) + "SELECT * from %s WHERE str_field IS NULL AND int_field = 4567".format(BaseIntegrationSpec.pgTestTable) )(identity)) resultRows.size must equal(numRows) @@ -259,7 +256,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { val preparedQuery = client.prepareAndQuery( - "UPDATE %s SET str_field = $1 where int_field = 4567 RETURNING *".format(IntegrationSpec.pgTestTable), + "UPDATE %s SET str_field = $1 where int_field = 4567 RETURNING *".format(BaseIntegrationSpec.pgTestTable), Param("hello_updated") )(identity) @@ -275,12 +272,12 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { insertSampleData(client) Await.result(client.prepareAndExecute( - s"""INSERT INTO ${IntegrationSpec.pgTestTable} + s"""INSERT INTO ${BaseIntegrationSpec.pgTestTable} VALUES ('delete', 9012, 15.8, '2015-01-09 16:55:12+0500', FALSE)""" )) val preparedQuery = client.prepareAndQuery ( - "DELETE FROM %s where int_field = 9012 RETURNING *".format(IntegrationSpec.pgTestTable) + "DELETE FROM %s where int_field = 9012 RETURNING *".format(BaseIntegrationSpec.pgTestTable) )(identity) val resultRows = Await.result(preparedQuery) @@ -295,7 +292,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { cleanDb(client) insertSampleData(client) val preparedQuery = client.prepareAndQuery( - "UPDATE %s SET str_field = $1 where str_field = $2 RETURNING *".format(IntegrationSpec.pgTestTable), + "UPDATE %s SET str_field = $1 where str_field = $2 RETURNING *".format(BaseIntegrationSpec.pgTestTable), Param("hello_updated"), Param("xxxx") )(identity) @@ -312,7 +309,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { insertSampleData(client) val preparedQuery = client.prepareAndQuery( - "DELETE FROM %s WHERE str_field=$1".format(IntegrationSpec.pgTestTable), + "DELETE FROM %s WHERE str_field=$1".format(BaseIntegrationSpec.pgTestTable), Param("xxxx") )(identity) @@ -347,7 +344,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { cleanDb(client) val selectQuery = client.select( - "SELECT * FROM %s WHERE unknown_column='hello_updated'".format(IntegrationSpec.pgTestTable) + "SELECT * FROM %s WHERE unknown_column='hello_updated'".format(BaseIntegrationSpec.pgTestTable) )(identity) a[ServerError] must be thrownBy { @@ -368,7 +365,7 @@ class IntegrationSpec extends EmbeddedPgSqlSpec { cleanDb(client) val preparedQuery = client.prepareAndQuery( - "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(IntegrationSpec.pgTestTable), + "SELECT * FROM %s WHERE str_field=$1 AND bool_field=$2".format(BaseIntegrationSpec.pgTestTable), Param("hello") )(identity)