This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 55cb0a3c77 [#6505] improvement(test): Reuse core database fixtures by
test class (#13518)
55cb0a3c77 is described below
commit 55cb0a3c77d3d3f2d7136fb42182eaece6ec8bc7
Author: Nevin Zheng <[email protected]>
AuthorDate: Mon Oct 19 01:45:17 2026 -0700
[#6505] improvement(test): Reuse core database fixtures by test class
(#13518)
### What changes were proposed in this pull request?
Make core database tests cheap to set up and reset, without changing
what each test sees.
- **Reuse:** each test class builds its backend once (database,
connection pool and schema) instead of once per test.
`SqlSessionFactoryHelper` is process-global, so only one backend can be
live per JVM. `BackendTestExtension` keeps it in a single slot in the
JUnit root store, builds a new one when the test class or backend type
changes, and closes the class's backend when the class finishes
(`AfterAllCallback`).
- **Reset:** before each test, the extension empties the shared
database. One query finds the tables that have rows, and `DELETE` clears
only those, in one transaction. The reset runs before the test's own
`@BeforeEach`, so test setup always starts from empty tables.
- **`@DatabaseFixture(resetStrategy = ...)`** picks how much is rebuilt
before each test, on a class or a method (a method-level fixture
replaces the class-level one):
- `ResetStrategy.DELETE_ROWS` (default): deletes the rows of every
non-empty table; `AUTO_INCREMENT` counters keep counting.
- `ResetStrategy.TRUNCATE_TABLES`: truncates every table, so counters
restart; slower.
- `ResetStrategy.FRESH_DATABASE`: a new database and backend per test.
Used by `TestSQLScripts`, `TestJobManagerMultiNode` and
`TestRelationalEntityStoreBackendLockWaitTimeout`.
- **Containers:** `AbstractEntityStorageTest` closes the Docker
containers once at the end of the test plan, instead of after every
test. `ContainerSuite` cleans the disk only when it starts a container,
and `ContainerSuite.initialized()` is removed: the close hook registers
on every run and closes whatever started.
- **Docs:** `docs/how-to-test.md` lists which database each lane uses,
how to run one class or method, and the reset strategies.
`TestJDBCBackend` no longer resets tables itself. A test that needs an
extra reset mid-test calls `resetDatabase()`. Test classes in
`iceberg-rest-server` (2) and `plugins/tms` (6) extend `TestJDBCBackend`
through core's test jar, so they get the same reuse and reset; they run
in `build (17)`.
The extension's own tests run through JUnit's `EngineTestKit` (new test
dependency `org.junit.platform:junit-platform-testkit`, matching JUnit
5.8.1). They run small fixture classes through a real JUnit engine and
check the order of builds, resets and closes, which hand-mocked
`ExtensionContext`s can't prove. The fixture classes are tagged
`gravitino-test-fixture`, and core's build excludes that tag from its
test tasks.
### Why are the changes needed?
Core database tests rebuilt the backend and schema for nearly every
test, then truncated all 46 tables before each one. On MySQL and
PostgreSQL every `TRUNCATE` recreates the table's files and syncs them
to disk, even when the table is empty: about 1.2 s per test on MySQL and
0.3 s on PostgreSQL, measured in a container with the current schema.
Together, rebuilding and truncating dominated each database lane.
Fix: #6505
Related to: #13515
### Does this PR introduce _any_ user-facing change?
No. The changes are test-only.
### How was this patch tested?
- `TestBackendTestExtension` (EngineTestKit, 12 cases): reuse within a
class; a class closing its backend when it finishes; each class closing
its backend before the next starts, with `TRUNCATE_TABLES` reaching the
backend; a backend-type change; a fresh class; a fresh method between
shared ones; a shared method in a fresh class; a subclass inheriting
`FRESH_DATABASE`; display names; a failed build failing only its test; a
failed reset discarding the backend; a failed close during a
replacement. It runs in `coreUnitTest`.
- `TestResetStrategy` checks on each backend that `DELETE_ROWS` empties
every table a test wrote to and keeps counters, and that
`TRUNCATE_TABLES` restarts them.
- All four lanes pass locally: `coreUnitTest`, `coreH2Test`,
`coreMySQLTest` and `corePostgreSQLTest`, with 0 failures; so do the
`iceberg-rest-server` and `plugins/tms` tests that extend
`TestJDBCBackend`.
Wall time on one machine (Apple Silicon, OrbStack), one round,
`--rerun-tasks`, build cache off, measured on an earlier revision with
the same reuse and reset behaviour:
| Lane | `main` | Class reuse only | This PR | Speed-up |
| --- | ---: | ---: | ---: | ---: |
| H2 (`coreH2Test`) | 4m 30s | 1m 37s | 1m 39s | 2.7× |
| MySQL (`coreMySQLTest`) | 33m 58s | 16m 07s | 6m 46s | 5.0× |
| PostgreSQL (`corePostgreSQLTest`) | 16m 22s | 9m 15s | 6m 12s | 2.6× |
| **Total** | **54m 50s** | **26m 59s** | **14m 37s** | **3.8×** |
Backend builds per lane fall from 531 to 57. H2 gains only from reuse,
because truncation is cheap in-process.
CI job times (whole job, setup included), the latest `main` run against
this PR on the same earlier revision:
| Lane | `main` | This PR | Speed-up |
| --- | ---: | ---: | ---: |
| H2 (`core-test (17, h2)`) | 7m 29s | 5m 07s | 1.5× |
| MySQL (`core-test (17, mysql)`) | 31m 09s | 9m 41s | 3.2× |
| PostgreSQL (`core-test (17, postgresql)`) | 20m 24s | 7m 28s | 2.7× |
This fixture is the template for a wider look at test-container usage in
#13900.
**Stack:** **this PR** → #13899 (lower durability)
---
core/build.gradle.kts | 7 +
.../gravitino/job/TestJobManagerMultiNode.java | 3 +
.../storage/AbstractEntityStorageTest.java | 12 +-
.../apache/gravitino/storage/TestSQLScripts.java | 3 +
.../storage/relational/BackendTestExtension.java | 311 ++++++++++-----
.../storage/relational/DatabaseFixture.java | 45 +++
.../storage/relational/DatabaseResetter.java | 146 +++++++
.../storage/relational/ResetStrategy.java | 43 ++
.../relational/TestBackendTestExtension.java | 434 +++++++++++++++++++++
.../storage/relational/TestJDBCBackend.java | 71 +---
...elationalEntityStoreBackendLockWaitTimeout.java | 37 +-
.../storage/relational/TestResetStrategy.java | 95 +++++
.../service/TestModelVersionMetaService.java | 2 +-
docs/how-to-test.md | 36 +-
gradle/libs.versions.toml | 3 +
.../AbstractIcebergCleanupJobStoreBackendTest.java | 3 +-
.../service/cleanup/TestIcebergCleanupManager.java | 2 +-
.../integration/test/container/ContainerSuite.java | 8 +-
.../test/util/CloseContainerExtension.java | 15 +-
19 files changed, 1043 insertions(+), 233 deletions(-)
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index 2c846b6e48..f281d658c9 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -80,6 +80,7 @@ dependencies {
testImplementation(libs.awaitility)
testImplementation(libs.junit.jupiter.api)
testImplementation(libs.junit.jupiter.params)
+ testImplementation(libs.junit.platform.testkit)
testImplementation(libs.mockito.core)
testImplementation(libs.mockito.inline)
testImplementation(libs.mysql.driver)
@@ -172,6 +173,12 @@ fun registerCoreTestTask(
}
}
+// Nested fixture classes TestBackendTestExtension runs through EngineTestKit;
Gradle's class scan
+// would run them directly too. Keep in sync with
TestBackendTestExtension.FIXTURE.
+tasks.withType<Test>().configureEach {
+ useJUnitPlatform { excludeTags("gravitino-test-fixture") }
+}
+
registerCoreTestTask("coreUnitTest")
registerCoreTestTask("coreH2Test", "h2")
registerCoreTestTask("coreMySQLTest", "mysql")
diff --git
a/core/src/test/java/org/apache/gravitino/job/TestJobManagerMultiNode.java
b/core/src/test/java/org/apache/gravitino/job/TestJobManagerMultiNode.java
index 487645a10d..94c84fd324 100644
--- a/core/src/test/java/org/apache/gravitino/job/TestJobManagerMultiNode.java
+++ b/core/src/test/java/org/apache/gravitino/job/TestJobManagerMultiNode.java
@@ -51,7 +51,9 @@ import org.apache.gravitino.meta.BaseMetalake;
import org.apache.gravitino.meta.JobEntity;
import org.apache.gravitino.meta.JobTemplateEntity;
import org.apache.gravitino.storage.RandomIdGenerator;
+import org.apache.gravitino.storage.relational.DatabaseFixture;
import org.apache.gravitino.storage.relational.RelationalEntityStore;
+import org.apache.gravitino.storage.relational.ResetStrategy;
import org.apache.gravitino.storage.relational.TestJDBCBackend;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
@@ -66,6 +68,7 @@ import org.junit.jupiter.api.TestTemplate;
* JobManager} and {@link LocalJobExecutor}, share the same relational
metadata store. The status
* pull and the cleanup are triggered manually, so that the test controls
which node runs them when.
*/
+@DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
public class TestJobManagerMultiNode extends TestJDBCBackend {
private static final String METALAKE = "metalake_job_multi_node";
diff --git
a/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
b/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
index b62d01b743..4e2ed6ea5e 100644
---
a/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
+++
b/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
@@ -83,8 +83,8 @@ import org.apache.gravitino.authorization.SecurableObjects;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NonEmptyEntityException;
import org.apache.gravitino.file.Fileset;
-import org.apache.gravitino.integration.test.container.ContainerSuite;
import org.apache.gravitino.integration.test.util.BaseIT;
+import org.apache.gravitino.integration.test.util.CloseContainerExtension;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.BaseMetalake;
import org.apache.gravitino.meta.CatalogEntity;
@@ -111,13 +111,14 @@ import
org.apache.gravitino.storage.relational.converters.PostgreSQLExceptionCon
import
org.apache.gravitino.storage.relational.converters.SQLExceptionConverterFactory;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.ibatis.session.SqlSession;
-import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@CoreBackend.All
+@ExtendWith(CloseContainerExtension.class)
abstract class AbstractEntityStorageTest {
protected static final Logger LOG =
LoggerFactory.getLogger(AbstractEntityStorageTest.class);
@@ -141,13 +142,6 @@ abstract class AbstractEntityStorageTest {
.toArray(Object[][]::new);
}
- @AfterEach
- void closeSuit() throws IOException {
- // todo: refactor TestEntityStorage to extend TestJDBCBackend, otherwise,
each test will start
- // and stop the container suite.
- ContainerSuite.getInstance().close();
- }
-
protected void init(String type, Config config) throws
IllegalAccessException {
Preconditions.checkArgument(StringUtils.isNotBlank(type));
File dir = new File(DB_DIR);
diff --git
a/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
b/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
index e3ccdb44cc..813934acaa 100644
--- a/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
+++ b/core/src/test/java/org/apache/gravitino/storage/TestSQLScripts.java
@@ -38,6 +38,8 @@ import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.apache.gravitino.storage.relational.DatabaseFixture;
+import org.apache.gravitino.storage.relational.ResetStrategy;
import org.apache.gravitino.storage.relational.TestJDBCBackend;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.ibatis.session.SqlSession;
@@ -45,6 +47,7 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.TestTemplate;
import org.opentest4j.AssertionFailedError;
+@DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
public class TestSQLScripts extends TestJDBCBackend {
@TestTemplate
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
index d442fb3113..0ff8685823 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestExtension.java
@@ -34,14 +34,11 @@ import static
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_M
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.ArrayList;
+import java.sql.SQLException;
import java.util.Collections;
import java.util.List;
-import java.util.Optional;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;
+import javax.annotation.Nullable;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Config;
@@ -51,54 +48,67 @@ import org.apache.gravitino.integration.test.util.BaseIT;
import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.service.EntityIdService;
import org.junit.jupiter.api.extension.AfterAllCallback;
-import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
+import org.junit.platform.commons.support.AnnotationSupport;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+/**
+ * Runs each {@code @TestTemplate} against a relational backend and injects it
into {@link
+ * TestJDBCBackend} tests.
+ *
+ * <p>The backend is the one {@link BackendTestSelector} names, which each
core database lane sets.
+ * With none selected, tests run on H2, plus MySQL and PostgreSQL when {@code
dockerTest=true}.
+ *
+ * <p>{@code SqlSessionFactoryHelper} is process-global, so only one backend
can be live in a JVM.
+ * The extension keeps it in a single slot and reuses it for the tests of one
class, emptying it
+ * before each test per the test's {@link DatabaseFixture}. It builds a new
backend when the test
+ * class or backend type changes and for every {@link
ResetStrategy#FRESH_DATABASE} test, and closes
+ * the class's backend when the class finishes.
+ *
+ * <p>The slot is read and replaced without locking, so tests must run one at
a time: the core
+ * database lanes run in one JVM with JUnit parallel execution off.
+ */
public class BackendTestExtension
- implements TestTemplateInvocationContextProvider, BeforeAllCallback,
AfterAllCallback {
+ implements TestTemplateInvocationContextProvider, AfterAllCallback {
private static final Logger LOG =
LoggerFactory.getLogger(BackendTestExtension.class);
private static final String DOCKER_TEST_FLAG = "dockerTest";
private static final ExtensionContext.Namespace NAMESPACE =
ExtensionContext.Namespace.create(BackendTestExtension.class);
- private static final String STORE_KEY = "BACKEND_MAP";
+ // Root store: one slot serves every class, and JUnit closes a leftover when
the run ends.
+ private static final String CURRENT_BACKEND = "CURRENT_BACKEND";
- @Override
- public void beforeAll(ExtensionContext context) {
- // Initialize a Map and store it at the Class level.
- // Key: backendType ("h2", "mysql"...), Value: RelationalBackend instance
- context.getStore(NAMESPACE).put(STORE_KEY, new ConcurrentHashMap<String,
RelationalBackend>());
+ private final BackendFactory backendFactory;
+
+ public BackendTestExtension() {
+ this(new DefaultBackendFactory());
+ }
+
+ BackendTestExtension(BackendFactory backendFactory) {
+ this.backendFactory = backendFactory;
}
@Override
- @SuppressWarnings("unchecked")
- public void afterAll(ExtensionContext context) {
- // Test class ended, close all started Backend
- ConcurrentHashMap<String, RelationalBackend> map =
- (ConcurrentHashMap<String, RelationalBackend>)
context.getStore(NAMESPACE).get(STORE_KEY);
-
- if (map != null) {
- map.forEach(
- (type, backend) -> {
- try {
- LOG.info("Tearing down backend: {}", type);
- backend.close();
- // H2 special cleaning logic
- if ("h2".equals(type) && backend instanceof H2BackendWrapper) {
- ((H2BackendWrapper) backend).cleanFile();
- }
- } catch (Exception e) {
- LOG.error("Failed to close backend {}", type, e);
- }
- });
+ public void afterAll(ExtensionContext context) throws Exception {
+ ExtensionContext.Store store = context.getRoot().getStore(NAMESPACE);
+ LiveBackend current = store.get(CURRENT_BACKEND, LiveBackend.class);
+ if (current != null && current.testClass ==
context.getRequiredTestClass()) {
+ closeCurrent(store);
+ }
+ }
+
+ private static void closeCurrent(ExtensionContext.Store store) throws
IOException {
+ LiveBackend current = store.remove(CURRENT_BACKEND, LiveBackend.class);
+ if (current != null) {
+ current.close();
}
}
@@ -111,35 +121,28 @@ public class BackendTestExtension
public Stream<TestTemplateInvocationContext>
provideTestTemplateInvocationContexts(
ExtensionContext context) {
String testMethodName = context.getRequiredTestMethod().getName();
- Optional<String> selectedBackend = BackendTestSelector.selectedBackend();
- if (selectedBackend.isPresent()) {
- LOG.info("Running tests with the selected {} backend.",
selectedBackend.get());
- return Stream.of(selectedBackend.get())
- .map(backendType -> new BackendInvocationContext(testMethodName,
backendType));
- }
-
- List<String> backendsToTest = new ArrayList<>();
- backendsToTest.add("h2"); // Always test with H2
-
- String dockerTest = System.getenv(DOCKER_TEST_FLAG);
- if ("true".equalsIgnoreCase(dockerTest)) {
- backendsToTest.add("mysql");
- backendsToTest.add("postgresql");
- LOG.info("Running tests with H2, MySQL, and PostgreSQL backends.");
- } else {
- LOG.info(
- "Running tests with H2 backend only. Set env var 'dockerTest=true'
to include all backends.");
- }
-
- return backendsToTest.stream()
+ return backends().stream()
.map(backendType -> new BackendInvocationContext(testMethodName,
backendType));
}
- private static class BackendInvocationContext implements
TestTemplateInvocationContext {
+ List<String> backends() {
+ List<String> backends =
+ BackendTestSelector.selectedBackend()
+ .map(List::of)
+ .orElseGet(
+ () ->
+ "true".equalsIgnoreCase(System.getenv(DOCKER_TEST_FLAG))
+ ? List.of("h2", "mysql", "postgresql")
+ : List.of("h2"));
+ LOG.info("Running tests against {}.", backends);
+ return backends;
+ }
+
+ private class BackendInvocationContext implements
TestTemplateInvocationContext {
private final String testMethodName;
private final String backendType;
- public BackendInvocationContext(String testMethodName, String backendType)
{
+ private BackendInvocationContext(String testMethodName, String
backendType) {
this.testMethodName = testMethodName;
this.backendType = backendType;
}
@@ -158,31 +161,141 @@ public class BackendTestExtension
}
}
- private static class BackendSetupCallback implements BeforeEachCallback {
+ @FunctionalInterface
+ interface BackendFactory {
+ LiveBackend create(String backendType, Class<?> testClass) throws
Exception;
+ }
+
+ private class BackendSetupCallback implements BeforeEachCallback,
AfterEachCallback {
private final String backendType;
- private final BaseIT baseIT = new BaseIT();
- public BackendSetupCallback(String backendType) {
+ private BackendSetupCallback(String backendType) {
this.backendType = backendType;
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
+ ExtensionContext.Store store = context.getRoot().getStore(NAMESPACE);
+ Class<?> testClass = context.getRequiredTestClass();
+ ResetStrategy resetStrategy = resetStrategy(context);
+ boolean fresh = resetStrategy == ResetStrategy.FRESH_DATABASE;
+ LiveBackend current = store.get(CURRENT_BACKEND, LiveBackend.class);
+ if (current != null
+ && (fresh
+ || current.testClass != testClass
+ || !current.backendType.equals(backendType))) {
+ // Only one backend can be live (see the class Javadoc), so the old
one goes first.
+ closeCurrent(store);
+ current = null;
+ }
+ if (current == null) {
+ current = backendFactory.create(backendType, testClass);
+ store.put(CURRENT_BACKEND, current);
+ }
+ // Other core tests overwrite these globals, so point them back before
every test.
+ current.bindGravitinoEnv();
+ if (!fresh) {
+ // Runs before the test's own @BeforeEach, so its setup always starts
from empty tables.
+ resetOrDiscard(store, current, resetStrategy);
+ }
- // Lazy loading: If the backend of this type has not started, start it
- RelationalBackend backend = startBackend(backendType);
-
- // Inject into the test instance
Object testInstance = context.getRequiredTestInstance();
if (testInstance instanceof TestJDBCBackend) {
- LOG.info("Injecting {} backend into test instance", backendType);
- ((TestJDBCBackend) testInstance).setBackend(backend);
+ ((TestJDBCBackend) testInstance).setBackend(current.backend);
((TestJDBCBackend) testInstance).setBackendType(backendType);
}
}
- private RelationalBackend startBackend(String type) throws Exception {
- LOG.info("Initializing backend resource: {}", type);
+ @Override
+ public void afterEach(ExtensionContext context) throws Exception {
+ if (resetStrategy(context) == ResetStrategy.FRESH_DATABASE) {
+ closeCurrent(context.getRoot().getStore(NAMESPACE));
+ }
+ }
+
+ // A reset that fails (e.g. on a lock an earlier test left) would fail
every later test the same
+ // way, so drop the backend and let the next test build a clean one.
+ private void resetOrDiscard(
+ ExtensionContext.Store store, LiveBackend current, ResetStrategy
resetStrategy)
+ throws Exception {
+ try {
+ current.reset(resetStrategy);
+ } catch (Exception e) {
+ try {
+ closeCurrent(store);
+ } catch (IOException closeFailure) {
+ e.addSuppressed(closeFailure);
+ }
+ throw e;
+ }
+ }
+
+ private ResetStrategy resetStrategy(ExtensionContext context) {
+ return AnnotationSupport.findAnnotation(
+ context.getRequiredTestMethod(), DatabaseFixture.class)
+ .or(
+ () ->
+ AnnotationSupport.findAnnotation(
+ context.getRequiredTestClass(), DatabaseFixture.class))
+ .map(DatabaseFixture::resetStrategy)
+ .orElse(ResetStrategy.DELETE_ROWS);
+ }
+ }
+
+ /**
+ * Closes with the root store, in reverse insertion order, so before the
containers {@code
+ * CloseContainerExtension} registered earlier.
+ */
+ static class LiveBackend implements ExtensionContext.Store.CloseableResource
{
+ private final String backendType;
+ private final RelationalBackend backend;
+ private final Config config;
+ @Nullable private final File h2Directory;
+ private final Class<?> testClass;
+
+ LiveBackend(
+ String backendType,
+ Class<?> testClass,
+ RelationalBackend backend,
+ Config config,
+ @Nullable File h2Directory) {
+ this.backendType = backendType;
+ this.testClass = testClass;
+ this.backend = backend;
+ this.config = config;
+ this.h2Directory = h2Directory;
+ }
+
+ void bindGravitinoEnv() throws IllegalAccessException {
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "config", config,
true);
+ FieldUtils.writeField(
+ GravitinoEnv.getInstance(), "idGenerator",
RandomIdGenerator.INSTANCE, true);
+ EntityIdService.initialize(new RelationalEntityStoreIdResolver());
+ }
+
+ void reset(ResetStrategy resetStrategy) throws SQLException {
+ DatabaseResetter.reset(backendType, resetStrategy);
+ }
+
+ @Override
+ public void close() throws IOException {
+ LOG.info("Tearing down backend: {}", backendType);
+ try {
+ backend.close();
+ } finally {
+ if (h2Directory != null) {
+ FileUtils.deleteDirectory(h2Directory);
+ }
+ }
+ }
+ }
+
+ private static class DefaultBackendFactory implements BackendFactory {
+ private final BaseIT baseIT = new BaseIT();
+
+ @Override
+ public LiveBackend create(String backendType, Class<?> testClass) throws
Exception {
+ LOG.info("Initializing backend resource: {}", backendType);
Config config = Mockito.mock(Config.class);
Mockito.when(config.get(Configs.ENTITY_STORE)).thenReturn(Configs.RELATIONAL_ENTITY_STORE);
Mockito.when(config.get(Configs.ENTITY_RELATIONAL_STORE))
@@ -194,20 +307,15 @@ public class BackendTestExtension
.thenReturn(DEFAULT_RELATIONAL_JDBC_BACKEND_MAX_WAIT_MILLISECONDS);
Mockito.when(config.get(CACHE_ENABLED)).thenReturn(true);
-
- FieldUtils.writeField(GravitinoEnv.getInstance(), "config", config,
true);
- FieldUtils.writeField(
- GravitinoEnv.getInstance(), "idGenerator",
RandomIdGenerator.INSTANCE, true);
-
- RelationalBackend backend = new JDBCBackend();
- if ("mysql".equals(type)) {
+ File h2Directory = null;
+ if ("mysql".equals(backendType)) {
String url = baseIT.startAndInitMySQLBackend();
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_URL)).thenReturn(url);
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_USER)).thenReturn("root");
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("root");
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
.thenReturn("com.mysql.cj.jdbc.Driver");
- } else if ("postgresql".equals(type)) {
+ } else if ("postgresql".equals(backendType)) {
String url = baseIT.startAndInitPGBackend();
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_URL)).thenReturn(url);
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_USER)).thenReturn("root");
@@ -215,43 +323,34 @@ public class BackendTestExtension
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER))
.thenReturn("org.postgresql.Driver");
} else {
- // H2 Logic
- String uuid = UUID.randomUUID().toString().replace("-", "");
- String jdbcPath = "/tmp/gravitino_jdbc_test_h2_" + uuid;
- File dir = new File(jdbcPath);
- if (!dir.exists() && !dir.mkdirs()) throw new IOException("Create dir
failed");
-
+ h2Directory =
Files.createTempDirectory("gravitino_jdbc_test_h2_").toFile();
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_URL))
-
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL",
jdbcPath));
+ .thenReturn(
+ String.format(
+ "jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL",
+ // Inside the directory, so deleting it removes H2's
.mv.db files too.
+ new File(h2Directory, "gravitino").getPath()));
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_USER)).thenReturn("root");
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)).thenReturn("123456");
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
-
- // Wrap it with a Wrapper so that files can be deleted during the
clean process
- backend = new H2BackendWrapper(config, jdbcPath);
}
- // close the backend before initializing to make sure the singleton
sqlSession
- // has been cleared.
- backend.close();
- backend.initialize(config);
- EntityIdService.initialize(new RelationalEntityStoreIdResolver());
- return backend;
- }
- }
-
- // A simple Wrapper solves the H2 file cleanup issue.
- public static class H2BackendWrapper extends JDBCBackend {
- private final String path;
-
- public H2BackendWrapper(Config config, String path) {
- this.path = path;
- super.initialize(config);
- }
-
- public void cleanFile() throws IOException {
- if (Files.exists(Paths.get(path))) {
- FileUtils.deleteDirectory(new File(path));
+ RelationalBackend backend = new JDBCBackend();
+ LiveBackend created = new LiveBackend(backendType, testClass, backend,
config, h2Directory);
+ try {
+ created.bindGravitinoEnv();
+ // close() first: it drops the global SqlSessionFactoryHelper session
another test (e.g.
+ // TestEntityStorage) may have left open.
+ backend.close();
+ backend.initialize(config);
+ return created;
+ } catch (Exception e) {
+ try {
+ created.close();
+ } catch (IOException closeFailure) {
+ e.addSuppressed(closeFailure);
+ }
+ throw e;
}
}
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseFixture.java
b/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseFixture.java
new file mode 100644
index 0000000000..fc3cba666f
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseFixture.java
@@ -0,0 +1,45 @@
+/*
+ * 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.gravitino.storage.relational;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Declares how much of the database is rebuilt before each test of a class or
method. A
+ * method-level fixture replaces the class-level one. Without the annotation,
a test class shares
+ * one database and {@link ResetStrategy#DELETE_ROWS} empties it before each
test.
+ *
+ * <p>{@code @Inherited} applies to classes only: a subclass method that
overrides an annotated
+ * method must repeat the annotation.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+public @interface DatabaseFixture {
+
+ /** What is rebuilt before each test. */
+ ResetStrategy resetStrategy() default ResetStrategy.DELETE_ROWS;
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseResetter.java
b/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseResetter.java
new file mode 100644
index 0000000000..cc076d7361
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/DatabaseResetter.java
@@ -0,0 +1,146 @@
+/*
+ * 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.gravitino.storage.relational;
+
+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.stream.Collectors;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.session.SqlSession;
+
+/**
+ * Empties the live backend's tables before a test, as its {@link
ResetStrategy} asks. {@link
+ * ResetStrategy#FRESH_DATABASE} has nothing to empty, so it is rejected.
+ */
+final class DatabaseResetter {
+
+ private static final int RESET_TIMEOUT_SECONDS = 30;
+
+ private DatabaseResetter() {}
+
+ static void reset(String backendType, ResetStrategy resetStrategy) throws
SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Statement statement = sqlSession.getConnection().createStatement()) {
+ // A lock left by an earlier test would block the reset forever
(PostgreSQL has no default
+ // lock timeout); fail this test instead.
+ statement.setQueryTimeout(RESET_TIMEOUT_SECONDS);
+ try {
+ List<String> allTables = tables(backendType, statement);
+ switch (resetStrategy) {
+ case DELETE_ROWS:
+ deleteRows(sqlSession.getConnection(), statement,
nonEmptyTables(statement, allTables));
+ break;
+ case TRUNCATE_TABLES:
+ // Every table, not only non-empty ones: an earlier DELETE_ROWS
test may have emptied a
+ // table but left its counter running.
+ truncate(backendType, statement, allTables);
+ break;
+ default:
+ throw new IllegalArgumentException("Nothing to reset for " +
resetStrategy);
+ }
+ } finally {
+ // H2 applies a statement's timeout to its whole session, and the pool
keeps the
+ // connection, so the test that runs next would inherit it.
+ statement.setQueryTimeout(0);
+ }
+ }
+ }
+
+ private static void deleteRows(Connection connection, Statement statement,
List<String> tables)
+ throws SQLException {
+ if (tables.isEmpty()) {
+ return;
+ }
+ // One transaction, so the deletes share one commit instead of one fsync
per table.
+ connection.setAutoCommit(false);
+ try {
+ for (String table : tables) {
+ statement.execute("DELETE FROM " + table);
+ }
+ connection.commit();
+ } catch (SQLException e) {
+ connection.rollback();
+ throw e;
+ } finally {
+ connection.setAutoCommit(true);
+ }
+ }
+
+ private static void truncate(String backendType, Statement statement,
List<String> tables)
+ throws SQLException {
+ if (tables.isEmpty()) {
+ return;
+ }
+ if ("postgresql".equalsIgnoreCase(backendType)) {
+ statement.execute(
+ tables.stream()
+ .map(table -> "TRUNCATE TABLE " + table + " RESTART IDENTITY
CASCADE;")
+ .collect(Collectors.joining("\n", "DO $$ BEGIN\n", "\nEND
$$;")));
+ return;
+ }
+ // H2 keeps identity counters on TRUNCATE unless asked; MySQL always
restarts them.
+ String restart = "h2".equalsIgnoreCase(backendType) ? " RESTART IDENTITY"
: "";
+ for (String table : tables) {
+ statement.execute("TRUNCATE TABLE " + table + restart);
+ }
+ }
+
+ private static List<String> tables(String backendType, Statement statement)
throws SQLException {
+ String query =
+ "postgresql".equalsIgnoreCase(backendType)
+ ? "SELECT table_name FROM information_schema.tables"
+ + " WHERE table_schema = current_schema() AND table_type =
'BASE TABLE'"
+ : "SHOW TABLES";
+ List<String> tables = new ArrayList<>();
+ try (ResultSet rs = statement.executeQuery(query)) {
+ while (rs.next()) {
+ tables.add(rs.getString(1));
+ }
+ }
+ return tables;
+ }
+
+ // One query finds the tables a test wrote to, so DELETE_ROWS issues one
DELETE per dirty table
+ // rather than one per table.
+ private static List<String> nonEmptyTables(Statement statement, List<String>
tables)
+ throws SQLException {
+ List<String> dirtyTables = new ArrayList<>();
+ if (tables.isEmpty()) {
+ return dirtyTables;
+ }
+ // Table names come from the database's own catalog, not from input, so
formatting them in is
+ // safe.
+ String query =
+ tables.stream()
+ .map(table -> String.format("(SELECT '%s' FROM %s LIMIT 1)",
table, table))
+ .collect(Collectors.joining(" UNION ALL "));
+ try (ResultSet rs = statement.executeQuery(query)) {
+ while (rs.next()) {
+ dirtyTables.add(rs.getString(1));
+ }
+ }
+ return dirtyTables;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/ResetStrategy.java
b/core/src/test/java/org/apache/gravitino/storage/relational/ResetStrategy.java
new file mode 100644
index 0000000000..c6f88b0185
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/ResetStrategy.java
@@ -0,0 +1,43 @@
+/*
+ * 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.gravitino.storage.relational;
+
+/**
+ * What is rebuilt before each database-backed test, cheapest first. The first
two share the test
+ * class's database; {@link #FRESH_DATABASE} builds its own.
+ */
+public enum ResetStrategy {
+ /** Rows are deleted from every non-empty table. {@code AUTO_INCREMENT}
counters keep counting. */
+ DELETE_ROWS,
+
+ /**
+ * Every table is truncated, which restarts its {@code AUTO_INCREMENT}
counters. Slower than
+ * {@link #DELETE_ROWS} because {@code TRUNCATE} recreates each table; use
it only when a test
+ * relies on the counters.
+ */
+ TRUNCATE_TABLES,
+
+ /**
+ * A new database (a new schema on PostgreSQL) and backend for the test,
closed afterwards. Use it
+ * when a test changes the schema, closes the backend itself, or leaves
writers running after it
+ * ends. Costs a full schema build per test.
+ */
+ FRESH_DATABASE
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestBackendTestExtension.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestBackendTestExtension.java
new file mode 100644
index 0000000000..091e6b82ed
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestBackendTestExtension.java
@@ -0,0 +1,434 @@
+/*
+ * 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.gravitino.storage.relational;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.platform.testkit.engine.EventConditions.displayName;
+import static org.junit.platform.testkit.engine.EventConditions.event;
+import static
org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
+import static org.junit.platform.testkit.engine.EventConditions.test;
+import static
org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
+import static org.mockito.Mockito.mock;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.gravitino.Config;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.TestTemplate;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.platform.engine.DiscoverySelector;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.testkit.engine.EngineTestKit;
+import org.junit.platform.testkit.engine.Events;
+
+/**
+ * Runs small fixture classes through a real JUnit engine and checks, from a
log of what the
+ * extension did, when it builds, resets and closes backends.
+ */
+class TestBackendTestExtension {
+
+ // Gradle would otherwise discover and run these nested fixtures directly;
core/build.gradle.kts
+ // excludes the tag. Keep the two in sync.
+ private static final String FIXTURE = "gravitino-test-fixture";
+
+ private static final List<String> LOG = new ArrayList<>();
+ private static boolean failNextCreate;
+ private static boolean failNextReset;
+ private static boolean failNextClose;
+
+ @BeforeEach
+ void resetFake() {
+ LOG.clear();
+ failNextCreate = false;
+ failNextReset = false;
+ failNextClose = false;
+ }
+
+ @Test
+ void testClassReusesOneBackend() {
+ run(SharedFixture.class).assertStatistics(stats ->
stats.succeeded(2).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 SharedFixture",
+ "reset DELETE_ROWS",
+ "reset DELETE_ROWS",
+ "close h2 SharedFixture"),
+ LOG);
+ }
+
+ @Test
+ void testEachClassClosesItsBackendBeforeTheNextStarts() {
+ run(SharedFixture.class, TruncateFixture.class)
+ .assertStatistics(stats -> stats.succeeded(3).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 SharedFixture",
+ "reset DELETE_ROWS",
+ "reset DELETE_ROWS",
+ "close h2 SharedFixture",
+ "create h2 TruncateFixture",
+ "reset TRUNCATE_TABLES",
+ "close h2 TruncateFixture"),
+ LOG);
+ }
+
+ @Test
+ void testClassClosesItsBackendWhenItFinishes() {
+ run(SharedFixture.class, UnmanagedFixture.class)
+ .assertStatistics(stats -> stats.succeeded(3).failed(0));
+
+ // UnmanagedFixture has no extension, so only SharedFixture's own afterAll
can close it first.
+ assertEquals(
+ List.of(
+ "create h2 SharedFixture",
+ "reset DELETE_ROWS",
+ "reset DELETE_ROWS",
+ "close h2 SharedFixture",
+ "unmanaged test"),
+ LOG);
+ }
+
+ @Test
+ void testResetRunsBeforeTheTestsOwnBeforeEach() {
+ run(BeforeEachFixture.class).assertStatistics(stats ->
stats.succeeded(1).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 BeforeEachFixture",
+ "reset DELETE_ROWS",
+ "test's @BeforeEach",
+ "close h2 BeforeEachFixture"),
+ LOG);
+ }
+
+ @Test
+ void testBackendTypeChangeReplacesTheBackend() {
+ run(TwoBackendFixture.class).assertStatistics(stats ->
stats.succeeded(2).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 TwoBackendFixture",
+ "reset DELETE_ROWS",
+ "close h2 TwoBackendFixture",
+ "create mysql TwoBackendFixture",
+ "reset DELETE_ROWS",
+ "close mysql TwoBackendFixture"),
+ LOG);
+ }
+
+ @Test
+ void testFreshClassGetsABackendPerTest() {
+ run(FreshFixture.class).assertStatistics(stats ->
stats.succeeded(2).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 FreshFixture",
+ "close h2 FreshFixture",
+ "create h2 FreshFixture",
+ "close h2 FreshFixture"),
+ LOG);
+ }
+
+ @Test
+ void testSubclassInheritsFreshDatabase() {
+ run(InheritedFreshFixture.class).assertStatistics(stats ->
stats.succeeded(3).failed(0));
+
+ assertEquals(3, count("create h2 InheritedFreshFixture"));
+ assertEquals(0, LOG.stream().filter(entry ->
entry.startsWith("reset")).count());
+ assertEquals(3, count("close h2 InheritedFreshFixture"));
+ }
+
+ @Test
+ void testFreshMethodReplacesTheSharedBackend() {
+ run(MixedFixture.class).assertStatistics(stats ->
stats.succeeded(3).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 MixedFixture",
+ "reset DELETE_ROWS",
+ "close h2 MixedFixture",
+ "create h2 MixedFixture",
+ "close h2 MixedFixture",
+ "create h2 MixedFixture",
+ "reset DELETE_ROWS",
+ "close h2 MixedFixture"),
+ LOG);
+ }
+
+ @Test
+ void testSharedMethodInsideFreshClass() {
+ run(FreshClassWithSharedMethodFixture.class)
+ .assertStatistics(stats -> stats.succeeded(3).failed(0));
+
+ assertEquals(
+ List.of(
+ "create h2 FreshClassWithSharedMethodFixture",
+ "reset DELETE_ROWS",
+ "reset DELETE_ROWS",
+ "close h2 FreshClassWithSharedMethodFixture",
+ "create h2 FreshClassWithSharedMethodFixture",
+ "close h2 FreshClassWithSharedMethodFixture"),
+ LOG);
+ }
+
+ @Test
+ void testFailedCreateFailsOnlyThatTest() {
+ failNextCreate = true;
+ Events events = run(SharedFixture.class);
+
+ events.assertStatistics(stats -> stats.succeeded(1).failed(1));
+ events
+ .assertThatEvents()
+ .haveExactly(1, event(test("first"),
finishedWithFailure(message("create failed"))));
+
+ assertEquals(
+ List.of(
+ "create failed",
+ "create h2 SharedFixture",
+ "reset DELETE_ROWS",
+ "close h2 SharedFixture"),
+ LOG);
+ }
+
+ @Test
+ void testFailedResetDiscardsTheBackend() {
+ failNextReset = true;
+ Events events = run(SharedFixture.class);
+
+ events.assertStatistics(stats -> stats.succeeded(1).failed(1));
+ events
+ .assertThatEvents()
+ .haveExactly(1, event(test("first"),
finishedWithFailure(message("reset failed"))));
+
+ assertEquals(
+ List.of(
+ "create h2 SharedFixture",
+ "reset failed",
+ "close h2 SharedFixture",
+ "create h2 SharedFixture",
+ "reset DELETE_ROWS",
+ "close h2 SharedFixture"),
+ LOG);
+ }
+
+ @Test
+ void testFailedCloseOnReplacementFailsTheReplacingTest() {
+ failNextClose = true;
+ Events events = run(TwoBackendFixture.class);
+
+ events.assertStatistics(stats -> stats.succeeded(1).failed(1));
+ events
+ .assertThatEvents()
+ .haveExactly(
+ 1,
+ event(
+ test(),
+ displayName("first[MYSQL Backend]"),
+ finishedWithFailure(message("close failed"))));
+
+ // The failed close leaves the slot empty, so nothing is left to close at
the end.
+ assertEquals(
+ List.of("create h2 TwoBackendFixture", "reset DELETE_ROWS", "close h2
TwoBackendFixture"),
+ LOG);
+ }
+
+ @Test
+ void testDisplayNameNamesTheBackend() {
+ List<String> names =
+ run(TwoBackendFixture.class).started().stream()
+ .map(event -> event.getTestDescriptor().getDisplayName())
+ .collect(Collectors.toList());
+
+ assertEquals(List.of("first[H2 Backend]", "first[MYSQL Backend]"), names);
+ }
+
+ private static Events run(Class<?>... fixtures) {
+ return EngineTestKit.engine("junit-jupiter")
+ // Tests assert the cross-class order of LOG; system properties must
not change it.
+ .enableImplicitConfigurationParameters(false)
+ .configurationParameter(
+ "junit.jupiter.testclass.order.default",
"org.junit.jupiter.api.ClassOrderer$ClassName")
+ .selectors(
+ Arrays.stream(fixtures)
+ .map(DiscoverySelectors::selectClass)
+ .toArray(DiscoverySelector[]::new))
+ .execute()
+ .testEvents();
+ }
+
+ private static long count(String entry) {
+ return LOG.stream().filter(entry::equals).count();
+ }
+
+ private static BackendTestExtension fakeExtension(String... backends) {
+ return new BackendTestExtension(
+ (backendType, testClass) -> {
+ if (failNextCreate) {
+ failNextCreate = false;
+ LOG.add("create failed");
+ throw new IllegalStateException("create failed");
+ }
+ String name = backendType + " " + testClass.getSimpleName();
+ LOG.add("create " + name);
+ return new BackendTestExtension.LiveBackend(
+ backendType, testClass, mock(RelationalBackend.class),
mock(Config.class), null) {
+ @Override
+ void bindGravitinoEnv() {}
+
+ @Override
+ void reset(ResetStrategy resetStrategy) {
+ if (failNextReset) {
+ failNextReset = false;
+ LOG.add("reset failed");
+ throw new IllegalStateException("reset failed");
+ }
+ LOG.add("reset " + resetStrategy);
+ }
+
+ @Override
+ public void close() throws IOException {
+ LOG.add("close " + name);
+ if (failNextClose) {
+ failNextClose = false;
+ throw new IOException("close failed");
+ }
+ }
+ };
+ }) {
+ @Override
+ List<String> backends() {
+ return List.of(backends);
+ }
+ };
+ }
+
+ @Tag(FIXTURE)
+ @TestMethodOrder(MethodOrderer.MethodName.class)
+ static class SharedFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @TestTemplate
+ void first() {}
+
+ @TestTemplate
+ void second() {}
+ }
+
+ @Tag(FIXTURE)
+ static class BeforeEachFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @BeforeEach
+ void setUp() {
+ LOG.add("test's @BeforeEach");
+ }
+
+ @TestTemplate
+ void first() {}
+ }
+
+ @Tag(FIXTURE)
+ static class UnmanagedFixture {
+ @Test
+ void first() {
+ LOG.add("unmanaged test");
+ }
+ }
+
+ @Tag(FIXTURE)
+ @DatabaseFixture(resetStrategy = ResetStrategy.TRUNCATE_TABLES)
+ static class TruncateFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @TestTemplate
+ void first() {}
+ }
+
+ @Tag(FIXTURE)
+ static class TwoBackendFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2", "mysql");
+
+ @TestTemplate
+ void first() {}
+ }
+
+ @Tag(FIXTURE)
+ @TestMethodOrder(MethodOrderer.MethodName.class)
+ @DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
+ static class FreshFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @TestTemplate
+ void first() {}
+
+ @TestTemplate
+ void second() {}
+ }
+
+ @Tag(FIXTURE)
+ @TestMethodOrder(MethodOrderer.MethodName.class)
+ static class InheritedFreshFixture extends FreshFixture {
+ @TestTemplate
+ void third() {}
+ }
+
+ @Tag(FIXTURE)
+ @TestMethodOrder(MethodOrderer.MethodName.class)
+ static class MixedFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @TestTemplate
+ void first() {}
+
+ @TestTemplate
+ @DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
+ void second() {}
+
+ @TestTemplate
+ void third() {}
+ }
+
+ @Tag(FIXTURE)
+ @TestMethodOrder(MethodOrderer.MethodName.class)
+ @DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
+ static class FreshClassWithSharedMethodFixture {
+ @RegisterExtension static final BackendTestExtension EXTENSION =
fakeExtension("h2");
+
+ @TestTemplate
+ @DatabaseFixture(resetStrategy = ResetStrategy.DELETE_ROWS)
+ void first() {}
+
+ @TestTemplate
+ @DatabaseFixture(resetStrategy = ResetStrategy.DELETE_ROWS)
+ void second() {}
+
+ @TestTemplate
+ void third() {}
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
index 41d08dff85..894937a1e6 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
@@ -29,7 +29,6 @@ import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
-import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -80,10 +79,19 @@ import org.apache.gravitino.storage.RandomIdGenerator;
import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.gravitino.utils.NamespaceUtil;
import org.apache.ibatis.session.SqlSession;
-import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
+/**
+ * Base class for tests that run against a real relational backend. Write each
test as a
+ * {@code @TestTemplate}: {@link BackendTestExtension} runs it once per
backend and sets {@link
+ * #backend} and {@link #backendType} before it. A plain {@code @Test} gets
neither, nor a reset.
+ *
+ * <p>The class shares one database; before each test, and before the test's
own
+ * {@code @BeforeEach}, the tables written to are emptied ({@link
ResetStrategy#DELETE_ROWS}). Use
+ * {@link DatabaseFixture} on the class or a method to choose another {@link
ResetStrategy}. The
+ * backend is not built yet in {@code @BeforeAll}.
+ */
@CoreBackend.All
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith({
@@ -106,59 +114,12 @@ public abstract class TestJDBCBackend {
this.backendType = backendType;
}
- @BeforeEach
- public void init() throws SQLException {
- truncateAllTables();
- }
-
- private void truncateAllTables() throws SQLException {
- try (SqlSession sqlSession =
-
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true))
{
- try (Connection connection = sqlSession.getConnection()) {
- try (Statement statement = connection.createStatement()) {
- if ("postgresql".equalsIgnoreCase(backendType)) {
- truncateAllTablesForPostgreSQL(connection);
- } else {
- String query = "SHOW TABLES";
- List<String> tableList = new ArrayList<>();
- try (ResultSet rs = statement.executeQuery(query)) {
- while (rs.next()) {
- tableList.add(rs.getString(1));
- }
- }
- for (String table : tableList) {
- statement.execute("TRUNCATE TABLE " + table);
- }
- }
- }
- }
- }
- }
-
- private void truncateAllTablesForPostgreSQL(Connection connection) throws
SQLException {
- List<String> tableList = new ArrayList<>();
- try (Statement statement = connection.createStatement()) {
- String query =
- "SELECT table_name FROM information_schema.tables WHERE table_schema
= current_schema()";
- try (ResultSet rs = statement.executeQuery(query)) {
- while (rs.next()) {
- tableList.add(rs.getString(1));
- }
- }
-
- if (tableList.isEmpty()) {
- return;
- }
-
- // Merge all TRUNCATE operations into a single DO block to improve
efficiency
- StringBuilder pgTruncateCommand = new StringBuilder("DO $$ BEGIN\n");
- for (String table : tableList) {
- pgTruncateCommand.append(
- String.format("TRUNCATE TABLE %s RESTART IDENTITY CASCADE;",
table));
- }
- pgTruncateCommand.append("END $$;");
- statement.execute(pgTruncateCommand.toString());
- }
+ /**
+ * Empties the shared database mid-test ({@link ResetStrategy#DELETE_ROWS}),
for a test that runs
+ * several scenarios in a loop.
+ */
+ protected void resetDatabase() throws SQLException {
+ DatabaseResetter.reset(backendType, ResetStrategy.DELETE_ROWS);
}
protected BaseMetalake createAndInsertMakeLake(String metalakeName) throws
IOException {
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBackendLockWaitTimeout.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBackendLockWaitTimeout.java
index 6ee5afb5d0..73f3392409 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBackendLockWaitTimeout.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreBackendLockWaitTimeout.java
@@ -18,15 +18,12 @@
*/
package org.apache.gravitino.storage.relational;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import org.apache.commons.dbcp2.BasicDataSource;
-import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.Namespace;
@@ -39,7 +36,6 @@ import
org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
import org.apache.gravitino.storage.relational.session.SqlSessions;
import org.apache.ibatis.mapping.Environment;
import org.apache.ibatis.session.Configuration;
-import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestTemplate;
@@ -50,40 +46,9 @@ import org.mockito.Mockito;
* Verifies real backend lock timeouts, transaction cleanup, and retries
through the entity store.
*/
@Tag("gravitino-docker-test")
+@DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
public class TestRelationalEntityStoreBackendLockWaitTimeout extends
TestJDBCBackend {
- @AfterEach
- void closeBackend() throws Exception {
- String databasePath = null;
- try {
- if ("h2".equals(backendType)) {
- BasicDataSource source =
- (BasicDataSource)
- SqlSessionFactoryHelper.getInstance()
- .getSqlSessionFactory()
- .getConfiguration()
- .getEnvironment()
- .getDataSource();
- databasePath =
source.getUrl().substring("jdbc:h2:file:".length()).split(";")[0];
- // The shared fixture uses DB_CLOSE_DELAY=-1, so close the engine
before deleting its files.
- try (Connection connection = source.getConnection();
- Statement statement = connection.createStatement()) {
- statement.execute("SHUTDOWN");
- }
- }
- } finally {
- try {
- backend.close();
- } finally {
- if (databasePath != null) {
- FileUtils.deleteDirectory(Path.of(databasePath).toFile());
- Files.deleteIfExists(Path.of(databasePath + ".mv.db"));
- Files.deleteIfExists(Path.of(databasePath + ".trace.db"));
- }
- }
- }
- }
-
@TestTemplate
void testDropTimeoutPreservesTableAndRetrySucceeds() throws Exception {
createParents();
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestResetStrategy.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestResetStrategy.java
new file mode 100644
index 0000000000..1a8098bbe2
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestResetStrategy.java
@@ -0,0 +1,95 @@
+/*
+ * 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.gravitino.storage.relational;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
+import org.apache.ibatis.session.SqlSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.TestTemplate;
+
+/**
+ * Checks that {@link ResetStrategy#DELETE_ROWS} and {@link
ResetStrategy#TRUNCATE_TABLES} empty the
+ * tables a test wrote to and leave counters as their Javadoc says; {@code
TestBackendTestExtension}
+ * covers {@link ResetStrategy#FRESH_DATABASE}.
+ */
+public class TestResetStrategy extends TestJDBCBackend {
+
+ @TestTemplate
+ void testDeleteRowsEmptiesEveryWrittenTable() throws SQLException {
+ insertChangeLogRow();
+ execute(
+ "INSERT INTO table_metrics (table_identifier, metric_name, metric_ts,
metric_value)"
+ + " VALUES ('table', 'metric', 0, 'value')");
+ DatabaseResetter.reset(backendType, ResetStrategy.DELETE_ROWS);
+ Assertions.assertEquals(0L, rowCount("entity_change_log"));
+ Assertions.assertEquals(0L, rowCount("table_metrics"));
+ }
+
+ @TestTemplate
+ void testDeleteRowsKeepsCounting() throws SQLException {
+ long before = insertChangeLogRow();
+ DatabaseResetter.reset(backendType, ResetStrategy.DELETE_ROWS);
+ Assertions.assertTrue(insertChangeLogRow() > before);
+ }
+
+ @TestTemplate
+ void testTruncateTablesRestartsCounters() throws SQLException {
+ insertChangeLogRow();
+ DatabaseResetter.reset(backendType, ResetStrategy.TRUNCATE_TABLES);
+ Assertions.assertEquals(1L, insertChangeLogRow());
+ }
+
+ private static void execute(String sql) throws SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Statement statement = sqlSession.getConnection().createStatement()) {
+ statement.executeUpdate(sql);
+ }
+ }
+
+ private static long rowCount(String table) throws SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Statement statement = sqlSession.getConnection().createStatement();
+ ResultSet rs = statement.executeQuery("SELECT COUNT(*) FROM " +
table)) {
+ Assertions.assertTrue(rs.next());
+ return rs.getLong(1);
+ }
+ }
+
+ private static long insertChangeLogRow() throws SQLException {
+ try (SqlSession sqlSession =
+
SqlSessionFactoryHelper.getInstance().getSqlSessionFactory().openSession(true);
+ Statement statement = sqlSession.getConnection().createStatement()) {
+ statement.executeUpdate(
+ "INSERT INTO entity_change_log"
+ + " (metalake_name, entity_type, entity_full_name, operate_type,
created_at)"
+ + " VALUES ('metalake', 'TABLE', 'table', 3, 0)",
+ Statement.RETURN_GENERATED_KEYS);
+ try (ResultSet keys = statement.getGeneratedKeys()) {
+ Assertions.assertTrue(keys.next());
+ return keys.getLong(1);
+ }
+ }
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
index a83f646909..903c751128 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestModelVersionMetaService.java
@@ -988,7 +988,7 @@ public class TestModelVersionMetaService extends
TestJDBCBackend {
@TestTemplate
public void testDeleteModelVersionsInDeletion() throws IOException,
SQLException {
for (String param : new String[] {"model", "schema", "catalog",
"metalake"}) {
- init();
+ resetDatabase();
performDeletionTestLogic(param);
}
}
diff --git a/docs/how-to-test.md b/docs/how-to-test.md
index 2e56705488..b80a329a94 100644
--- a/docs/how-to-test.md
+++ b/docs/how-to-test.md
@@ -37,26 +37,44 @@ This command runs all the unit tests and skips the
integration tests.
### Core test lanes
-`core` tests run in four lanes, one CI job each:
+`core` tests run in four lanes, one CI job each. Each database lane selects
its database through the `gravitino.core.test.backend` system property:
-| Lane | Runs | Needs Docker |
-| --- | --- | --- |
-| `./gradlew :core:coreUnitTest` | tests with no database backend | no |
-| `./gradlew :core:coreH2Test` | classes tagged for H2 | no |
-| `./gradlew :core:coreMySQLTest -PskipDockerTests=false` | classes tagged for
MySQL | yes |
-| `./gradlew :core:corePostgreSQLTest -PskipDockerTests=false` | classes
tagged for PostgreSQL | yes |
+| Lane | Runs | Database | Needs Docker |
+| --- | --- | --- | --- |
+| `./gradlew :core:coreUnitTest` | tests with no database backend | none | no |
+| `./gradlew :core:coreH2Test` | classes tagged for H2 | H2 | no |
+| `./gradlew :core:coreMySQLTest -PskipDockerTests=false` | classes tagged for
MySQL | MySQL | yes |
+| `./gradlew :core:corePostgreSQLTest -PskipDockerTests=false` | classes
tagged for PostgreSQL | PostgreSQL | yes |
A class joins a database lane through `@CoreBackend.H2`, `@CoreBackend.MySQL`,
`@CoreBackend.PostgreSQL` or `@CoreBackend.All`. Subclasses of
`TestJDBCBackend` and `AbstractEntityStorageTest` inherit `@CoreBackend.All`. A
class with none of these runs in `coreUnitTest`. A class tagged
`gravitino-docker-test` with none of these runs in no lane, and
`TestCoreLaneMembership` fails until you add one.
-When you add or move a core test, check that it runs in the lane you expect:
+To run one class or method, add `--tests` to its lane:
```shell
./gradlew :core:coreH2Test --tests
'org.apache.gravitino.storage.relational.service.TestYourService'
+./gradlew :core:coreMySQLTest -PskipDockerTests=false --tests
'org.apache.gravitino.storage.relational.service.TestYourService.testInsert'
```
If the class is not in that lane, Gradle fails with "No tests found for given
includes".
-In CI, `build (17)` skips core tests; their results appear under the
`core-test (17, <lane>)` checks. `./gradlew :core:test` still runs all core
tests in one task locally (H2 only unless you pass `-PskipDockerTests=false`),
but it is deprecated and no CI job runs core tests through it.
+#### Database reuse and reset
+
+In a database lane, each test class shares one database per backend.
`@DatabaseFixture(resetStrategy = ...)` picks how much is rebuilt before each
test, from cheapest to most isolated:
+
+- `ResetStrategy.DELETE_ROWS` (the default) deletes the rows of every
non-empty table; `AUTO_INCREMENT` counters keep counting.
+- `ResetStrategy.TRUNCATE_TABLES` truncates every table, so the counters
restart; it is slower, since `TRUNCATE` recreates each table.
+- `ResetStrategy.FRESH_DATABASE` gives each test a new database and backend.
Use it when a test changes the schema, closes the backend itself, or leaves
writers running after it ends.
+
+```java
+@DatabaseFixture(resetStrategy = ResetStrategy.FRESH_DATABASE)
+public class TestSQLScripts extends TestJDBCBackend { ... }
+```
+
+A method-level `@DatabaseFixture` replaces the class-level one. A class's
backend is closed when the class finishes.
+
+#### `:core:test`
+
+In CI, `build (17)` skips core tests; their results appear under the
`core-test (17, <lane>)` checks. `./gradlew :core:test` runs all core tests in
one task. It is deprecated, and no CI job uses it. It selects no database:
every database test runs against H2, and with `-PskipDockerTests=false` also
against MySQL and PostgreSQL, building a new backend at every change of
database. Without Docker, it runs the database tests like `coreH2Test`.
## Run the Integration Tests
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 589b91b10d..0c90a42a6f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -23,7 +23,9 @@ azure-identity = "1.13.1"
azure-storage-file-datalake = "12.20.0"
reactor-netty-http = "1.2.1"
reactor-netty-core = "1.2.1"
+# JUnit Jupiter 5.x ships with JUnit Platform 1.x of the same minor; bump them
together.
junit = "5.8.1"
+junit-platform = "1.8.1"
jackson = "2.15.2"
snakeyaml = "2.0"
gson = "2.11.0"
@@ -220,6 +222,7 @@ lombok = { group = "org.projectlombok", name = "lombok",
version.ref = "lombok"
junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api",
version.ref = "junit" }
junit-jupiter-params = { group = "org.junit.jupiter", name =
"junit-jupiter-params", version.ref = "junit" }
junit-jupiter-engine = { group = "org.junit.jupiter", name =
"junit-jupiter-engine", version.ref = "junit" }
+junit-platform-testkit = { group = "org.junit.platform", name =
"junit-platform-testkit", version.ref = "junit-platform" }
slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" }
slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref =
"slf4j" }
slf4j-jdk14 = { group = "org.slf4j", name = "slf4j-jdk14", version.ref =
"slf4j" }
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/AbstractIcebergCleanupJobStoreBackendTest.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/AbstractIcebergCleanupJobStoreBackendTest.java
index 005653da64..ae2b81d8e6 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/AbstractIcebergCleanupJobStoreBackendTest.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/AbstractIcebergCleanupJobStoreBackendTest.java
@@ -34,7 +34,8 @@ import org.junit.jupiter.api.TestTemplate;
/**
* Shared cleanup-store test logic exercised against the same relational
backend matrix as core
* metadata service tests. {@link TestJDBCBackend} initializes H2 by default,
adds MySQL and
- * PostgreSQL when {@code dockerTest=true}, and truncates all backend tables
before each invocation.
+ * PostgreSQL when {@code dockerTest=true}, and empties the tables written to
before each
+ * invocation.
*/
abstract class AbstractIcebergCleanupJobStoreBackendTest extends
TestJDBCBackend {
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/TestIcebergCleanupManager.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/TestIcebergCleanupManager.java
index a525dd3fa6..0029a89b1e 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/TestIcebergCleanupManager.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/cleanup/TestIcebergCleanupManager.java
@@ -63,7 +63,7 @@ import org.junit.jupiter.api.TestTemplate;
/**
* Exercises {@link IcebergCleanupManager} against the same relational backend
matrix as the cleanup
* store: {@link TestJDBCBackend} initializes H2 by default, adds MySQL and
PostgreSQL when {@code
- * dockerTest=true}, and truncates all backend tables before each invocation.
+ * dockerTest=true}, and empties the tables written to before each invocation.
*/
class TestIcebergCleanupManager extends TestJDBCBackend {
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
index 88e4cae939..7be314f041 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/container/ContainerSuite.java
@@ -132,10 +132,6 @@ public class ContainerSuite implements Closeable {
}
}
- public static boolean initialized() {
- return initialized;
- }
-
public static ContainerSuite getInstance() {
if (instance == null) {
synchronized (ContainerSuite.class) {
@@ -386,10 +382,10 @@ public class ContainerSuite implements Closeable {
}
public void startMySQLContainer(TestDatabaseName testDatabaseName) {
- ITUtils.cleanDisk();
if (mySQLContainer == null) {
synchronized (ContainerSuite.class) {
if (mySQLContainer == null) {
+ ITUtils.cleanDisk();
initIfNecessary();
// Start MySQL container
MySQLContainer.Builder mysqlBuilder =
@@ -443,10 +439,10 @@ public class ContainerSuite implements Closeable {
}
public void startPostgreSQLContainer(TestDatabaseName testDatabaseName,
PGImageName pgImageName) {
- ITUtils.cleanDisk();
if (!pgContainerMap.containsKey(pgImageName)) {
synchronized (ContainerSuite.class) {
if (!pgContainerMap.containsKey(pgImageName)) {
+ ITUtils.cleanDisk();
initIfNecessary();
// Start PostgreSQL container
PostgreSQLContainer.Builder pgBuilder =
diff --git
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/CloseContainerExtension.java
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/CloseContainerExtension.java
index a37a964700..28ebc741d7 100644
---
a/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/CloseContainerExtension.java
+++
b/integration-test-common/src/test/java/org/apache/gravitino/integration/test/util/CloseContainerExtension.java
@@ -32,21 +32,18 @@ import org.slf4j.LoggerFactory;
public class CloseContainerExtension implements BeforeAllCallback {
@Override
public void beforeAll(ExtensionContext extensionContext) {
- // Ensure that the container suite is initialized before closing it
- if (ContainerSuite.initialized()) {
- synchronized (CloseContainerExtension.class) {
- extensionContext
- .getRoot()
- .getStore(ExtensionContext.Namespace.GLOBAL)
- .getOrComputeIfAbsent(CloseableContainer.class);
- }
- }
+ // Store.getOrComputeIfAbsent is atomic, so concurrent classes register
the closer once.
+ extensionContext
+ .getRoot()
+ .getStore(ExtensionContext.Namespace.GLOBAL)
+ .getOrComputeIfAbsent(CloseableContainer.class);
}
private static class CloseableContainer implements
ExtensionContext.Store.CloseableResource {
private static final Logger LOGGER =
LoggerFactory.getLogger(CloseableContainer.class);
private static final ContainerSuite CONTAINER_SUITE =
ContainerSuite.getInstance();
+ // Log instead of failing: a container teardown error must not fail an
otherwise green run.
@Override
public void close() {
try {