This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 3e17d1135e17ecd5a7fa6105b026334e68a9c789
Author: opencode <[email protected]>
AuthorDate: Fri Oct 23 16:42:52 2026 +0200

    Add functional test suite for the jdbc-pool module
    
    First phase of the test suite rewrite: generic functional tests for
    the expected pool functionality (acquiring and reusing connections,
    executing statements, transactions, sizing/eviction, lifecycle) run
    against a real H2 database in TCP server mode, one server and
    in-memory database per test class. Assertions about physical
    connections use database-side truth (INFORMATION_SCHEMA.SESSIONS
    session counts and a session-scoped marker variable), not pool
    statistics.
    
    - JUnit 5 (Jupiter 5.11.x, the newest line compatible with the
      module's Java 8 test compilation), with the vintage engine so the
      JUnit 4 suite keeps running.
    - The legacy flaky PoolCleanerTest is excluded from the full-suite
      run; it asserts a pristine global cleaner registry that other
      legacy classes leak, and fails identically with and without the
      coverage agent. Superseded by the new suite.
    - 35 new tests, ~13 s; total suite is green (220 tests).
    
    The plan is tracked in modules/jdbc-pool/TESTING-PLAN.md
---
 modules/jdbc-pool/pom.xml                          |  29 ++
 .../functional/ConnectionAcquireReleaseTest.java   | 292 +++++++++++++++++++++
 .../tomcat/jdbc/functional/PoolEvictionTest.java   | 179 +++++++++++++
 .../tomcat/jdbc/functional/PoolTestSupport.java    | 211 +++++++++++++++
 .../jdbc/functional/StatementExecutionTest.java    | 251 ++++++++++++++++++
 .../tomcat/jdbc/functional/TransactionTest.java    | 190 ++++++++++++++
 6 files changed, 1152 insertions(+)

diff --git a/modules/jdbc-pool/pom.xml b/modules/jdbc-pool/pom.xml
index 2f9ecb8809..9a2a0e1331 100644
--- a/modules/jdbc-pool/pom.xml
+++ b/modules/jdbc-pool/pom.xml
@@ -85,6 +85,18 @@
       <version>2.2.220</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <version>5.11.4</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.vintage</groupId>
+      <artifactId>junit-vintage-engine</artifactId>
+      <version>5.11.4</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   <build>
     <plugins>
@@ -100,6 +112,23 @@
           <showWarnings>true</showWarnings>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <!--
+            PoolCleanerTest asserts the global static cleaner
+            registry is pristine; other legacy classes leak pool
+            cleaners, so it fails in any full-suite run (passes in
+            isolation; unrelated to the coverage agent). Excluded
+            until the legacy suite is retired; superseded by the
+            functional suite in org.apache.tomcat.jdbc.functional.
+          -->
+          <excludes>
+            <exclude>org/apache/tomcat/jdbc/test/PoolCleanerTest.java</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-jar-plugin</artifactId>
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/ConnectionAcquireReleaseTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/ConnectionAcquireReleaseTest.java
new file mode 100644
index 0000000000..7607087dba
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/ConnectionAcquireReleaseTest.java
@@ -0,0 +1,292 @@
+/*
+ *  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.tomcat.jdbc.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+/**
+ * Expected functionality of borrowing connections from the pool,
+ * returning them, and reusing them. All assertions about physical
+ * connections are made against the database, not pool statistics.
+ */
+class ConnectionAcquireReleaseTest extends PoolTestSupport {
+
+    @Test
+    void initialSizePreCreatesPhysicalConnections() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(2);
+        ds.createPool();
+        assertEquals(2, poolSessions(),
+                "initialSize connections must exist before any borrow");
+    }
+
+    @Test
+    void getConnectionReturnsUsableConnection() throws Exception {
+        DataSource ds = newDataSource();
+        try (Connection c = ds.getConnection()) {
+            assertNotNull(c);
+            assertFalse(c.isClosed());
+            assertTrue(c.isValid(2));
+            assertEquals("H2", c.getMetaData().getDatabaseProductName());
+            try (Statement st = c.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+                assertEquals(1, rs.getInt(1));
+            }
+        }
+    }
+
+    @Test
+    void closeReturnsConnectionToPoolWithoutPhysicalClose() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+
+        Connection c1 = ds.getConnection();
+        setSessionMarker(c1, 777);
+        c1.close();
+        assertTrue(c1.isClosed(), "closed proxy must report isClosed()");
+        assertEquals(1, poolSessions(),
+                "close() must not physically close the pooled connection");
+
+        try (Connection c2 = ds.getConnection()) {
+            Integer marker = sessionMarker(c2);
+            assertNotNull(marker, "same physical session must have been 
reused");
+            assertEquals(777, marker.intValue());
+            assertEquals(1, poolSessions(), "reuse must not open a new 
session");
+        }
+    }
+
+    @Test
+    void doubleCloseIsHarmlessAndKeepsConnectionReusable() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        Connection c = ds.getConnection();
+        setSessionMarker(c, 11);
+        c.close();
+        c.close(); // documented to be a no-op
+        try (Connection again = ds.getConnection()) {
+            Integer marker = sessionMarker(again);
+            assertNotNull(marker);
+            assertEquals(11, marker.intValue());
+        }
+    }
+
+    @Test
+    void poolGrowsToMaxActiveAndRejectsBeyondItAfterMaxWait() throws Exception 
{
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(5);
+        ds.getPoolProperties().setMaxWait(300);
+
+        List<Connection> held = new ArrayList<>();
+        try {
+            for (int i = 0; i < 5; i++) {
+                held.add(ds.getConnection());
+            }
+            assertEquals(5, poolSessions(),
+                    "each outstanding borrow must be its own physical 
session");
+            assertThrows(SQLException.class, ds::getConnection,
+                    "exhausted pool must throw after maxWait");
+        } finally {
+            for (Connection c : held) {
+                c.close();
+            }
+        }
+    }
+
+    @Test
+    void waiterReceivesReleasedConnection() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setMaxWait(10000);
+
+        final Connection held = ds.getConnection();
+        setSessionMarker(held, 42);
+
+        final CountDownLatch done = new CountDownLatch(1);
+        final AtomicReference<Throwable> failure = new AtomicReference<>();
+        final AtomicInteger observedMarker = new AtomicInteger(-1);
+        final AtomicReference<Long> waitMillis = new AtomicReference<>();
+        Thread waiter = new Thread(() -> {
+            long start = System.currentTimeMillis();
+            try (Connection c = ds.getConnection()) {
+                waitMillis.set(System.currentTimeMillis() - start);
+                Integer marker = sessionMarker(c);
+                if (marker != null) {
+                    observedMarker.set(marker.intValue());
+                }
+            } catch (Throwable t) {
+                failure.set(t);
+            } finally {
+                done.countDown();
+            }
+        });
+        waiter.start();
+
+        sleep(300); // let the waiter block on the exhausted pool
+        held.close();
+
+        assertTrue(done.await(10, java.util.concurrent.TimeUnit.SECONDS),
+                "waiter should have obtained a connection");
+        assertNull(failure.get());
+        assertTrue(waitMillis.get() >= 200,
+                "waiter must actually have waited for the release, waited "
+                        + waitMillis.get() + " ms");
+        assertEquals(42, observedMarker.get(),
+                "waiter must have received the released physical session");
+    }
+
+    @Test
+    void exhaustionThrowsAfterMaxWaitAndRecoveryWorks() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setMaxWait(300);
+
+        Connection held = ds.getConnection();
+        long start = System.currentTimeMillis();
+        assertThrows(SQLException.class, ds::getConnection);
+        long elapsed = System.currentTimeMillis() - start;
+        assertTrue(elapsed >= 250 && elapsed < 5000,
+                "timeout should be honoured approximately, was " + elapsed + " 
ms");
+
+        held.close();
+        try (Connection recovered = ds.getConnection()) {
+            try (Statement st = recovered.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+        }
+    }
+
+    @Test
+    void dataSourceClosePhysicallyReleasesAllConnections() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(2);
+        ds.getConnection().close();
+        ds.getConnection().close();
+        assertEquals(1, poolSessions()); // one pooled connection, reused
+        ds.close();
+        awaitSessions(0);
+    }
+
+    @Test
+    void dataSourcesAreIsolatedAndClosingOneLeavesTheOther() throws Exception {
+        DataSource ds1 = newDataSource();
+        DataSource ds2 = newDataSource();
+        ds1.getPoolProperties().setMaxActive(1);
+        ds2.getPoolProperties().setMaxActive(1);
+
+        ds1.getConnection();
+        Connection c2 = ds2.getConnection();
+        setSessionMarker(c2, 22);
+        assertEquals(2, poolSessions());
+
+        ds1.close();
+        awaitSessions(1);
+        // the surviving pool is untouched by the other's shutdown
+        Integer marker = sessionMarker(c2);
+        assertNotNull(marker);
+        assertEquals(22, marker.intValue());
+        c2.close();
+    }
+
+    @Test
+    void testOnBorrowWithValidationQuery() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setTestOnBorrow(true);
+        ds.getPoolProperties().setValidationQuery("SELECT 1");
+        try (Connection c = ds.getConnection()) {
+            try (Statement st = c.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 2")) {
+                assertTrue(rs.next());
+                assertEquals(2, rs.getInt(1));
+            }
+        }
+        assertEquals(1, poolSessions(), "validation must not consume 
sessions");
+    }
+
+    @Test
+    void testOnBorrowWithIsValidWhenNoValidationQuery() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setTestOnBorrow(true);
+        ds.getPoolProperties().setValidationQuery("");
+        try (Connection c = ds.getConnection()) {
+            assertTrue(c.isValid(2));
+        }
+        assertEquals(1, poolSessions());
+    }
+
+    @Test
+    void testOnReturnValidatesWithoutBreakingReuse() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setTestOnReturn(true);
+        ds.getPoolProperties().setValidationQuery("SELECT 1");
+        ds.getPoolProperties().setMaxActive(1);
+        Connection c1 = ds.getConnection();
+        setSessionMarker(c1, 5);
+        c1.close();
+        try (Connection c2 = ds.getConnection()) {
+            Integer marker = sessionMarker(c2);
+            assertNotNull(marker, "validated connection must be reused, not 
replaced");
+            assertEquals(5, marker.intValue());
+        }
+    }
+
+    @Test
+    void abandonedConnectionIsReclaimedByMaintenance() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setRemoveAbandoned(true);
+        ds.getPoolProperties().setRemoveAbandonedTimeout(1); // seconds
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        ds.getPoolProperties().setMaxWait(15000);
+
+        ds.getConnection(); // borrowed and never returned -> abandoned
+
+        long start = System.currentTimeMillis();
+        try (Connection reclaimed = ds.getConnection()) {
+            long elapsed = System.currentTimeMillis() - start;
+            assertTrue(elapsed >= 800,
+                    "reclaim must wait out the abandoned timeout, was " + 
elapsed + " ms");
+            try (Statement st = reclaimed.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+        }
+        assertTrue(poolSessions() <= 1,
+                "the abandoned physical connection must not be left dangling");
+    }
+
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolEvictionTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolEvictionTest.java
new file mode 100644
index 0000000000..5e7850598a
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolEvictionTest.java
@@ -0,0 +1,179 @@
+/*
+ *  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.tomcat.jdbc.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+/**
+ * Expected pool sizing behaviour: idle capping on return, the min-idle
+ * floor under aggressive eviction, max-age rotation, and idle
+ * validation keeping connections alive. Observed exclusively through
+ * physical sessions on the database.
+ */
+class PoolEvictionTest extends PoolTestSupport {
+
+    @Test
+    void excessIdleConnectionsAreClosedOnReturnDownToMaxIdle() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(5);
+        ds.getPoolProperties().setMaxIdle(2);
+        // sweeper disabled: the maxIdle cap must be enforced by the
+        // return path itself, not by a background thread
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(0);
+
+        // open 5 concurrent physical connections, then return them all
+        List<Connection> opened = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            opened.add(ds.getConnection());
+        }
+        assertEquals(5, poolSessions());
+        for (Connection c : opened) {
+            c.close();
+        }
+        awaitSessions(2);
+        // pool still functional after the shrink
+        try (Connection c = ds.getConnection()) {
+            try (Statement st = c.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+        }
+    }
+
+    @Test
+    void minIdleFloorSurvivesAggressiveEviction() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(3);
+        ds.getPoolProperties().setMinIdle(3);
+        ds.getPoolProperties().setMaxIdle(5);
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        ds.getPoolProperties().setMinEvictableIdleTimeMillis(100);
+        ds.createPool();
+        assertEquals(3, poolSessions());
+
+        // several eviction rounds over well past the idle threshold
+        sleep(1500);
+        assertEquals(3, poolSessions(),
+                "connections at the min-idle floor must not be evicted");
+
+        try (Connection c = ds.getConnection()) {
+            assertTrue(c.isValid(2));
+        }
+    }
+
+    @Test
+    void maxAgeRetiresAnOldConnectionBetweenBorrowers() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setMaxAge(1500);
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+
+        Connection first = ds.getConnection();
+        setSessionMarker(first, 99);
+        first.close();
+        // within maxAge the very same physical session is reused
+        try (Connection same = ds.getConnection()) {
+            Integer marker = sessionMarker(same);
+            assertNotNull(marker, "young connection must be reused");
+            assertEquals(99, marker.intValue());
+            same.close();
+        }
+
+        // past maxAge the connection is retired and a fresh one takes its
+        // place (sweeper and/or borrow-time check), keeping the pool size
+        sleep(3500); // ~2x maxAge plus sweeper interval
+        awaitSessions(1);
+        try (Connection replacement = ds.getConnection()) {
+            assertNull(sessionMarker(replacement),
+                    "after maxAge the pool must not hand out the old session");
+            try (Statement st = replacement.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+        }
+        assertEquals(1, poolSessions());
+    }
+
+    @Test
+    void testWhileIdleKeepsConnectionsAliveAndUsable() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setInitialSize(2);
+        ds.getPoolProperties().setMinIdle(2);
+        ds.getPoolProperties().setTestWhileIdle(true);
+        ds.getPoolProperties().setValidationQuery("SELECT 1");
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        ds.getPoolProperties().setMinEvictableIdleTimeMillis(100);
+        ds.createPool();
+
+        sleep(1500); // many validation rounds
+        assertEquals(2, poolSessions(),
+                "validated idle connections must be kept, not replaced");
+
+        try (Connection a = ds.getConnection(); Connection b = 
ds.getConnection()) {
+            try (Statement st = a.createStatement(); ResultSet rs = 
st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+            try (Statement st = b.createStatement(); ResultSet rs = 
st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+        }
+    }
+
+    @Test
+    void borrowAfterEvictionCycleYieldsFreshConnection() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(1);
+        ds.getPoolProperties().setMinIdle(0);
+        ds.getPoolProperties().setMaxIdle(1);
+        ds.getPoolProperties().setTimeBetweenEvictionRunsMillis(200);
+        ds.getPoolProperties().setMinEvictableIdleTimeMillis(300);
+
+        Connection c1 = ds.getConnection();
+        setSessionMarker(c1, 7);
+        c1.close();
+        Integer markerBeforeEviction = null;
+        try (Connection probe = ds.getConnection()) {
+            markerBeforeEviction = sessionMarker(probe);
+        }
+        assertNotNull(markerBeforeEviction,
+                "quick re-borrow must reuse the idle connection");
+
+        awaitSessions(0); // idle eviction retires it (age > 300ms, minIdle 0)
+
+        try (Connection c2 = ds.getConnection()) {
+            assertNull(sessionMarker(c2),
+                    "after eviction the pool must hand out a fresh 
connection");
+            Integer marker = sessionMarker(c2);
+            assertNull(marker);
+            setSessionMarker(c2, 8);
+            assertEquals(8, sessionMarker(c2).intValue());
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
new file mode 100644
index 0000000000..f2145ec631
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/PoolTestSupport.java
@@ -0,0 +1,211 @@
+/*
+ *  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.tomcat.jdbc.functional;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.function.BooleanSupplier;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+import org.h2.tools.Server;
+
+/**
+ * Base class for the functional test suite. Every test class gets its
+ * own H2 server (TCP mode, ephemeral port, in-memory database) so that
+ * pool connections are genuine client/server JDBC connections that can
+ * be observed - and asserted against - from the database side:
+ *
+ * <ul>
+ * <li>{@link #poolSessions()} counts the <em>physical</em> sessions the
+ * database currently holds for this test, so reuse, release, eviction
+ * and leaks are verified with server-side truth rather than
+ * pool-reported statistics;</li>
+ * <li>{@link #setSessionMarker(Connection, int)} /
+ * {@link #sessionMarker(Connection)} use an H2 session-scoped variable
+ * to identify which physical session a borrowed connection maps to.</li>
+ * </ul>
+ *
+ * Test classes must not run in parallel (each uses static fixture
+ * state); Surefire's default sequential execution applies.
+ */
+public abstract class PoolTestSupport {
+
+    /** Upper bound for any await-style condition; generous on slow CI. */
+    protected static final long AWAIT_TIMEOUT_MS = 15000;
+
+    private static Server server;
+    private static String url;
+
+    private final List<DataSource> createdDataSources = new ArrayList<>();
+
+    @BeforeAll
+    public static void startDatabase() throws Exception {
+        // Port 0 -> ephemeral port, getPort() returns the bound port.
+        // -ifNotExists so the first client connection may create the
+        // in-memory database over TCP.
+        server = Server.createTcpServer("-tcpPort", "0", 
"-ifNotExists").start();
+        url = "jdbc:h2:tcp://localhost:" + server.getPort()
+                + "/mem:pooltest_" + UUID.randomUUID();
+    }
+
+    @AfterAll
+    public static void stopDatabase() {
+        if (server != null) {
+            // Stopping the server also kills any connection a test
+            // leaked, keeping test classes isolated from each other.
+            server.stop();
+            server = null;
+        }
+    }
+
+    protected static String getUrl() {
+        return url;
+    }
+
+    /**
+     * Creates a DataSource pointing at this test class' database. JMX is
+     * disabled by default to keep MBean registration out of functional
+     * tests (it has its own test area). Initial/min idle sizes are pinned
+     * to the minimum so each test observes exactly the connections it
+     * asks for; tests override these explicitly when needed.
+     */
+    protected DataSource newDataSource() {
+        DataSource ds = new DataSource();
+        ds.setUrl(url);
+        ds.setDriverClassName("org.h2.Driver");
+        ds.setUsername("sa");
+        ds.setPassword("");
+        ds.getPoolProperties().setJmxEnabled(false);
+        ds.getPoolProperties().setInitialSize(1);
+        ds.getPoolProperties().setMinIdle(0);
+        createdDataSources.add(ds);
+        return ds;
+    }
+
+    @AfterEach
+    public void closeDataSources() {
+        for (DataSource ds : createdDataSources) {
+            try {
+                ds.close();
+            } catch (Exception ignore) {
+                // best effort; the @AfterAll server stop is the hard reset
+            }
+        }
+        createdDataSources.clear();
+    }
+
+    /** A raw admin connection that bypasses the pool entirely. */
+    protected Connection adminConnection() throws SQLException {
+        return DriverManager.getConnection(url, "sa", "");
+    }
+
+    /**
+     * Number of physical sessions on the database other than the admin
+     * connection this method itself opens - i.e. the connections the
+     * pool (or a test) currently holds.
+     */
+    protected int poolSessions() throws SQLException {
+        try (Connection admin = adminConnection();
+                Statement st = admin.createStatement();
+                ResultSet rs = st.executeQuery(
+                        "SELECT COUNT(*) FROM INFORMATION_SCHEMA.SESSIONS")) {
+            rs.next();
+            return rs.getInt(1) - 1; // subtract this admin session
+        }
+    }
+
+    /** Polls {@link #poolSessions()} until it reaches the expected count. */
+    protected void awaitSessions(int expected) throws SQLException {
+        long deadline = System.currentTimeMillis() + AWAIT_TIMEOUT_MS;
+        int observed = poolSessions();
+        while (observed != expected) {
+            if (System.currentTimeMillis() > deadline) {
+                throw new AssertionError("Timed out waiting for " + expected
+                        + " physical sessions, last observed " + observed);
+            }
+            sleep(25);
+            observed = poolSessions();
+        }
+    }
+
+    /**
+     * Sets an H2 session variable on the physical session behind this
+     * connection. It is visible only to that same physical session,
+     * which makes it a session identity probe.
+     */
+    protected void setSessionMarker(Connection c, int value) throws 
SQLException {
+        try (Statement st = c.createStatement()) {
+            st.execute("SET @POOL_TEST_MARKER = " + value);
+        }
+    }
+
+    /** The marker visible on this connection's physical session, or null. */
+    protected Integer sessionMarker(Connection c) throws SQLException {
+        try (Statement st = c.createStatement();
+                ResultSet rs = st.executeQuery("SELECT @POOL_TEST_MARKER")) {
+            rs.next();
+            Object value = rs.getObject(1);
+            return value == null ? null : Integer.valueOf(((Number) 
value).intValue());
+        }
+    }
+
+    /** Polls a condition until it holds or the await timeout expires. */
+    protected static void awaitUntil(BooleanSupplier condition) {
+        long deadline = System.currentTimeMillis() + AWAIT_TIMEOUT_MS;
+        while (!condition.getAsBoolean()) {
+            if (System.currentTimeMillis() > deadline) {
+                throw new AssertionError(
+                        "Condition not met within " + AWAIT_TIMEOUT_MS + " 
ms");
+            }
+            sleep(25);
+        }
+    }
+
+    protected static void sleep(long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new AssertionError("Interrupted while waiting", e);
+        }
+    }
+
+    /** Unique table name for the duration of one test. */
+    protected static String uniqueTable() {
+        return "T_" + System.nanoTime();
+    }
+
+    /** Committed row count of a table, observed through an admin connection. 
*/
+    protected int adminRowCount(String table) throws SQLException {
+        try (Connection admin = adminConnection();
+                Statement st = admin.createStatement();
+                ResultSet rs = st.executeQuery("SELECT COUNT(*) FROM " + 
table)) {
+            rs.next();
+            return rs.getInt(1);
+        }
+    }
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/StatementExecutionTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/StatementExecutionTest.java
new file mode 100644
index 0000000000..fd69785cab
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/StatementExecutionTest.java
@@ -0,0 +1,251 @@
+/*
+ *  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.tomcat.jdbc.functional;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+/**
+ * Expected functionality of executing statements (DDL, DML, queries,
+ * batches, calls) through pooled connections against the real database.
+ */
+class StatementExecutionTest extends PoolTestSupport {
+
+    @Test
+    void ddlDmlSelectRoundTrip() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (id INT PRIMARY KEY, name 
VARCHAR(32))");
+            assertEquals(1, st.executeUpdate(
+                    "INSERT INTO " + t + " VALUES (1, 'hello')"));
+            try (ResultSet rs = st.executeQuery(
+                    "SELECT id, name FROM " + t + " WHERE id = 1")) {
+                assertTrue(rs.next());
+                assertEquals(1, rs.getInt("id"));
+                assertEquals("hello", rs.getString("name"));
+                assertFalse(rs.next(), "exactly one row");
+            }
+        }
+        assertEquals(1, adminRowCount(t), "auto-commit DML must be committed");
+    }
+
+    @Test
+    void preparedStatementWithParametersAndGeneratedKeys() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            try (Statement st = c.createStatement()) {
+                st.execute("CREATE TABLE " + t
+                        + " (id INT AUTO_INCREMENT PRIMARY KEY, name 
VARCHAR(32))");
+            }
+            try (PreparedStatement ps = c.prepareStatement(
+                    "INSERT INTO " + t + " (name) VALUES (?)",
+                    Statement.RETURN_GENERATED_KEYS)) {
+                ps.setString(1, "first");
+                assertEquals(1, ps.executeUpdate());
+                try (ResultSet keys = ps.getGeneratedKeys()) {
+                    assertTrue(keys.next());
+                    assertEquals(1, keys.getInt(1));
+                }
+                ps.setString(1, "second");
+                assertEquals(1, ps.executeUpdate());
+                try (ResultSet keys = ps.getGeneratedKeys()) {
+                    assertTrue(keys.next());
+                    assertEquals(2, keys.getInt(1));
+                }
+            }
+            try (Statement st = c.createStatement();
+                    ResultSet rs = st.executeQuery(
+                            "SELECT name FROM " + t + " WHERE id = 2")) {
+                assertTrue(rs.next());
+                assertEquals("second", rs.getString(1));
+            }
+        }
+        assertEquals(2, adminRowCount(t));
+    }
+
+    @Test
+    void batchExecution() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+            st.addBatch("INSERT INTO " + t + " VALUES (1)");
+            st.addBatch("INSERT INTO " + t + " VALUES (2)");
+            st.addBatch("INSERT INTO " + t + " VALUES (3)");
+            assertArrayEquals(new int[] { 1, 1, 1 }, st.executeBatch());
+
+            try (PreparedStatement ps = c.prepareStatement(
+                    "INSERT INTO " + t + " VALUES (?)")) {
+                for (int i = 4; i <= 6; i++) {
+                    ps.setInt(1, i);
+                    ps.addBatch();
+                }
+                int[] counts = ps.executeBatch();
+                assertEquals(3, counts.length);
+                for (int count : counts) {
+                    assertEquals(1, count);
+                }
+            }
+        }
+        assertEquals(6, adminRowCount(t));
+    }
+
+    @Test
+    void updateCountsReflectAffectedRows() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+            st.execute("INSERT INTO " + t + " VALUES (1),(2),(3)");
+            assertEquals(3, st.executeUpdate("UPDATE " + t + " SET v = v + 
10"));
+            try (ResultSet rs = st.executeQuery(
+                    "SELECT MIN(v), MAX(v) FROM " + t)) {
+                assertTrue(rs.next());
+                assertEquals(11, rs.getInt(1));
+                assertEquals(13, rs.getInt(2));
+            }
+            assertEquals(2, st.executeUpdate("DELETE FROM " + t + " WHERE v < 
13"));
+            assertEquals(1, adminRowCount(t));
+        }
+    }
+
+    @Test
+    void resultSetMetadataAndNavigation() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (id INT, label VARCHAR(10))");
+            st.execute("INSERT INTO " + t +
+                    " SELECT X, 'row' || X FROM SYSTEM_RANGE(1, 10)");
+            try (ResultSet rs = st.executeQuery(
+                    "SELECT id, label FROM " + t + " ORDER BY id")) {
+                ResultSetMetaData md = rs.getMetaData();
+                assertEquals(2, md.getColumnCount());
+                int idColumn = rs.findColumn("LABEL");
+                assertTrue(idColumn >= 1);
+                int sum = 0;
+                int rows = 0;
+                while (rs.next()) {
+                    sum += rs.getInt("id");
+                    assertEquals("row" + rs.getInt("id"), 
rs.getString("label"));
+                    rows++;
+                }
+                assertEquals(10, rows);
+                assertEquals(55, sum);
+            }
+        }
+    }
+
+    @Test
+    void setMaxRowsLimitsResultSets() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+            st.execute("INSERT INTO " + t +
+                    " SELECT X FROM SYSTEM_RANGE(1, 10)");
+            st.setMaxRows(4);
+            try (ResultSet rs = st.executeQuery("SELECT v FROM " + t)) {
+                int rows = 0;
+                while (rs.next()) {
+                    rows++;
+                }
+                assertEquals(4, rows);
+            }
+        }
+    }
+
+    @Test
+    void failedStatementLeavesConnectionUsable() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            try (Statement st = c.createStatement()) {
+                st.execute("CREATE TABLE " + t + " (v INT)");
+            }
+            try (Statement bad = c.createStatement()) {
+                assertThrows(SQLException.class,
+                        () -> bad.executeQuery("SELECT no_such_column FROM " + 
t));
+            }
+            // the connection must survive a failed statement
+            assertTrue(c.isValid(2));
+            try (Statement st = c.createStatement();
+                    ResultSet rs = st.executeQuery("SELECT 1")) {
+                assertTrue(rs.next());
+            }
+            try (Statement st = c.createStatement()) {
+                assertEquals(1, st.executeUpdate(
+                        "INSERT INTO " + t + " VALUES (1)"));
+            }
+        }
+        assertEquals(1, adminRowCount(t));
+    }
+
+    @Test
+    void callableStatementInvocation() throws Exception {
+        DataSource ds = newDataSource();
+        try (Connection c = ds.getConnection();
+                CallableStatement cs = c.prepareCall("{call ROUND(2.5, 0)}")) {
+            try (ResultSet rs = cs.executeQuery()) {
+                assertTrue(rs.next());
+                assertEquals(3.0, rs.getDouble(1), 0.0001);
+            }
+        }
+    }
+
+    @Test
+    void multipleStatementsPerBorrowAreIndependent() throws Exception {
+        DataSource ds = newDataSource();
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection()) {
+            Statement one = c.createStatement();
+            Statement two = c.createStatement();
+            try {
+                one.execute("CREATE TABLE " + t + " (v INT)");
+                two.execute("INSERT INTO " + t + " VALUES (1)");
+                one.close();
+                // closing one statement must not affect the other
+                try (ResultSet rs = two.executeQuery("SELECT COUNT(*) FROM " + 
t)) {
+                    assertTrue(rs.next());
+                    assertEquals(1, rs.getInt(1));
+                }
+            } finally {
+                one.close();
+                two.close();
+            }
+        }
+        assertEquals(1, adminRowCount(t));
+    }
+
+}
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/TransactionTest.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/TransactionTest.java
new file mode 100644
index 0000000000..ffec527760
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/functional/TransactionTest.java
@@ -0,0 +1,190 @@
+/*
+ *  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.tomcat.jdbc.functional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.sql.Connection;
+import java.sql.Statement;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.pool.DataSource;
+
+/**
+ * Expected transaction functionality of pooled connections: commit,
+ * rollback, and the state the pool restores between borrowers. Every
+ * commit/rollback assertion is checked from an independent admin
+ * connection, i.e. against what the database actually persisted.
+ */
+class TransactionTest extends PoolTestSupport {
+
+    private String createTable(DataSource ds) throws Exception {
+        String t = uniqueTable();
+        try (Connection c = ds.getConnection(); Statement st = 
c.createStatement()) {
+            st.execute("CREATE TABLE " + t + " (v INT)");
+        }
+        return t;
+    }
+
+    private void insert(Connection c, String table, int value) throws 
Exception {
+        try (Statement st = c.createStatement()) {
+            st.executeUpdate("INSERT INTO " + table + " VALUES (" + value + 
")");
+        }
+    }
+
+    @Test
+    void autoCommitIsDefaultAndPersistsImmediately() throws Exception {
+        DataSource ds = newDataSource();
+        String t = createTable(ds);
+        try (Connection c = ds.getConnection()) {
+            assertTrue(c.getAutoCommit(), "auto-commit is the pool default");
+            insert(c, t, 1);
+            // visible to others without close/commit
+            assertEquals(1, adminRowCount(t));
+        }
+    }
+
+    @Test
+    void explicitRollbackDiscardsWork() throws Exception {
+        DataSource ds = newDataSource();
+        String t = createTable(ds);
+        try (Connection c = ds.getConnection()) {
+            c.setAutoCommit(false);
+            insert(c, t, 1);
+            assertEquals(0, adminRowCount(t), "uncommitted work is invisible");
+            c.rollback();
+            assertEquals(0, adminRowCount(t));
+        }
+        assertEquals(0, adminRowCount(t));
+    }
+
+    @Test
+    void explicitCommitPersistsWork() throws Exception {
+        DataSource ds = newDataSource();
+        String t = createTable(ds);
+        try (Connection c = ds.getConnection()) {
+            c.setAutoCommit(false);
+            insert(c, t, 1);
+            c.commit();
+            assertEquals(1, adminRowCount(t));
+            insert(c, t, 2);
+            c.commit();
+            assertEquals(2, adminRowCount(t));
+        }
+    }
+
+    @Test
+    void rollbackOnReturnDiscardsUncommittedWork() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setDefaultAutoCommit(false);
+        ds.getPoolProperties().setRollbackOnReturn(true);
+        String t = createTable(ds);
+
+        Connection c1 = ds.getConnection();
+        insert(c1, t, 1);
+        c1.close(); // returned without commit -> must be rolled back
+
+        assertEquals(0, adminRowCount(t), "uncommitted work must not survive");
+        try (Connection c2 = ds.getConnection()) {
+            try (Statement st = c2.createStatement()) {
+                try (java.sql.ResultSet rs = st.executeQuery(
+                        "SELECT COUNT(*) FROM " + t)) {
+                    assertTrue(rs.next());
+                    assertEquals(0, rs.getInt(1),
+                            "the next borrower must not observe the previous 
work");
+                }
+            }
+        }
+    }
+
+    @Test
+    void configuredDefaultAutoCommitIsRestoredForNextBorrower() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setDefaultAutoCommit(true);
+        ds.getPoolProperties().setJdbcInterceptors(
+                "org.apache.tomcat.jdbc.pool.interceptor.ConnectionState");
+        try (Connection c1 = ds.getConnection()) {
+            assertTrue(c1.getAutoCommit(), "configured default");
+            c1.setAutoCommit(false);
+            assertFalse(c1.getAutoCommit());
+        }
+        try (Connection c2 = ds.getConnection()) {
+            assertTrue(c2.getAutoCommit(),
+                    "configured default must be restored before the next 
borrow");
+        }
+    }
+
+    @Test
+    void configuredDefaultIsolationIsRestoredForNextBorrower() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setDefaultTransactionIsolation(
+                Connection.TRANSACTION_READ_COMMITTED);
+        ds.getPoolProperties().setJdbcInterceptors(
+                "org.apache.tomcat.jdbc.pool.interceptor.ConnectionState");
+        try (Connection c1 = ds.getConnection()) {
+            assertEquals(Connection.TRANSACTION_READ_COMMITTED,
+                    c1.getTransactionIsolation(), "configured default");
+            c1.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
+            assertEquals(Connection.TRANSACTION_SERIALIZABLE,
+                    c1.getTransactionIsolation());
+        }
+        try (Connection c2 = ds.getConnection()) {
+            assertEquals(Connection.TRANSACTION_READ_COMMITTED,
+                    c2.getTransactionIsolation(),
+                    "configured default must not leak to the next borrower");
+        }
+    }
+
+    @Test
+    void configuredDefaultReadOnlyIsRestoredAndNextBorrowerCanWrite() throws 
Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setDefaultReadOnly(false);
+        ds.getPoolProperties().setJdbcInterceptors(
+                "org.apache.tomcat.jdbc.pool.interceptor.ConnectionState");
+        String t = createTable(ds);
+        try (Connection c1 = ds.getConnection()) {
+            c1.setReadOnly(true);
+            assertTrue(c1.isReadOnly());
+        }
+        try (Connection c2 = ds.getConnection()) {
+            assertFalse(c2.isReadOnly(),
+                    "configured default must be restored before the next 
borrow");
+            insert(c2, t, 1);
+        }
+        assertEquals(1, adminRowCount(t));
+    }
+
+    @Test
+    void transactionsAcrossBorrowBoundariesAreIndependent() throws Exception {
+        DataSource ds = newDataSource();
+        ds.getPoolProperties().setMaxActive(2);
+        String t = createTable(ds);
+        try (Connection a = ds.getConnection(); Connection b = 
ds.getConnection()) {
+            a.setAutoCommit(false);
+            b.setAutoCommit(false);
+            insert(a, t, 1);
+            insert(b, t, 2);
+            b.rollback();
+            a.commit();
+            assertEquals(1, adminRowCount(t),
+                    "one connection's rollback must not undo the other's 
commit");
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to