This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-5698-263093aaa54b34996013c717e4bdf7f8902230d0 in repository https://gitbox.apache.org/repos/asf/texera.git
commit 83df2b5132d3b9ab76f8796c890a0685edd9f1fd Author: Xinyuan Lin <[email protected]> AuthorDate: Sun Jun 14 20:39:07 2026 -0700 test(workflow-operator): add unit test coverage for PostgreSQLConnUtil and MySQLConnUtil (#5698) ### What changes were proposed in this PR? Pin the JDBC URL composition for the two SQL-source connection helpers in `common/workflow-operator/operator/source/sql/{postgresql,mysql}/` without standing up a real DB. No production-code changes. | Spec | Source class | Tests | | --- | --- | --- | | `PostgreSQLConnUtilSpec` | `PostgreSQLConnUtil` | 8 | | `MySQLConnUtilSpec` | `MySQLConnUtil` | 10 | Both spec files follow the `<srcClassName>Spec.scala` one-to-one convention. **Behavior pinned — `PostgreSQLConnUtil`** | Surface | Contract | | --- | --- | | URL format | `jdbc:postgresql://{host}:{port}/{database}` (exact substring) | | Host/port/database interpolation | distinct values reach their slots; host BEFORE port | | Subprotocol | `jdbc:postgresql:`, never `jdbc:mysql:` | | Empty database name | produces a well-formed `jdbc:postgresql://{host}:{port}/` URL | | Credentials | `user` / `password` reach the driver via `Properties` | | `setReadOnly(true)` | called on the returned Connection (query-efficiency contract) | | `SQLException` | propagated when the driver throws | **Behavior pinned — `MySQLConnUtil`** | Surface | Contract | | --- | --- | | URL format | `jdbc:mysql://{host}:{port}/{database}?…` (exact substring) | | Host/port/database interpolation | distinct values reach their slots; host BEFORE port | | `autoReconnect=true` query parameter | present (retry-behavior contract) | | `useSSL=true` query parameter | present (TLS contract — drift here would silently downgrade security) | | `?` / `&` separators | canonical `jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true` sequence pinned end-to-end | | Subprotocol | `jdbc:mysql:`, never `jdbc:postgresql:` | | Credentials | `user` / `password` reach the driver via `Properties` | | `setReadOnly(true)` | called on the returned Connection (query-efficiency contract) | | `SQLException` | propagated when the driver throws | **Test strategy** Both specs use the same capturing-driver pattern: - Deregister every driver that claims the relevant `jdbc:postgresql:` / `jdbc:mysql:` scheme via a `safeAcceptsURL` helper (the JDBC spec allows `Driver.acceptsURL` to throw `SQLException`, so the probe must be defensive). - Register a capturing driver that records each URL + the `Properties` it is asked to open, and returns a `java.lang.reflect.Proxy`-backed `Connection` so the production code can call `setReadOnly(true)` against a stand-in. - Restore the original drivers in `afterAll`. Setup failures also trigger best-effort re-registration so a sibling suite never sees a half-deregistered JDBC registry. This approach works regardless of whether a real driver happens to be on the test classpath (the PostgreSQL driver is loaded transitively from `org.postgresql:postgresql`; the MySQL driver is not currently on the workflow-operator classpath, but the spec is robust if it gets added later). ### Any related issues, documentation, discussions? Closes #5695. ### How was this PR tested? Pure unit-test additions; verified locally with: - \`sbt \"WorkflowOperator/testOnly org.apache.texera.amber.operator.source.sql.postgresql.PostgreSQLConnUtilSpec org.apache.texera.amber.operator.source.sql.mysql.MySQLConnUtilSpec\"\` — 18 tests, all green - \`sbt scalafmtCheckAll\` — clean - CI to confirm ### Was this PR authored or co-authored using generative AI tooling? Generated-by: Claude Code (Opus 4.7 [1M context]) --- .../source/sql/mysql/MySQLConnUtilSpec.scala | 260 ++++++++++++++++++++ .../sql/postgresql/PostgreSQLConnUtilSpec.scala | 263 +++++++++++++++++++++ 2 files changed, 523 insertions(+) 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 new file mode 100644 index 0000000000..e4bd71cb18 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/mysql/MySQLConnUtilSpec.scala @@ -0,0 +1,260 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 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 with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // 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)`. + // --------------------------------------------------------------------------- + + 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")) + } + + 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")) + } + + it should "place host BEFORE port" in { + clearCapture() + MySQLConnUtil.connect("a", "1", "x", "u", "p") + val url = CapturingMySQLDriver.seenUrls.head + assert(url.contains("//a:1/")) + assert(!url.contains("//1:a/")) + } + + // --------------------------------------------------------------------------- + // 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 + 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 + 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 + assert( + url == "jdbc:mysql://h:3306/db?autoReconnect=true&useSSL=true", + s"URL must match canonical pattern, got: $url" + ) + } + + 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 + 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 new file mode 100644 index 0000000000..5081c11c26 --- /dev/null +++ b/common/workflow-operator/src/test/scala/org/apache/texera/amber/operator/source/sql/postgresql/PostgreSQLConnUtilSpec.scala @@ -0,0 +1,263 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 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 with BeforeAndAfterAll { + + // --------------------------------------------------------------------------- + // 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 + // --------------------------------------------------------------------------- + + "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") + } + + 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") + } + + 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 + 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 + 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) + } + } +}
