This is an automated email from the ASF dual-hosted git repository.
github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git
The following commit(s) were added to refs/heads/main by this push:
new 990b7cb753 test(workflow-operator): pin ConnUtil URL composition
without DriverManager (#5728)
990b7cb753 is described below
commit 990b7cb75330ea64efd76af26046368dce4900a3
Author: Matthew B. <[email protected]>
AuthorDate: Tue Jun 16 09:51:36 2026 -0700
test(workflow-operator): pin ConnUtil URL composition without DriverManager
(#5728)
### What changes were proposed in this PR?
- `PostgreSQLConnUtilSpec` / `MySQLConnUtilSpec` were flaky: they ran a
multi-step sequence against the process-global `java.sql.DriverManager`
registry (deregister the real driver, register a capturing stub,
restore), and the module's tests run un-forked with sbt's default
`parallelExecution = true`, so the suites raced any concurrent suite
that touched JDBC in the same JVM.
- Extracted the JDBC URL composition (the only application-controlled
logic) into a pure `buildJdbcUrl(host, port, database)` in
`PostgreSQLConnUtil` and `MySQLConnUtil`; the existing `connect` now
calls it, so the public signature and runtime behavior are unchanged.
- Rewrote both specs to assert URL composition directly on
`buildJdbcUrl`, with no `DriverManager` mutation and no
`BeforeAndAfterAll`, so the suites are deterministic under parallel
execution.
- Dropped the former `setReadOnly` / credential / `SQLException`
assertions: those exercise JDBC plumbing rather than application logic
and were what forced the global-registry manipulation behind the flake.
### Trade-off: the dropped assertions
The previous specs reached `connect` by mutating the global
`DriverManager`; that seam, not the assertions, is what caused the race.
The three dropped behaviors break down as:
| Behavior | Regression risk | Notes |
| --- | --- | --- |
| `buildJdbcUrl` composition | Meaningful (kept) | The only logic with
string composition / ordering that can plausibly regress. |
| `setReadOnly(true)` | Low | A single constant line with no inputs, but
a genuine read-only/efficiency contract. |
| Credential propagation | Negligible | `connect` forwards
`username`/`password` straight to `DriverManager`; a test mostly asserts
the injected seam, not app logic. |
| `SQLException` propagation | Negligible | No `try`/`catch` in
`connect`; the test pins "Scala lets exceptions bubble." |
These three could be restored *without* reintroducing the race, via a
dependency-injection seam (a package-private `openConnection(url, user,
pass, opener = DriverManager.getConnection)` that tests call with a fake
opener plus a `Proxy`-backed `Connection`): no global registry mutation,
so nothing for parallel suites to race on.
> [!NOTE]
> Deliberately did **not** add the DI seam here. For these utilities,
the seam would be permanent production complexity in service of tests
that mostly verify the mock.
### Any related issues, documentation, discussions?
Closes: #5727
### How was this PR tested?
- Run `sbt "WorkflowOperator/testOnly *PostgreSQLConnUtilSpec
*MySQLConnUtilSpec"` (under JDK 17); expect 12 tests succeeded, 0 failed
across 2 suites.
- Run `sbt scalafmtCheckAll`; expect success (no formatting diffs).
- The original race was timing-dependent and not reproducible on demand;
the fix removes the shared `DriverManager` state entirely, so there is
no longer cross-suite state to race. Reviewers can confirm by grepping
the two specs for `DriverManager` and `BeforeAndAfterAll`, both now
absent.
### Was this PR authored or co-authored using generative AI tooling?
Co-authored with Claude Opus 4.8 in compliance with ASF
---
.../operator/source/sql/mysql/MySQLConnUtil.scala | 8 +-
.../source/sql/postgresql/PostgreSQLConnUtil.scala | 7 +-
.../source/sql/mysql/MySQLConnUtilSpec.scala | 234 +++-----------------
.../sql/postgresql/PostgreSQLConnUtilSpec.scala | 235 ++-------------------
4 files changed, 65 insertions(+), 419 deletions(-)
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala
index 3e1ea93826..120f5dfe51 100644
---
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtil.scala
@@ -22,6 +22,11 @@ package org.apache.texera.amber.operator.source.sql.mysql
import java.sql.{Connection, DriverManager, SQLException}
object MySQLConnUtil {
+
+ // Builds the JDBC URL.
+ private[mysql] def buildJdbcUrl(host: String, port: String, database:
String): String =
+ "jdbc:mysql://" + host + ":" + port + "/" + database +
"?autoReconnect=true&useSSL=true"
+
@throws[SQLException]
def connect(
host: String,
@@ -30,8 +35,7 @@ object MySQLConnUtil {
username: String,
password: String
): Connection = {
- val url =
- "jdbc:mysql://" + host + ":" + port + "/" + database +
"?autoReconnect=true&useSSL=true"
+ val url = buildJdbcUrl(host, port, database)
val connection = DriverManager.getConnection(url, username, password)
// set to readonly to improve efficiency
connection.setReadOnly(true)
diff --git
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala
index c6fb4ff792..ba201a2ef7 100644
---
a/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala
+++
b/common/workflow-operator/src/main/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtil.scala
@@ -22,6 +22,11 @@ package
org.apache.texera.amber.operator.source.sql.postgresql
import java.sql.{Connection, DriverManager, SQLException}
object PostgreSQLConnUtil {
+
+ // Builds the JDBC URL.
+ private[postgresql] def buildJdbcUrl(host: String, port: String, database:
String): String =
+ "jdbc:postgresql://" + host + ":" + port + "/" + database
+
@throws[SQLException]
def connect(
host: String,
@@ -30,7 +35,7 @@ object PostgreSQLConnUtil {
username: String,
password: String
): Connection = {
- val url = "jdbc:postgresql://" + host + ":" + port + "/" + database
+ val url = buildJdbcUrl(host, port, database)
val connection = DriverManager.getConnection(url, username, password)
// set to readonly to improve efficiency
connection.setReadOnly(true)
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
index e4bd71cb18..99f3f38445 100644
---
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala
@@ -20,241 +20,71 @@
package org.apache.texera.amber.operator.source.sql.mysql
import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.BeforeAndAfterAll
-import java.lang.reflect.{InvocationHandler, Method, Proxy}
-import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo,
SQLException}
-import java.util.Properties
-import java.util.logging.Logger
-import scala.collection.mutable.ArrayBuffer
-import scala.jdk.CollectionConverters._
+class MySQLConnUtilSpec extends AnyFlatSpec {
-class MySQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll {
+ // Strategy: same as PostgreSQLConnUtilSpec. Pin the JDBC URL composition
+ // (the only application-logic in this util) without a real DB.
//
---------------------------------------------------------------------------
- // Strategy — same capturing-driver pattern as PostgreSQLConnUtilSpec.
- // The MySQL driver may or may not be present transitively, so we
- // proactively deregister anything that claims jdbc:mysql: and swap in a
- // capturing driver that records each URL and returns a Proxy-backed
- // Connection so the production code can call `setReadOnly(true)`.
+ // URL composition - host/port/database
//
---------------------------------------------------------------------------
- private object CapturingMySQLDriver extends Driver {
- val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty
- val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty
- val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty
-
- override def connect(url: String, info: Properties): Connection = {
- if (!acceptsURL(url)) return null
- seenUrls += url
- seenProps += info
- Proxy
- .newProxyInstance(
- getClass.getClassLoader,
- Array(classOf[Connection]),
- new InvocationHandler {
- override def invoke(p: Any, m: Method, args: Array[AnyRef]):
AnyRef =
- m.getName match {
- case "setReadOnly" =>
- readOnlyCalls +=
args(0).asInstanceOf[java.lang.Boolean].booleanValue()
- null
- case "equals" => java.lang.Boolean.valueOf(p eq args(0))
- case "hashCode" =>
java.lang.Integer.valueOf(System.identityHashCode(p))
- case "toString" =>
- "CapturingMySQLDriver.StubConnection@" +
System.identityHashCode(p)
- case "isWrapperFor" => java.lang.Boolean.FALSE
- case "close" => null
- case _ => null
- }
- }
- )
- .asInstanceOf[Connection]
- }
- override def acceptsURL(url: String): Boolean =
- url != null && url.startsWith("jdbc:mysql:")
- override def getPropertyInfo(url: String, info: Properties):
Array[DriverPropertyInfo] =
- Array.empty
- override def getMajorVersion: Int = 1
- override def getMinorVersion: Int = 0
- override def jdbcCompliant(): Boolean = false
- override def getParentLogger: Logger =
Logger.getLogger("test-mysql-capturing")
- }
-
- private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty
-
- private def safeAcceptsURL(d: Driver, url: String): Boolean =
- try d.acceptsURL(url)
- catch { case _: Throwable => false }
-
- override protected def beforeAll(): Unit = {
- super.beforeAll()
- // The probe URL mirrors the exact shape `MySQLConnUtil.connect`
- // constructs (`jdbc:mysql://{host}:{port}/{database}?…`), including
- // the canonical query parameters. A permissive third-party driver
- // that returns `false` on a stripped-down probe but `true` on the
- // real URL would otherwise slip past us.
- try {
- val others = DriverManager.getDrivers.asScala.toList.filter { d =>
- d != CapturingMySQLDriver && safeAcceptsURL(
- d,
-
"jdbc:mysql://probe-host:3306/probe-db?autoReconnect=true&useSSL=true"
- )
- }
- others.foreach { d =>
- savedRealDrivers += d
- DriverManager.deregisterDriver(d)
- }
- DriverManager.registerDriver(CapturingMySQLDriver)
- } catch {
- case t: Throwable =>
- savedRealDrivers.foreach { d =>
- try DriverManager.registerDriver(d)
- catch { case _: Throwable => () }
- }
- throw t
- }
- }
-
- override protected def afterAll(): Unit = {
- try {
- try DriverManager.deregisterDriver(CapturingMySQLDriver)
- catch { case _: Throwable => () }
- savedRealDrivers.foreach { d =>
- try DriverManager.registerDriver(d)
- catch { case _: Throwable => () }
- }
- } finally {
- super.afterAll()
- }
- }
-
- private def clearCapture(): Unit = {
- CapturingMySQLDriver.seenUrls.clear()
- CapturingMySQLDriver.seenProps.clear()
- CapturingMySQLDriver.readOnlyCalls.clear()
- }
-
- //
---------------------------------------------------------------------------
- // URL composition — host/port/database
- //
---------------------------------------------------------------------------
-
- "MySQLConnUtil.connect" should
- "build a JDBC URL of the form jdbc:mysql://{host}:{port}/{database}?…" in {
- clearCapture()
- val conn = MySQLConnUtil.connect("host-m", "3306", "db-m", "u", "p")
- assert(conn != null)
- assert(CapturingMySQLDriver.seenUrls.size == 1)
-
assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-m:3306/db-m"))
+ "MySQLConnUtil.buildJdbcUrl" should
+ "build a URL of the form jdbc:mysql://{host}:{port}/{database}?..." in {
+ assert(
+ MySQLConnUtil
+ .buildJdbcUrl("host-m", "3306", "db-m")
+ .startsWith("jdbc:mysql://host-m:3306/db-m")
+ )
}
it should "interpolate distinct host/port/database values into the URL" in {
- clearCapture()
- MySQLConnUtil.connect("host-1", "3306", "db-1", "u", "p")
-
assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-1:3306/db-1"))
- clearCapture()
- MySQLConnUtil.connect("host-2", "33060", "db-2", "u", "p")
-
assert(CapturingMySQLDriver.seenUrls.head.startsWith("jdbc:mysql://host-2:33060/db-2"))
+ assert(
+ MySQLConnUtil
+ .buildJdbcUrl("host-1", "3306", "db-1")
+ .startsWith("jdbc:mysql://host-1:3306/db-1")
+ )
+ assert(
+ MySQLConnUtil
+ .buildJdbcUrl("host-2", "33060", "db-2")
+ .startsWith("jdbc:mysql://host-2:33060/db-2")
+ )
}
it should "place host BEFORE port" in {
- clearCapture()
- MySQLConnUtil.connect("a", "1", "x", "u", "p")
- val url = CapturingMySQLDriver.seenUrls.head
+ val url = MySQLConnUtil.buildJdbcUrl("a", "1", "x")
assert(url.contains("//a:1/"))
assert(!url.contains("//1:a/"))
}
//
---------------------------------------------------------------------------
- // Query parameters — autoReconnect=true and useSSL=true must be present
+ // Query parameters - autoReconnect=true and useSSL=true must be present
//
---------------------------------------------------------------------------
it should "include the `autoReconnect=true` query parameter" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- val url = CapturingMySQLDriver.seenUrls.head
+ val url = MySQLConnUtil.buildJdbcUrl("h", "3306", "db")
assert(url.contains("autoReconnect=true"), s"URL must include
autoReconnect=true, got: $url")
}
it should "include the `useSSL=true` query parameter (TLS contract)" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- val url = CapturingMySQLDriver.seenUrls.head
+ val url = MySQLConnUtil.buildJdbcUrl("h", "3306", "db")
assert(url.contains("useSSL=true"), s"URL must include useSSL=true (TLS),
got: $url")
}
- it should "use the canonical `?…&…` separator pattern" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- val url = CapturingMySQLDriver.seenUrls.head
+ it should "use the canonical `?...&...` separator pattern" in {
assert(
- url == "jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true",
- s"URL must match canonical pattern, got: $url"
+ MySQLConnUtil.buildJdbcUrl(
+ "h",
+ "3306",
+ "db"
+ ) == "jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true"
)
}
it should "use the `mysql` JDBC subprotocol (not e.g. `postgresql`)" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- val url = CapturingMySQLDriver.seenUrls.head
+ val url = MySQLConnUtil.buildJdbcUrl("h", "3306", "db")
assert(url.startsWith("jdbc:mysql://"))
assert(!url.contains("jdbc:postgresql:"))
}
-
- //
---------------------------------------------------------------------------
- // Credentials propagation
- //
---------------------------------------------------------------------------
-
- it should "pass username and password through DriverManager properties" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "the-user", "the-pass")
- val props = CapturingMySQLDriver.seenProps.head
- assert(props.getProperty("user") == "the-user")
- assert(props.getProperty("password") == "the-pass")
- }
-
- //
---------------------------------------------------------------------------
- // setReadOnly(true) — pinned via the captured proxy (parity with PG spec)
- //
---------------------------------------------------------------------------
-
- it should "flip the returned Connection to read-only (query-efficiency
contract)" in {
- clearCapture()
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- assert(CapturingMySQLDriver.readOnlyCalls == ArrayBuffer(true))
- }
-
- //
---------------------------------------------------------------------------
- // SQLException propagation when the driver throws
- //
---------------------------------------------------------------------------
-
- it should "propagate SQLException when the driver throws" in {
- val throwingDriver = new Driver {
- override def acceptsURL(url: String): Boolean =
- url != null && url.startsWith("jdbc:mysql:")
- // Follow the JDBC contract: return `null` if the URL isn't ours
- // and throw only on a matching URL — keeps the helper from
- // interfering with `DriverManager.getConnection` calls for any
- // other scheme that might happen during the suite.
- override def connect(url: String, info: Properties): Connection = {
- if (!acceptsURL(url)) return null
- throw new SQLException("forced-fail-for-test")
- }
- override def getPropertyInfo(url: String, info: Properties) =
- Array.empty[DriverPropertyInfo]
- override def getMajorVersion: Int = 99
- override def getMinorVersion: Int = 0
- override def jdbcCompliant(): Boolean = false
- override def getParentLogger: Logger =
Logger.getLogger("test-mysql-throwing")
- }
- DriverManager.deregisterDriver(CapturingMySQLDriver)
- DriverManager.registerDriver(throwingDriver)
- try {
- val ex = intercept[SQLException] {
- MySQLConnUtil.connect("h", "3306", "db", "u", "p")
- }
- assert(ex.getMessage.contains("forced-fail-for-test"))
- } finally {
- DriverManager.deregisterDriver(throwingDriver)
- DriverManager.registerDriver(CapturingMySQLDriver)
- }
- }
}
diff --git
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
index 5081c11c26..be8ac359f9 100644
---
a/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
+++
b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala
@@ -20,244 +20,51 @@
package org.apache.texera.amber.operator.source.sql.postgresql
import org.scalatest.flatspec.AnyFlatSpec
-import org.scalatest.BeforeAndAfterAll
-import java.lang.reflect.{InvocationHandler, Method, Proxy}
-import java.sql.{Connection, Driver, DriverManager, DriverPropertyInfo,
SQLException}
-import java.util.Properties
-import java.util.logging.Logger
-import scala.collection.mutable.ArrayBuffer
-import scala.jdk.CollectionConverters._
+class PostgreSQLConnUtilSpec extends AnyFlatSpec {
-class PostgreSQLConnUtilSpec extends AnyFlatSpec with BeforeAndAfterAll {
-
- //
---------------------------------------------------------------------------
- // Strategy — pin the JDBC URL composition (the only application-logic in
+ // Strategy: pin the JDBC URL composition (the only application-logic in
// this util) without a real DB.
- //
- // The workflow-operator test classpath DOES include the real PostgreSQL
- // driver (transitively), and that driver eats `jdbc:postgresql:` URLs
- // before returning a generic "The connection attempt failed." exception.
- // So we can't rely on `DriverManager.getConnection`'s default
- // "No suitable driver" message.
- //
- // Instead, we deregister every driver claiming `jdbc:postgresql:`,
- // register a capturing driver that records each URL it is asked to open
- // (and returns a Proxy-backed Connection so the production code can call
- // `setReadOnly`), run the assertions, then restore the real drivers
- // in afterAll.
- //
---------------------------------------------------------------------------
-
- private object CapturingPGDriver extends Driver {
- val seenUrls: ArrayBuffer[String] = ArrayBuffer.empty
- val seenProps: ArrayBuffer[Properties] = ArrayBuffer.empty
- val readOnlyCalls: ArrayBuffer[Boolean] = ArrayBuffer.empty
-
- override def connect(url: String, info: Properties): Connection = {
- if (!acceptsURL(url)) return null
- seenUrls += url
- seenProps += info
- Proxy
- .newProxyInstance(
- getClass.getClassLoader,
- Array(classOf[Connection]),
- new InvocationHandler {
- override def invoke(p: Any, m: Method, args: Array[AnyRef]):
AnyRef =
- m.getName match {
- case "setReadOnly" =>
- readOnlyCalls +=
args(0).asInstanceOf[java.lang.Boolean].booleanValue()
- null
- // Object methods — required so `conn != null`,
`conn.toString`,
- // and identity HashMap-keying work without NPE on
auto-unboxing.
- case "equals" => java.lang.Boolean.valueOf(p eq args(0))
- case "hashCode" =>
java.lang.Integer.valueOf(System.identityHashCode(p))
- case "toString" => "CapturingPGDriver.StubConnection@" +
System.identityHashCode(p)
- case "isWrapperFor" => java.lang.Boolean.FALSE
- case "close" => null
- case _ => null
- }
- }
- )
- .asInstanceOf[Connection]
- }
- override def acceptsURL(url: String): Boolean =
- url != null && url.startsWith("jdbc:postgresql:")
- override def getPropertyInfo(url: String, info: Properties):
Array[DriverPropertyInfo] =
- Array.empty
- override def getMajorVersion: Int = 1
- override def getMinorVersion: Int = 0
- override def jdbcCompliant(): Boolean = false
- override def getParentLogger: Logger =
Logger.getLogger("test-pg-capturing")
- }
-
- // Snapshot of real PG drivers temporarily deregistered in beforeAll.
- // Restored in afterAll so other suites are not left with a broken
- // JDBC driver registry.
- private val savedRealDrivers: ArrayBuffer[Driver] = ArrayBuffer.empty
-
- /** `acceptsURL` is declared `throws SQLException`; treat any throw as
- * "this driver doesn't claim our scheme" so a flaky third-party driver
- * cannot abort the whole suite.
- */
- private def safeAcceptsURL(d: Driver, url: String): Boolean =
- try d.acceptsURL(url)
- catch { case _: Throwable => false }
-
- override protected def beforeAll(): Unit = {
- super.beforeAll()
- // Remove every other driver that claims jdbc:postgresql: so our
- // capturing driver is the only one DriverManager.getConnection sees.
- // The probe URL mirrors the exact shape `PostgreSQLConnUtil.connect`
- // constructs (`jdbc:postgresql://{host}:{port}/{database}`) so a
- // permissive third-party driver that returns `false` on a stripped-
- // down probe but `true` on the real URL can't slip past us.
- //
- // Wrapped in try/catch so that if any deregistration / registration
- // step throws, we restore whatever we already deregistered before
- // failing the suite — the alternative leaves the JVM's JDBC registry
- // in an inconsistent state for the rest of the test run.
- try {
- val others = DriverManager.getDrivers.asScala.toList.filter { d =>
- d != CapturingPGDriver && safeAcceptsURL(d,
"jdbc:postgresql://probe-host:5432/probe-db")
- }
- others.foreach { d =>
- savedRealDrivers += d
- DriverManager.deregisterDriver(d)
- }
- DriverManager.registerDriver(CapturingPGDriver)
- } catch {
- case t: Throwable =>
- // Best-effort restore before re-throwing.
- savedRealDrivers.foreach { d =>
- try DriverManager.registerDriver(d)
- catch { case _: Throwable => () }
- }
- throw t
- }
- }
-
- override protected def afterAll(): Unit = {
- try {
- try DriverManager.deregisterDriver(CapturingPGDriver)
- catch { case _: Throwable => () }
- savedRealDrivers.foreach { d =>
- try DriverManager.registerDriver(d)
- catch { case _: Throwable => () }
- }
- } finally {
- super.afterAll()
- }
- }
-
- private def clearCapture(): Unit = {
- CapturingPGDriver.seenUrls.clear()
- CapturingPGDriver.seenProps.clear()
- CapturingPGDriver.readOnlyCalls.clear()
- }
//
---------------------------------------------------------------------------
- // URL composition — pin the exact JDBC URL the driver receives
+ // URL composition - pin the exact JDBC URL
//
---------------------------------------------------------------------------
- "PostgreSQLConnUtil.connect" should
- "build a JDBC URL of the form jdbc:postgresql://{host}:{port}/{database}"
in {
- clearCapture()
- val conn = PostgreSQLConnUtil.connect("host-a", "5432", "db-a", "u", "p")
- assert(conn != null)
- assert(CapturingPGDriver.seenUrls.size == 1)
- assert(CapturingPGDriver.seenUrls.head ==
"jdbc:postgresql://host-a:5432/db-a")
+ "PostgreSQLConnUtil.buildJdbcUrl" should
+ "build a URL of the form jdbc:postgresql://{host}:{port}/{database}" in {
+ assert(
+ PostgreSQLConnUtil.buildJdbcUrl(
+ "host-a",
+ "5432",
+ "db-a"
+ ) == "jdbc:postgresql://host-a:5432/db-a"
+ )
}
it should "interpolate distinct host/port/database values into the URL" in {
- clearCapture()
- PostgreSQLConnUtil.connect("h-1", "1234", "d-1", "u", "p")
- assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h-1:1234/d-1")
- clearCapture()
- PostgreSQLConnUtil.connect("h-2", "9999", "d-2", "u", "p")
- assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h-2:9999/d-2")
+ assert(
+ PostgreSQLConnUtil.buildJdbcUrl("h-1", "1234", "d-1") ==
"jdbc:postgresql://h-1:1234/d-1"
+ )
+ assert(
+ PostgreSQLConnUtil.buildJdbcUrl("h-2", "9999", "d-2") ==
"jdbc:postgresql://h-2:9999/d-2"
+ )
}
it should "place host BEFORE port (host-then-port, not port-then-host)" in {
- clearCapture()
- PostgreSQLConnUtil.connect("a", "1", "x", "u", "p")
- val url = CapturingPGDriver.seenUrls.head
+ val url = PostgreSQLConnUtil.buildJdbcUrl("a", "1", "x")
assert(url.contains("//a:1/"), s"expected //a:1/ ordering, got: $url")
assert(!url.contains("//1:a/"), s"port-then-host ordering must NOT appear,
got: $url")
}
it should "use the `postgresql` JDBC subprotocol (not e.g. `mysql`)" in {
- clearCapture()
- PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p")
- val url = CapturingPGDriver.seenUrls.head
+ val url = PostgreSQLConnUtil.buildJdbcUrl("h", "5432", "db")
assert(url.startsWith("jdbc:postgresql://"))
assert(!url.contains("jdbc:mysql:"))
}
it should "accept an empty database name and still produce a well-formed
URL" in {
- clearCapture()
- PostgreSQLConnUtil.connect("h", "5432", "", "u", "p")
// The resulting `jdbc:postgresql://h:5432/` is well-formed even if a
// real driver would reject it.
- assert(CapturingPGDriver.seenUrls.head == "jdbc:postgresql://h:5432/")
- }
-
- //
---------------------------------------------------------------------------
- // Credentials propagation
- //
---------------------------------------------------------------------------
-
- it should "pass username and password through DriverManager properties" in {
- clearCapture()
- PostgreSQLConnUtil.connect("h", "5432", "db", "the-user", "the-pass")
- val props = CapturingPGDriver.seenProps.head
- assert(props.getProperty("user") == "the-user")
- assert(props.getProperty("password") == "the-pass")
- }
-
- //
---------------------------------------------------------------------------
- // setReadOnly(true) — pinned via the captured proxy
- //
---------------------------------------------------------------------------
-
- it should "flip the returned Connection to read-only (query-efficiency
contract)" in {
- clearCapture()
- PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p")
- assert(CapturingPGDriver.readOnlyCalls == ArrayBuffer(true))
- }
-
- //
---------------------------------------------------------------------------
- // SQLException propagation when the driver throws — pin the @throws contract
- //
---------------------------------------------------------------------------
-
- it should "propagate SQLException when the driver throws" in {
- // Swap in a one-shot throwing override of `connect`. We can't mutate
- // CapturingPGDriver in-place, so register a higher-priority throwing
- // driver and remove it after.
- val throwingDriver = new Driver {
- override def acceptsURL(url: String): Boolean =
- url != null && url.startsWith("jdbc:postgresql:")
- // Follow the JDBC contract: return `null` if the URL is not ours,
- // then throw only on a matching URL. A future refactor that calls
- // `DriverManager.getConnection` with a different scheme while
- // this driver is registered would otherwise see a spurious throw.
- override def connect(url: String, info: Properties): Connection = {
- if (!acceptsURL(url)) return null
- throw new SQLException("forced-fail-for-test")
- }
- override def getPropertyInfo(url: String, info: Properties) =
Array.empty[DriverPropertyInfo]
- override def getMajorVersion: Int = 99
- override def getMinorVersion: Int = 0
- override def jdbcCompliant(): Boolean = false
- override def getParentLogger: Logger =
Logger.getLogger("test-pg-throwing")
- }
- DriverManager.deregisterDriver(CapturingPGDriver)
- DriverManager.registerDriver(throwingDriver)
- try {
- val ex = intercept[SQLException] {
- PostgreSQLConnUtil.connect("h", "5432", "db", "u", "p")
- }
- assert(ex.getMessage.contains("forced-fail-for-test"))
- } finally {
- DriverManager.deregisterDriver(throwingDriver)
- DriverManager.registerDriver(CapturingPGDriver)
- }
+ assert(PostgreSQLConnUtil.buildJdbcUrl("h", "5432", "") ==
"jdbc:postgresql://h:5432/")
}
}