diff --git a/pom.xml b/pom.xml
index c6ba065ce..5b6a0731b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
+ * When {@link #closeConnectionAfterTest} is false this connection is kept + * across test methods (see {@link #closeReusableConnection()}), where the + * connection pool or the database server can close it between tests - a + * pool max-lifetime or reap, a bounced application context, a + * {@link org.dbunit.database.CachingConnectionProvider#close()}. A closed + * one is discarded here before it is handed back, so the next call + * re-acquires from databaseTester - letting a + * {@link org.dbunit.database.CachingConnectionProvider} behind it supply a + * live replacement - rather than this instance reusing a connection every + * later lifecycle step would only fail on. With + * {@link #closeConnectionAfterTest} left at its default the connection is + * closed and forgotten after each test anyway, so it is not re-checked + * here. * * @return The shared connection. * @throws Exception On dbUnit errors. @@ -285,6 +300,11 @@ private boolean lookupFeatureValue(final String featureName) */ private IDatabaseConnection getReusableConnection() throws Exception { + if (connection != null && !closeConnectionAfterTest + && isReusableConnectionClosed()) + { + connection = null; + } if (connection == null) { connection = getConnection(); @@ -292,13 +312,46 @@ private IDatabaseConnection getReusableConnection() throws Exception return connection; } + /** + * Returns whether the connection currently cached in {@link #connection} has + * been closed - by the connection pool, the database server, or a + * {@link org.dbunit.database.CachingConnectionProvider} behind + * {@code databaseTester} - since this instance last used it. A connection + * that throws while being asked is treated as closed. Uses only the local + * {@link Connection#isClosed()} flag, not a round-tripping + * {@link Connection#isValid(int)}: cheap, side-effect free, and enough to + * catch a connection closed between test methods before the next one's + * first statement. A server-side disconnect the driver has not noticed yet + * instead surfaces once, when a lifecycle step runs a statement against it; + * {@link #closeReusableConnectionSuppressing(Throwable)} then forgets the + * connection so the following test re-acquires regardless. + * + * @return True when the cached connection is known to be closed or unusable. + * @since 3.5.2 + */ + private boolean isReusableConnectionClosed() + { + try + { + return connection.getConnection().isClosed(); + } catch (final SQLException e) + { + log.debug("isReusableConnectionClosed: treating the cached connection" + + " as closed after it failed to report its state", e); + return true; + } + } + /** * Release the connection shared by lookupFeatureValue(), setupData(), * verifyData() and cleanupData(), if one was acquired: closes it and * forgets it when {@link #closeConnectionAfterTest} is true (the * default); otherwise leaves it open and keeps the field set, so a later * lifecycle step's getReusableConnection() call keeps reusing it rather - * than acquiring - and silently orphaning - another one. + * than acquiring - and silently orphaning - another one. That later call + * still drops the kept connection if it has since died (see + * {@link #getReusableConnection()}), so a connection the pool or server + * closed between test methods does not linger to fail every following one. * * @throws SQLException On close errors. * @since 3.4.0 @@ -333,6 +386,14 @@ private void closeReusableConnection() throws SQLException * {@link Throwable#addSuppressed(Throwable)} rather than letting it * replace and hide the primary. Mirrors the exception safety of * {@link #runTest} and {@code DatabaseTestCase.tearDown(Throwable)}. + *
+ * Only ever called from a lifecycle step that has already failed, so it
+ * also forgets the shared connection even when {@link #closeConnectionAfterTest}
+ * is false and {@link #closeReusableConnection()} therefore left it open: a
+ * step that just threw may have broken it, so the next
+ * {@link #getReusableConnection()} re-acquires rather than reusing it. Any
+ * {@link org.dbunit.database.CachingConnectionProvider} behind
+ * {@code databaseTester} still owns closing it.
*
* @param primary
* The exception already in flight to attach a close failure
@@ -348,6 +409,7 @@ private void closeReusableConnectionSuppressing(final Throwable primary)
{
primary.addSuppressed(closeFailure);
}
+ connection = null;
}
/**
diff --git a/src/test/java/org/dbunit/DatabaseTesterConnectionReuseIT.java b/src/test/java/org/dbunit/DatabaseTesterConnectionReuseIT.java
index 228966fa8..6c8dc2c6e 100644
--- a/src/test/java/org/dbunit/DatabaseTesterConnectionReuseIT.java
+++ b/src/test/java/org/dbunit/DatabaseTesterConnectionReuseIT.java
@@ -205,12 +205,100 @@ void testDefaultPrepAndExpectedTestCase_acrossFreshInstancesSharingAProviderWith
}
}
+ @Test
+ void testDefaultPrepAndExpectedTestCase_reusedAcrossTestMethodsWithCloseDisabled_keepsReusingItsOneOpenConnection()
+ throws Exception
+ {
+ final CachingConnectionProvider provider = new CachingConnectionProvider();
+ final IDatabaseTester tester = newSharedProviderTester(provider);
+ tester.setTearDownOperation(DatabaseOperation.DELETE_ALL);
+ final DefaultPrepAndExpectedTestCase tc = newCloseDisabledTestCase(tester);
+ final List