From 5fd4e07ad1551026e666421ca0fa8dc06466f9a6 Mon Sep 17 00:00:00 2001 From: Magnus Persson <1431685+magnusp@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:08:40 +0200 Subject: [PATCH] fix(conformance): make namespace cleanup failures visible (#52) --- .../NamespaceClosingHandlerTest.java | 121 +++++++++++++ .../conformance/XerialConformanceDriver.java | 77 +++++++- .../libsql/conformance/XerialTestServer.java | 165 +++++++++++++++--- .../XerialTestServerCleanupTest.java | 124 +++++++++++++ upstream-xerial_sqlite-jdbc.manifest | 6 + upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 | 4 +- .../java/org/sqlite/CachedRowSetTest.java | 19 +- .../src/test/java/org/sqlite/JSON1Test.java | 13 +- 8 files changed, 485 insertions(+), 44 deletions(-) create mode 100644 libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/NamespaceClosingHandlerTest.java create mode 100644 libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerCleanupTest.java diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/NamespaceClosingHandlerTest.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/NamespaceClosingHandlerTest.java new file mode 100644 index 0000000..cba908a --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/NamespaceClosingHandlerTest.java @@ -0,0 +1,121 @@ +package com.github.magnusp.libsql.conformance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.lang.reflect.Proxy; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import org.junit.jupiter.api.Test; + +class NamespaceClosingHandlerTest { + + @Test + void closeReleasesNamespaceOnlyOnce() throws SQLException { + List released = new ArrayList<>(); + List calls = new ArrayList<>(); + Connection connection = proxy(connection(calls, false, false), released::add); + + connection.close(); + connection.close(); + + assertThat(released).containsExactly("ns"); + assertThat(calls).containsExactly("close"); + } + + @Test + void closingAnUnwrappedConnectionReleasesNamespace() throws SQLException { + List released = new ArrayList<>(); + List calls = new ArrayList<>(); + Connection connection = proxy(connection(calls, false, false), released::add); + + Connection unwrapped = connection.unwrap(Connection.class); + assertThat(unwrapped).isSameAs(connection); + + unwrapped.close(); + + assertThat(released).containsExactly("ns"); + assertThat(calls).containsExactly("close"); + } + + @Test + void abortReleasesNamespace() throws SQLException { + List released = new ArrayList<>(); + List calls = new ArrayList<>(); + Connection connection = proxy(connection(calls, false, false), released::add); + + connection.abort(Runnable::run); + + assertThat(released).containsExactly("ns"); + assertThat(calls).containsExactly("abort"); + } + + @Test + void closeFailureStillReleasesNamespace() { + List released = new ArrayList<>(); + List calls = new ArrayList<>(); + Connection connection = proxy(connection(calls, true, false), released::add); + + assertThatThrownBy(connection::close) + .isInstanceOf(SQLException.class) + .hasMessage("close failed"); + + assertThat(released).containsExactly("ns"); + } + + @Test + void abortFailureStillReleasesNamespace() { + List released = new ArrayList<>(); + List calls = new ArrayList<>(); + Connection connection = proxy(connection(calls, false, true), released::add); + + assertThatThrownBy(() -> connection.abort(Runnable::run)) + .isInstanceOf(SQLException.class) + .hasMessage("abort failed"); + + assertThat(released).containsExactly("ns"); + } + + private static Connection proxy(Connection connection, Consumer release) { + return (Connection) + Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[] {Connection.class}, + new XerialConformanceDriver.NamespaceClosingHandler( + connection, "ns", release)); + } + + private static Connection connection( + List calls, boolean failClose, boolean failAbort) { + return (Connection) + Proxy.newProxyInstance( + Connection.class.getClassLoader(), + new Class[] {Connection.class}, + (proxy, method, args) -> + switch (method.getName()) { + case "close" -> { + calls.add("close"); + if (failClose) { + throw new SQLException("close failed"); + } + yield null; + } + case "abort" -> { + calls.add("abort"); + if (failAbort) { + throw new SQLException("abort failed"); + } + yield null; + } + case "toString" -> "fake connection"; + case "hashCode" -> System.identityHashCode(proxy); + case "equals" -> proxy == args[0]; + default -> + throw new UnsupportedOperationException( + method.getName()); + }); + } +} diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java index b755479..b59f778 100644 --- a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialConformanceDriver.java @@ -15,6 +15,8 @@ import java.sql.SQLFeatureNotSupportedException; import java.util.Properties; import java.util.UUID; +import java.util.concurrent.Executor; +import java.util.function.Consumer; import java.util.logging.Logger; public final class XerialConformanceDriver implements Driver { @@ -43,7 +45,7 @@ public Connection connect(String url, Properties info) throws SQLException { try { connection = delegate.connect(target.url(), info); } catch (SQLException e) { - XerialTestServer.releaseNamespace(target.namespace()); + releaseAfterConnectFailure(target.namespace(), e); throw e; } if (connection == null) { @@ -57,7 +59,11 @@ public Connection connect(String url, Properties info) throws SQLException { new Class[] {Connection.class}, new NamespaceClosingHandler(connection, target.namespace())); } catch (RuntimeException e) { - XerialTestServer.releaseNamespace(target.namespace()); + try { + XerialTestServer.releaseNamespace(target.namespace()); + } catch (RuntimeException cleanup) { + e.addSuppressed(cleanup); + } throw e; } } @@ -95,6 +101,14 @@ public Logger getParentLogger() throws SQLFeatureNotSupportedException { return delegate.getParentLogger(); } + private static void releaseAfterConnectFailure(String namespace, SQLException failure) { + try { + XerialTestServer.releaseNamespace(namespace); + } catch (RuntimeException cleanup) { + failure.addSuppressed(cleanup); + } + } + private static Target rewrite(String url) { String target = url.substring(PREFIX.length()); String namespace; @@ -112,15 +126,22 @@ private static Target rewrite(String url) { private record Target(String url, String namespace) {} - private static final class NamespaceClosingHandler implements InvocationHandler { + static final class NamespaceClosingHandler implements InvocationHandler { private final Connection connection; private final String namespace; + private final Consumer release; private boolean closed; - private NamespaceClosingHandler(Connection connection, String namespace) { + NamespaceClosingHandler(Connection connection, String namespace) { + this(connection, namespace, XerialTestServer::releaseNamespace); + } + + NamespaceClosingHandler( + Connection connection, String namespace, Consumer release) { this.connection = connection; this.namespace = namespace; + this.release = release; } @Override @@ -129,6 +150,16 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl close(); return null; } + if ("abort".equals(method.getName()) && method.getParameterCount() == 1) { + abort((Executor) args[0]); + return null; + } + if ("unwrap".equals(method.getName()) && method.getParameterCount() == 1) { + Class iface = (Class) args[0]; + if (iface != null && iface.isInstance(proxy)) { + return proxy; + } + } try { return method.invoke(connection, args); } catch (InvocationTargetException e) { @@ -143,8 +174,42 @@ private synchronized void close() throws SQLException { closed = true; try { connection.close(); - } finally { - XerialTestServer.releaseNamespace(namespace); + } catch (SQLException e) { + releaseWithSuppressed(e); + throw e; + } catch (RuntimeException e) { + releaseWithSuppressed(e); + throw e; + } + releaseNamespace(); + } + + private synchronized void abort(Executor executor) throws SQLException { + if (closed) { + return; + } + closed = true; + try { + connection.abort(executor); + } catch (SQLException e) { + releaseWithSuppressed(e); + throw e; + } catch (RuntimeException e) { + releaseWithSuppressed(e); + throw e; + } + releaseNamespace(); + } + + private void releaseNamespace() { + release.accept(namespace); + } + + private void releaseWithSuppressed(Throwable failure) { + try { + releaseNamespace(); + } catch (RuntimeException cleanup) { + failure.addSuppressed(cleanup); } } } diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java index e4f7148..7fa9003 100644 --- a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServer.java @@ -21,9 +21,19 @@ public final class XerialTestServer { private static final int HTTP_PORT = 8080; private static final int ADMIN_PORT = 9090; private static final Duration REQUEST_TIMEOUT = Duration.ofSeconds(30); + private static final int DELETE_ATTEMPTS = 3; + private static final Duration DELETE_RETRY_DELAY = Duration.ofMillis(100); private static final HttpClient HTTP = HttpClient.newHttpClient(); + private static final AdminClient DEFAULT_ADMIN = + request -> { + HttpResponse response = + HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + return new AdminResponse(response.statusCode(), response.body()); + }; private static final Map NAMESPACES = new ConcurrentHashMap<>(); + private static final Map FAILED_NAMESPACES = + new ConcurrentHashMap<>(); private static GenericContainer container; private static String httpBaseUrl; @@ -83,6 +93,7 @@ public static synchronized void stop() { httpBaseUrl = null; adminBaseUrl = null; NAMESPACES.clear(); + FAILED_NAMESPACES.clear(); } } @@ -93,6 +104,11 @@ public static String jdbcUrl(String namespace) { } public static synchronized void releaseNamespace(String namespace) { + releaseNamespace(namespace, adminBaseUrl, DEFAULT_ADMIN); + } + + static synchronized void releaseNamespace( + String namespace, String adminUrl, AdminClient admin) { int active = NAMESPACES.getOrDefault(namespace, 0); if (active == 0) { return; @@ -102,23 +118,15 @@ public static synchronized void releaseNamespace(String namespace) { return; } NAMESPACES.remove(namespace); - if (container == null) { + if (adminUrl == null) { return; } - HttpRequest request = - HttpRequest.newBuilder() - .uri(URI.create(adminBaseUrl + "/v1/namespaces/" + namespace)) - .timeout(REQUEST_TIMEOUT) - .DELETE() - .build(); - try { - HTTP.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException e) { - return; - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); + deleteNamespace(namespace, adminUrl, admin); + } catch (RuntimeException e) { + FAILED_NAMESPACES.put(namespace, e); + throw e; } } @@ -126,24 +134,44 @@ static boolean isReadyStatus(int status) { return status == 405 || (status >= 200 && status < 300); } - private static synchronized void ensureNamespace(String namespace) { + static synchronized void ensureNamespace(String namespace) { + ensureNamespace(namespace, adminBaseUrl, DEFAULT_ADMIN); + } + + static synchronized void ensureNamespace( + String namespace, String adminUrl, AdminClient admin) { + RuntimeException failed = FAILED_NAMESPACES.get(namespace); + if (failed != null) { + throw new IllegalStateException( + "Namespace " + + namespace + + " cannot be reused because an earlier cleanup failed", + failed); + } + int active = NAMESPACES.getOrDefault(namespace, 0); if (active > 0) { NAMESPACES.put(namespace, active + 1); return; } + if (adminUrl == null) { + throw new IllegalStateException("libSQL server is not running"); + } + HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(adminBaseUrl + "/v1/namespaces/" + namespace + "/create")) + .uri( + URI.create( + adminUrl + "/v1/namespaces/" + namespace + "/create")) .header("Content-Type", "application/json") .timeout(REQUEST_TIMEOUT) .POST(HttpRequest.BodyPublishers.ofString("{}")) .build(); - HttpResponse response; + AdminResponse response; try { - response = HTTP.send(request, HttpResponse.BodyHandlers.ofString()); + response = admin.send(request); } catch (IOException e) { throw new IllegalStateException("Failed to create namespace " + namespace, e); } catch (InterruptedException e) { @@ -152,16 +180,101 @@ private static synchronized void ensureNamespace(String namespace) { "Interrupted while creating namespace " + namespace, e); } - if (response.statusCode() != 200 && response.statusCode() != 409) { - throw new IllegalStateException( - "Failed to create namespace " - + namespace - + ": HTTP " - + response.statusCode() - + " " - + response.body()); + int status = response.status(); + if (status >= 200 && status < 300) { + NAMESPACES.put(namespace, 1); + return; + } + + IllegalStateException failure = + new IllegalStateException( + "Failed to create namespace " + + namespace + + ": HTTP " + + status + + " " + + response.body()); + if (status == 409) { + failure = + new IllegalStateException( + "Namespace " + + namespace + + " already exists and cannot be reused: HTTP " + + status + + " " + + response.body()); + FAILED_NAMESPACES.put(namespace, failure); } + throw failure; + } + + private static void deleteNamespace( + String namespace, String adminUrl, AdminClient admin) { + HttpRequest request = + HttpRequest.newBuilder() + .uri(URI.create(adminUrl + "/v1/namespaces/" + namespace)) + .timeout(REQUEST_TIMEOUT) + .DELETE() + .build(); + + RuntimeException failure = null; + for (int attempt = 1; attempt <= DELETE_ATTEMPTS; attempt++) { + try { + AdminResponse response = admin.send(request); + int status = response.status(); + if ((status >= 200 && status < 300) || status == 404) { + return; + } + failure = + new IllegalStateException( + "Failed to delete namespace " + + namespace + + ": HTTP " + + status + + " " + + response.body()); + if (!isRetryable(status) || attempt == DELETE_ATTEMPTS) { + break; + } + } catch (IOException e) { + failure = + new IllegalStateException( + "Failed to delete namespace " + namespace, e); + if (attempt == DELETE_ATTEMPTS) { + break; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException( + "Interrupted while deleting namespace " + namespace, e); + } - NAMESPACES.put(namespace, 1); + sleepBeforeRetry(namespace, failure); + } + throw failure; + } + + @FunctionalInterface + interface AdminClient { + AdminResponse send(HttpRequest request) throws IOException, InterruptedException; + } + + record AdminResponse(int status, String body) {} + + private static boolean isRetryable(int status) { + return status == 408 || status == 429 || status >= 500; + } + + private static void sleepBeforeRetry(String namespace, RuntimeException failure) { + try { + Thread.sleep(DELETE_RETRY_DELAY.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + IllegalStateException interrupted = + new IllegalStateException( + "Interrupted while deleting namespace " + namespace, e); + interrupted.addSuppressed(failure); + throw interrupted; + } } } diff --git a/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerCleanupTest.java b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerCleanupTest.java new file mode 100644 index 0000000..eb28c28 --- /dev/null +++ b/libsql-conformance/src/test/java/com/github/magnusp/libsql/conformance/XerialTestServerCleanupTest.java @@ -0,0 +1,124 @@ +package com.github.magnusp.libsql.conformance; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.http.HttpRequest; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.Test; + +class XerialTestServerCleanupTest { + + private static final String ADMIN_URL = "http://admin.test"; + + private final FakeAdmin admin = new FakeAdmin(); + + @Test + void failedDeleteIsVisibleAndBlocksNamespaceReuse() { + String namespace = namespace(); + + XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin); + assertThat(admin.createRequests).hasValue(1); + + admin.deleteStatus.set(500); + assertThatThrownBy( + () -> + XerialTestServer.releaseNamespace( + namespace, ADMIN_URL, admin)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Failed to delete namespace") + .hasMessageContaining("500"); + assertThat(admin.deleteRequests).hasValue(3); + + admin.createStatus.set(409); + assertThatThrownBy( + () -> XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("cannot be reused"); + assertThat(admin.createRequests).hasValue(1); + } + + @Test + void deleteRetriesTransientFailuresAndAllowsReuseAfterwards() { + String namespace = namespace(); + + XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin); + admin.deleteFailuresRemaining.set(2); + + assertThatCode( + () -> + XerialTestServer.releaseNamespace( + namespace, ADMIN_URL, admin)) + .doesNotThrowAnyException(); + assertThat(admin.deleteRequests).hasValue(3); + + XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin); + assertThat(admin.createRequests).hasValue(2); + } + + @Test + void deleteTreatsAlreadyMissingNamespaceAsSuccess() { + String namespace = namespace(); + + XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin); + admin.deleteStatus.set(404); + + assertThatCode( + () -> + XerialTestServer.releaseNamespace( + namespace, ADMIN_URL, admin)) + .doesNotThrowAnyException(); + assertThat(admin.deleteRequests).hasValue(1); + } + + @Test + void createConflictIsNotTreatedAsSuccess() { + String namespace = namespace(); + + admin.createStatus.set(409); + assertThatThrownBy( + () -> XerialTestServer.ensureNamespace(namespace, ADMIN_URL, admin)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("409") + .hasMessageContaining("cannot be reused"); + assertThat(admin.createRequests).hasValue(1); + } + + private static String namespace() { + return "cleanup_" + UUID.randomUUID().toString().replace("-", ""); + } + + private static final class FakeAdmin implements XerialTestServer.AdminClient { + + private final AtomicInteger createStatus = new AtomicInteger(200); + private final AtomicInteger deleteStatus = new AtomicInteger(200); + private final AtomicInteger deleteFailuresRemaining = new AtomicInteger(); + private final AtomicInteger createRequests = new AtomicInteger(); + private final AtomicInteger deleteRequests = new AtomicInteger(); + + @Override + public XerialTestServer.AdminResponse send(HttpRequest request) { + if ("POST".equals(request.method()) + && request.uri().getPath().endsWith("/create")) { + createRequests.incrementAndGet(); + return response(createStatus.get()); + } + if ("DELETE".equals(request.method())) { + deleteRequests.incrementAndGet(); + if (deleteFailuresRemaining.getAndUpdate( + remaining -> remaining > 0 ? remaining - 1 : 0) + > 0) { + return response(500); + } + return response(deleteStatus.get()); + } + return response(404); + } + + private static XerialTestServer.AdminResponse response(int status) { + return new XerialTestServer.AdminResponse(status, "status " + status); + } + } +} diff --git a/upstream-xerial_sqlite-jdbc.manifest b/upstream-xerial_sqlite-jdbc.manifest index a839a9d..847eeda 100644 --- a/upstream-xerial_sqlite-jdbc.manifest +++ b/upstream-xerial_sqlite-jdbc.manifest @@ -27,3 +27,9 @@ LICENSE LICENSE.zentus NOTICE VERSION + +# Deliberate divergences. libSQL's Hrana transport releases a sqld namespace +# when its last connection closes, so the conformance tests must close every +# connection they open; leaving one open leaks the namespace for the whole run. +# divergence: src/test/java/org/sqlite/JSON1Test.java -- Hrana holds a namespace until its last connection closes, so the conformance copy closes the leaked per-test connections. +# divergence: src/test/java/org/sqlite/CachedRowSetTest.java -- Hrana holds a namespace until its last connection closes, so the conformance copy closes the leaked connection. diff --git a/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 b/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 index 234b581..95607c9 100644 --- a/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 +++ b/upstream-xerial_sqlite-jdbc/CHECKSUMS.sha256 @@ -11,13 +11,13 @@ a0670cf020edf700524c43c85321c2ba027a8bace5682001db60875d4a9fe52b ./NOTICE # upstream 0a60829ce6edd56de46fb87303401c5c1e1e666da5631056340f5c5ce902dcdf ./VERSION 0a60829ce6edd56de46fb87303401c5c1e1e666da5631056340f5c5ce902dcdf ./VERSION # upstream 1351fdca2644e90ac9028ed99c6096c9ecee82ccb7d45cedd9bb454887fd2c6b ./src/test/java/org/sqlite/CachedRowSetTest.java -1351fdca2644e90ac9028ed99c6096c9ecee82ccb7d45cedd9bb454887fd2c6b ./src/test/java/org/sqlite/CachedRowSetTest.java +9e896e20522e4c5f4bc786ad7f8c5b6765428ad531fc0c07130b5f0beb81756f ./src/test/java/org/sqlite/CachedRowSetTest.java # upstream 9fb0f2df7de66cac0a46b241c0c1cd3311a80646d0431d377097741b99053760 ./src/test/java/org/sqlite/FetchSizeTest.java 9fb0f2df7de66cac0a46b241c0c1cd3311a80646d0431d377097741b99053760 ./src/test/java/org/sqlite/FetchSizeTest.java # upstream 6dfcecb209e01829ba0aea782b47351443e3970ad1d33ac4c3fb979e6dff1f40 ./src/test/java/org/sqlite/InsertQueryTest.java 6dfcecb209e01829ba0aea782b47351443e3970ad1d33ac4c3fb979e6dff1f40 ./src/test/java/org/sqlite/InsertQueryTest.java # upstream bb07398946480ca44c70d03e6f8c399f53506b44c83a312e2d28aebd239c0238 ./src/test/java/org/sqlite/JSON1Test.java -bb07398946480ca44c70d03e6f8c399f53506b44c83a312e2d28aebd239c0238 ./src/test/java/org/sqlite/JSON1Test.java +075745907a5174b922db6a45b9c29f45901af1835af27a0014f5fac835b41d0f ./src/test/java/org/sqlite/JSON1Test.java # upstream 242bb51330240174bab5f443aa4d8de1049e6d947374e080a636c4aa4d0049ee ./src/test/java/org/sqlite/MetadataLeakTest.java 242bb51330240174bab5f443aa4d8de1049e6d947374e080a636c4aa4d0049ee ./src/test/java/org/sqlite/MetadataLeakTest.java # upstream 7af8ae0999a0bf26a20c96ab23131505bd79827e6af17742771e36e9d8d985f4 ./src/test/java/org/sqlite/PreparedStatementThreadTest.java diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java index c879bc8..7b9de8d 100644 --- a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/CachedRowSetTest.java @@ -13,15 +13,16 @@ public class CachedRowSetTest { @Test public void gh_224() throws SQLException { - Connection connection = DriverManager.getConnection("jdbc:sqlite:"); - try (Statement statement = connection.createStatement()) { - statement.execute("create table person (id INTEGER, name VARCHAR(50))"); - statement.execute("insert into person values(1, 'leo')"); - } - RowSetFactory factory = RowSetProvider.newFactory(); - try (CachedRowSet crs = factory.createCachedRowSet()) { - crs.setCommand("select * from person"); - crs.execute(connection); + try (Connection connection = DriverManager.getConnection("jdbc:sqlite:")) { + try (Statement statement = connection.createStatement()) { + statement.execute("create table person (id INTEGER, name VARCHAR(50))"); + statement.execute("insert into person values(1, 'leo')"); + } + RowSetFactory factory = RowSetProvider.newFactory(); + try (CachedRowSet crs = factory.createCachedRowSet()) { + crs.setCommand("select * from person"); + crs.execute(connection); + } } } } diff --git a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java index 4bcd234..33fa009 100644 --- a/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java +++ b/upstream-xerial_sqlite-jdbc/src/test/java/org/sqlite/JSON1Test.java @@ -6,6 +6,7 @@ import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; /** @@ -15,8 +16,18 @@ */ public class JSON1Test { + private Connection connection; + public Connection getConnection() throws SQLException { - return DriverManager.getConnection("jdbc:sqlite::memory:"); + connection = DriverManager.getConnection("jdbc:sqlite::memory:"); + return connection; + } + + @AfterEach + public void closeConnection() throws SQLException { + if (connection != null) { + connection.close(); + } } @Test