From d571f1ee3384cb6010d170a164aec7c2ab3ff20d Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sat, 10 Oct 2026 23:09:46 -0600 Subject: [PATCH 1/4] Fix MySQL constructor initialization and failure cleanup --- .../api/user/userstorage/mysql/MySQL.java | 20 ++- .../bungeeapi/globaldata/GlobalMySQL.java | 22 +-- .../mysql/MySQLInitializationTest.java | 132 ++++++++++++++++++ docs/mysql-startup-failure.md | 23 +++ 4 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java create mode 100644 docs/mysql-startup-failure.md diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQL.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQL.java index dfda1db2b5..9e7a880b7c 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQL.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQL.java @@ -49,13 +49,19 @@ public MySQL(AdvancedCorePlugin plugin, String tableName, ConfigurationSection s super(tableName, new MysqlConfigSpigot(section), plugin.getOptions().getDebug().isDebug(), true); this.plugin = plugin; - init(); - - loadData(); - plugin.debug("UseBatchUpdates: " + isUseBatchUpdates()); - - // old behavior: ensure uuid type is best-case for db - alterColumnType("uuid", bestUuidType()); + // Superclass connection setup must not invoke our hooks before plugin is assigned. + // All subclass-dependent initialization belongs here, with ownership of cleanup. + try { + init(); + loadData(); + plugin.debug("UseBatchUpdates: " + isUseBatchUpdates()); + // Preserve the existing UUID schema reconciliation. + alterColumnType("uuid", bestUuidType()); + } catch (RuntimeException | Error failure) { + try { mysql.disconnect(); } + catch (RuntimeException | Error cleanupFailure) { failure.addSuppressed(cleanupFailure); } + throw failure; + } } // ------------------------- diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java index 0093e3f0df..cee037a3cd 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java @@ -106,18 +106,24 @@ public void debug(String msg) { } }; - if (!mysql.connect(config)) { - warning("Failed to connect to database (type=" + config.getDbType() + ")"); + if (!mysql.connect(config, false)) { + IllegalStateException failure = new IllegalStateException("Failed to initialize global SQL connection pool", + mysql.getLastConnectFailure()); + try { mysql.disconnect(); } + catch (RuntimeException | Error cleanupFailure) { failure.addSuppressed(cleanupFailure); } + throw failure; } // MySQL has a "USE db" command; Postgres does NOT. if (config.getDbType() != DbType.POSTGRESQL) { - try { - new Query(mysql, "USE " + quoteIdent(config.getDbType(), config.getDatabase()) + ";").executeUpdate(); - } catch (SQLException e) { - logSevere("Failed to send use database query: " + config.getDatabase() + " Error: " + e.getMessage() - + ", DB might still work"); - debugEx(e); + try (Connection connection = mysql.getConnectionManager().getConnectionChecked(); + PreparedStatement statement = connection.prepareStatement( + "USE " + quoteIdent(config.getDbType(), config.getDatabase()) + ";")) { + statement.executeUpdate(); + } catch (SQLException | RuntimeException | Error failure) { + try { mysql.disconnect(); } + catch (RuntimeException | Error cleanupFailure) { failure.addSuppressed(cleanupFailure); } + throw new IllegalStateException("Failed to initialize global SQL database selection", failure); } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java new file mode 100644 index 0000000000..12299bfa2c --- /dev/null +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java @@ -0,0 +1,132 @@ +package com.bencodez.advancedcore.api.user.userstorage.mysql; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.bukkit.configuration.file.YamlConfiguration; +import org.junit.jupiter.api.Test; +import com.bencodez.advancedcore.AdvancedCorePlugin; +import com.bencodez.advancedcore.core.user.storage.sql.MysqlUserBackend; +import com.bencodez.advancedcore.core.user.storage.sql.SqlBackendLogger; +import com.bencodez.advancedcore.core.user.storage.sql.SqlUserSchema; +import com.bencodez.simpleapi.sql.mysql.ConnectionManager; + +class MySQLInitializationTest { + @Test void rejectedAuthenticationAbortsBeforeUninitializedPluginHooksAndReleasesWorkers() throws Exception { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS); + when(plugin.getOptions().getDebug().isDebug()).thenReturn(true); + SQLException authentication = new SQLException("Access denied for test account", "28000", 1045); + ArrayList workers = new ArrayList<>(); + try (var executors = mockStatic(Executors.class); + var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(false); + when(manager.getLastOpenFailure()).thenReturn(authentication); + })) { + executors.when(() -> Executors.newFixedThreadPool(anyInt())).thenAnswer(call -> { + ExecutorService worker = mock(ExecutorService.class); workers.add(worker); return worker; + }); + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> new MySQL(plugin, "Users", config())); + assertSame(authentication, failure.getCause()); + verify(plugin, never()).getLogger(); + verify(plugin, never()).getUserManager(); + assertEquals(2, workers.size()); + workers.forEach(worker -> verify(worker).shutdown()); + verify(managers.constructed().get(0)).close(); + verify(managers.constructed().get(0), never()).getConnectionChecked(); + } + } + + @Test void sharedSqlBackendAlsoRejectsRatherThanPublishingAnUnusableProvider() throws Exception { + SQLException authentication = new SQLException("controlled authentication rejection", "28000"); + try (var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(false); + when(manager.getLastOpenFailure()).thenReturn(authentication); + })) { + var settings = new com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot(config()); + IllegalStateException failure = assertThrows(IllegalStateException.class, + () -> new MysqlUserBackend("Users", settings, SqlUserSchema.builder().build(), SqlBackendLogger.NO_OP)); + assertSame(authentication, failure.getCause()); + verify(managers.constructed().get(0)).close(); + verify(managers.constructed().get(0), never()).getConnectionChecked(); + } + } + + @Test void subclassSetupFailureClosesAlreadyOpenedPoolAndPreservesPrimaryFailure() throws Exception { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS); + RuntimeException setup = new IllegalStateException("controlled schema setup failure"); + when(plugin.getUserManager()).thenThrow(setup); + try (var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(true); + var connection = mock(java.sql.Connection.class); + when(manager.getConnectionChecked()).thenReturn(connection); + when(connection.prepareStatement(anyString())).thenReturn(mock(java.sql.PreparedStatement.class)); + })) { + assertSame(setup, assertThrows(IllegalStateException.class, () -> new MySQL(plugin, "Users", config()))); + verify(managers.constructed().get(0)).close(); + } + } + + @Test void globalSqlConstructorDoesNotCallUninitializedDiagnosticsAfterRejection() throws Exception { + SQLException authentication = new SQLException("controlled global authentication rejection", "28000"); + try (var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(false); + when(manager.getLastOpenFailure()).thenReturn(authentication); + })) { + IllegalStateException failure = assertThrows(IllegalStateException.class, () -> + new com.bencodez.advancedcore.bungeeapi.globaldata.GlobalMySQL("Global", + new com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot(config())) { + public void debugEx(Exception e) { fail("constructor callback"); } + public void debugLog(String text) { fail("constructor callback"); } + public void info(String text) { fail("constructor callback"); } + public void logSevere(String text) { fail("constructor callback"); } + public void warning(String text) { fail("constructor callback"); } + }); + assertSame(authentication, failure.getCause()); + verify(managers.constructed().get(0)).close(); + verify(managers.constructed().get(0), never()).getConnectionChecked(); + } + } + + @Test void successfulConstructionInitializesCachesAfterPluginAssignmentAndClosesNormally() throws Exception { + AdvancedCorePlugin plugin = mock(AdvancedCorePlugin.class, RETURNS_DEEP_STUBS); + when(plugin.getOptions().getDebug().isDebug()).thenReturn(true); + when(plugin.getUserManager().getDataManager().getKeys()).thenReturn(new ArrayList<>()); + try (var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(true); + when(manager.getDbType()).thenReturn(com.bencodez.simpleapi.sql.mysql.DbType.MYSQL); + var connection = mock(java.sql.Connection.class); + var statement = mock(java.sql.PreparedStatement.class); + when(manager.getConnectionChecked()).thenReturn(connection); + when(connection.prepareStatement(anyString())).thenReturn(statement); + when(statement.executeQuery()).thenAnswer(call -> mock(java.sql.ResultSet.class)); + var metadata = mock(java.sql.DatabaseMetaData.class); + when(connection.getMetaData()).thenReturn(metadata); + when(metadata.supportsBatchUpdates()).thenReturn(true); + })) { + MySQL table = new MySQL(plugin, "Users", config()); + try { + assertTrue(table.isUseBatchUpdates()); + assertEquals("Users", table.getTableName()); + assertNotNull(table.getUuids()); + verify(plugin, atLeastOnce()).getUserManager(); + verify(managers.constructed().get(0), atLeastOnce()).getConnectionChecked(); + } finally { table.close(); } + assertTrue(table.getMysql().getThreadPool().isShutdown()); + verify(managers.constructed().get(0)).close(); + } + } + + private static YamlConfiguration config() { + YamlConfiguration result = new YamlConfiguration(); + result.set("Host", "localhost"); result.set("Port", 3306); + result.set("Database", "test"); result.set("Username", "test"); + result.set("Password", "fixture"); result.set("MaxConnections", 1); + result.set("Debug", true); + return result; + } +} diff --git a/docs/mysql-startup-failure.md b/docs/mysql-startup-failure.md new file mode 100644 index 0000000000..e0b518dffa --- /dev/null +++ b/docs/mysql-startup-failure.md @@ -0,0 +1,23 @@ +# MySQL startup failures + +User storage remains fail-closed: a rejected MySQL connection aborts startup. It +never selects SQLite as a fallback. Correct the configured database credentials +or server permissions before restarting; existing tables and user data remain +unchanged. + +This fix requires the accompanying SimpleAPI SQL initialization fix. Its table +constructor retains the original connection failure and avoids invoking subclass +logging hooks before AdvancedCore assigns its plugin reference. AdvancedCore +continues to initialize its table, caches and UUID schema after that assignment. +If subclass initialization subsequently throws, it closes the owned SQL pool and +workers and preserves the primary failure, attaching cleanup failures as suppressed +exceptions. The platform-neutral MySQL backend already uses the same deferred +initialization boundary and owns equivalent failure cleanup. + +No configuration or schema migration is required. Pool initialization remains the +existing startup operation; this change does not move startup to a background +thread or change successful storage or reward processing. + +The proxy/global-data SQL constructor had the same rejected-connection continuation +pattern. It now aborts before invoking its subclass diagnostics or issuing queries, +and closes the pool if initial database selection fails. From a68cc2dfe6e44c1c0854e659c614708492fcd58d Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:18:09 -0600 Subject: [PATCH 2/4] Build merged SimpleAPI SQL lifecycle dependency in CI --- .github/workflows/maven.yml | 13 ++++++++++++- docs/mysql-startup-failure.md | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 729f8f3cbc..5b62d2add0 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -22,8 +22,19 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven + # The SQL lifecycle fix is merged upstream but the public snapshot may lag. + # Build the reviewed source so this job tests and packages the required API. + - name: Check out SimpleAPI SQL lifecycle dependency + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + repository: BenCodez/SimpleAPI + ref: fdbf26eec885ec3f1c1dbd937a243a0acfaa0a5e + path: .ci/simpleapi + persist-credentials: false + - name: Build and install SimpleAPI dependency + run: mvn -B -f .ci/simpleapi/SimpleAPI/pom.xml clean install - name: Build with Maven - run: mvn -B -f AdvancedCore/pom.xml package + run: mvn -B -nsu -f AdvancedCore/pom.xml package dependency-submission: if: github.event_name == 'push' diff --git a/docs/mysql-startup-failure.md b/docs/mysql-startup-failure.md index e0b518dffa..1fcc2220f7 100644 --- a/docs/mysql-startup-failure.md +++ b/docs/mysql-startup-failure.md @@ -21,3 +21,23 @@ thread or change successful storage or reward processing. The proxy/global-data SQL constructor had the same rejected-connection continuation pattern. It now aborts before invoking its subclass diagnostics or issuing queries, and closes the pool if initial database selection fails. + + +## Reproducing the dependency build + +SimpleAPI's SQL lifecycle fix is merged as +`fdbf26eec885ec3f1c1dbd937a243a0acfaa0a5e` (PR #111). The public +`1.0.2-SNAPSHOT` can still resolve to an older build without the required API. +GitHub CI checks out this exact reviewed commit, runs its full `clean install`, +then builds AdvancedCore against the installed artifact with snapshot refresh +disabled (`-nsu`), so the older remote snapshot cannot replace it. A failed dependency +build stops the consumer build. It does not deploy either artifact or change +runtime configuration. + +For a local build, first check out that SimpleAPI revision and run +`mvn -B -f SimpleAPI/pom.xml clean install` in its checkout, then run +`mvn -B -nsu -f AdvancedCore/pom.xml clean package` in AdvancedCore. Normal server +installation uses the shaded AdvancedCore JAR; no separate runtime SimpleAPI +installation is added. This CI source pin can be removed once the repository's +configured snapshot reliably includes the corrected lifecycle API, or updated +explicitly alongside a later dependency change. From 4ca3d3530e002bcafd0d815e752d4f61c9bd0a8f Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:56:40 -0600 Subject: [PATCH 3/4] Pin published SimpleAPI SQL lifecycle dependency for clean builds --- .github/workflows/maven.yml | 13 +------------ AdvancedCore/pom.xml | 3 ++- docs/mysql-startup-failure.md | 29 +++++++++++------------------ 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 5b62d2add0..729f8f3cbc 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -22,19 +22,8 @@ jobs: java-version: '21' distribution: 'temurin' cache: maven - # The SQL lifecycle fix is merged upstream but the public snapshot may lag. - # Build the reviewed source so this job tests and packages the required API. - - name: Check out SimpleAPI SQL lifecycle dependency - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - with: - repository: BenCodez/SimpleAPI - ref: fdbf26eec885ec3f1c1dbd937a243a0acfaa0a5e - path: .ci/simpleapi - persist-credentials: false - - name: Build and install SimpleAPI dependency - run: mvn -B -f .ci/simpleapi/SimpleAPI/pom.xml clean install - name: Build with Maven - run: mvn -B -nsu -f AdvancedCore/pom.xml package + run: mvn -B -f AdvancedCore/pom.xml package dependency-submission: if: github.event_name == 'push' diff --git a/AdvancedCore/pom.xml b/AdvancedCore/pom.xml index 67e96d3b91..0a3e8209fc 100644 --- a/AdvancedCore/pom.xml +++ b/AdvancedCore/pom.xml @@ -238,7 +238,8 @@ com.bencodez simpleapi - 1.0.2-SNAPSHOT + + 1.0.2-20261011.161601-40 compile diff --git a/docs/mysql-startup-failure.md b/docs/mysql-startup-failure.md index 1fcc2220f7..40046ccaae 100644 --- a/docs/mysql-startup-failure.md +++ b/docs/mysql-startup-failure.md @@ -23,21 +23,14 @@ pattern. It now aborts before invoking its subclass diagnostics or issuing queri and closes the pool if initial database selection fails. -## Reproducing the dependency build - -SimpleAPI's SQL lifecycle fix is merged as -`fdbf26eec885ec3f1c1dbd937a243a0acfaa0a5e` (PR #111). The public -`1.0.2-SNAPSHOT` can still resolve to an older build without the required API. -GitHub CI checks out this exact reviewed commit, runs its full `clean install`, -then builds AdvancedCore against the installed artifact with snapshot refresh -disabled (`-nsu`), so the older remote snapshot cannot replace it. A failed dependency -build stops the consumer build. It does not deploy either artifact or change -runtime configuration. - -For a local build, first check out that SimpleAPI revision and run -`mvn -B -f SimpleAPI/pom.xml clean install` in its checkout, then run -`mvn -B -nsu -f AdvancedCore/pom.xml clean package` in AdvancedCore. Normal server -installation uses the shaded AdvancedCore JAR; no separate runtime SimpleAPI -installation is added. This CI source pin can be removed once the repository's -configured snapshot reliably includes the corrected lifecycle API, or updated -explicitly alongside a later dependency change. +## Building against the published dependency + +The POM pins published SimpleAPI `1.0.2-20261011.161601-40`, which includes the +merged SQL lifecycle fix from PR #111 (`fdbf26eec885ec3f1c1dbd937a243a0acfaa0a5e`). +A normal clean checkout can run `mvn -B -f AdvancedCore/pom.xml clean package` +without checking out or installing SimpleAPI manually. CI uses the same dependency +resolution path. The immutable timestamp avoids older rolling snapshot cache entries. + +Normal server installation uses the shaded AdvancedCore JAR; no separate runtime +SimpleAPI installation, configuration change, schema migration or storage fallback +is added. The dependency must be available from the configured Maven repository. From d09f0d891127952f0787aef879380a0b8ee0dde8 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:02:16 -0600 Subject: [PATCH 4/4] Preserve unchecked global SQL initialization failures after cleanup --- .../bungeeapi/globaldata/GlobalMySQL.java | 6 ++- .../mysql/MySQLInitializationTest.java | 51 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java index cee037a3cd..f59598ca92 100644 --- a/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java +++ b/AdvancedCore/src/main/java/com/bencodez/advancedcore/bungeeapi/globaldata/GlobalMySQL.java @@ -122,7 +122,11 @@ public void debug(String msg) { statement.executeUpdate(); } catch (SQLException | RuntimeException | Error failure) { try { mysql.disconnect(); } - catch (RuntimeException | Error cleanupFailure) { failure.addSuppressed(cleanupFailure); } + catch (RuntimeException | Error cleanupFailure) { + if (cleanupFailure != failure) failure.addSuppressed(cleanupFailure); + } + if (failure instanceof RuntimeException runtime) throw runtime; + if (failure instanceof Error error) throw error; throw new IllegalStateException("Failed to initialize global SQL database selection", failure); } } diff --git a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java index 12299bfa2c..2b2dba8cad 100644 --- a/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java +++ b/AdvancedCore/src/test/java/com/bencodez/advancedcore/api/user/userstorage/mysql/MySQLInitializationTest.java @@ -121,6 +121,57 @@ class MySQLInitializationTest { } } + @Test void globalConnectionAcquisitionPreservesUncheckedFailures() throws Exception { + assertGlobalSelectionFailures(0); + } + + @Test void globalStatementPreparationPreservesUncheckedFailures() throws Exception { + assertGlobalSelectionFailures(1); + } + + @Test void globalStatementExecutionPreservesUncheckedFailures() throws Exception { + assertGlobalSelectionFailures(2); + } + + private void assertGlobalSelectionFailures(int phase) throws Exception { + for (Throwable original : new Throwable[] { new IllegalArgumentException("driver failure"), + new LinkageError("driver linkage failure"), new SQLException("database selection failure") }) { + RuntimeException cleanup = new IllegalStateException("pool cleanup failure"); + try (var managers = mockConstruction(ConnectionManager.class, (manager, context) -> { + when(manager.open()).thenReturn(true); + when(manager.getDbType()).thenReturn(com.bencodez.simpleapi.sql.mysql.DbType.MYSQL); + var connection = mock(java.sql.Connection.class); + var statement = mock(java.sql.PreparedStatement.class); + if (phase == 0) when(manager.getConnectionChecked()).thenThrow(original); + else { + when(manager.getConnectionChecked()).thenReturn(connection); + if (phase == 1) when(connection.prepareStatement(anyString())).thenThrow(original); + else { + when(connection.prepareStatement(anyString())).thenReturn(statement); + when(statement.executeUpdate()).thenThrow(original); + } + } + doThrow(cleanup).when(manager).close(); + })) { + Throwable actual = assertThrows(Throwable.class, () -> + new com.bencodez.advancedcore.bungeeapi.globaldata.GlobalMySQL("Global", + new com.bencodez.simpleapi.sql.mysql.config.MysqlConfigSpigot(config())) { + public void debugEx(Exception e) { fail("constructor callback"); } + public void debugLog(String text) { fail("constructor callback"); } + public void info(String text) { fail("constructor callback"); } + public void logSevere(String text) { fail("constructor callback"); } + public void warning(String text) { fail("constructor callback"); } + }); + if (original instanceof SQLException) { + assertInstanceOf(IllegalStateException.class, actual); + assertSame(original, actual.getCause()); + } else assertSame(original, actual); + assertArrayEquals(new Throwable[] { cleanup }, original.getSuppressed()); + verify(managers.constructed().get(0)).close(); + } + } + } + private static YamlConfiguration config() { YamlConfiguration result = new YamlConfiguration(); result.set("Host", "localhost"); result.set("Port", 3306);