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 105fe71472 [#13516] improvement(ci): Split core tests into explicit
database lanes (#13517)
105fe71472 is described below
commit 105fe714720f3b845f48863ef7688ed21b4296a4
Author: Nevin Zheng <[email protected]>
AuthorDate: Wed Oct 14 18:30:15 2026 -0700
[#13516] improvement(ci): Split core tests into explicit database lanes
(#13517)
### What changes were proposed in this pull request?
Split `core` tests into four Gradle lanes, each run as its own CI job,
so unit tests and each database backend report, fail and time
independently.
- `coreUnitTest` runs tests with no database backend. `coreH2Test`,
`coreMySQLTest` and `corePostgreSQLTest` each run one backend,
sequentially. A class joins a lane with
`@CoreBackend.H2/MySQL/PostgreSQL/All`.
- `TestCoreLaneMembership` fails `coreUnitTest` when a Docker-tagged
class has no backend tag, so a class can't silently drop out of every
lane.
- The `coverage` job merges the four lanes' raw JaCoCo data into one
core report.
- Three plain `@Test` methods move from `TestJobMetaService` to
`TestJobMetaServiceValidation`: they need no database, and in a
`@CoreBackend.All` class they would run three times.
- `:core:test` still works locally and prints a deprecation warning.
- `docs/how-to-test.md` describes the lanes and how to check which lane
a class runs in.
### Why are the changes needed?
`:core:test` combines unit tests with all three database backends in one
task. Failures, timing and coverage can't be told apart per backend, and
the follow-up concurrency work (#13518, #13530, #13553) has no safe
boundary to build on. Running the lanes in parallel also shortens the
critical path (see Timing).
Fix: #13516
Related to: #13515
### Does this PR introduce _any_ user-facing change?
No production API, storage format or configuration changes. Test-only
changes:
- New Gradle tasks `coreUnitTest`, `coreH2Test`, `coreMySQLTest`,
`corePostgreSQLTest`, and the test-only system property
`gravitino.core.test.backend`.
- `:core:test` is deprecated: it still runs all core tests locally but
prints a warning pointing to the lanes.
- Core `@TestTemplate` cases are now reported as `testMethod[H2
Backend]` instead of `[H2 Backend]`, in JUnit XML and HTML for the new
lanes and for legacy `:core:test`. Test history keyed on the old names
starts fresh, and exact-match `--tests` filters need the new name.
Trailing-wildcard filters such as `*[H2 Backend]` still match.
- CI checks: `build (17)` and the BackendIT checks keep their names, but
`build (17)` now skips `:core:test`, so core results appear only under
the new `core-test (17, unit|h2|mysql|postgresql)` checks. `coverage` is
now its own check. `.asf.yaml` requires no status checks by name, so
branch protection needs no change. Merge order for the follow-up PRs is
on #13515.
### How was this patch tested?
Locally (macOS, JDK 17, Docker via OrbStack) on the PR head:
- `./gradlew :core:spotlessCheck :core:compileTestJava -PskipWeb=true
-PskipDockerTests=true`
- `./gradlew :core:coreUnitTest -PskipWeb=true -PskipDockerTests=true`:
2,203 tests, 0 failures.
- `./gradlew :core:coreH2Test -PskipWeb=true -PskipDockerTests=true`:
588 tests, 0 failures; no Docker container was started.
- `./gradlew :core:corePostgreSQLTest -PskipWeb=true
-PskipDockerTests=false`: 588 tests, 0 failures, 3 skipped.
- `./gradlew :core:coreMySQLTest -PskipWeb=true
-PskipDockerTests=false`: 588 tests, 0 failures, 2 skipped.
- The three database lanes ran the same 588 tests once backend names are
normalized, and no class ran in both the unit lane and a database lane.
All 312 core test classes JUnit discovers ran in a lane.
- `TestCoreLaneMembership` fails, naming the four `TestEntityStorage*`
classes, when `@CoreBackend.All` is removed from
`AbstractEntityStorageTest`.
- `actionlint .github/workflows/build.yml`
<details><summary>Timing (CI)</summary>
Lane jobs from [run
38280654558](https://github.com/apache/gravitino/actions/runs/38280654558)
on `656282cc6`. Later commits remove code and comments and leave lane
selection unchanged; the lanes' JUnit and JaCoCo outputs now use
Gradle's default paths.
| Job | Wall time |
| --- | ---: |
| `build (17)` on `main`, core included
([1](https://github.com/apache/gravitino/actions/runs/38357234677),
[2](https://github.com/apache/gravitino/actions/runs/38245414857),
[3](https://github.com/apache/gravitino/actions/runs/38139898061)) |
1h28m – 1h46m |
| `build (17)` on this PR, core skipped | 49m30s |
| `core-test (17, unit)` | 6m04s |
| `core-test (17, h2)` | 6m59s |
| `core-test (17, postgresql)` | 19m11s |
| `core-test (17, mysql)` | 27m20s |
| `coverage` | 1m28s |
The lanes run in parallel with `build`, so the critical path is `build`
plus `coverage`, about 51m.
</details>
---------
Co-authored-by: Claude <[email protected]>
Co-authored-by: Jerry Shao <[email protected]>
---
.github/workflows/build.yml | 167 ++++++++++++++++++---
build.gradle.kts | 19 ++-
core/build.gradle.kts | 106 ++++++++++++-
.../TestJdbcPartitionStatisticStorageIT.java | 7 +-
.../storage/AbstractEntityStorageTest.java | 24 ++-
.../gravitino/storage/TestBackendTestSelector.java | 84 +++++++++++
.../storage/relational/BackendTestExtension.java | 21 ++-
.../storage/relational/BackendTestSelector.java | 71 +++++++++
.../gravitino/storage/relational/CoreBackend.java | 86 +++++++++++
.../storage/relational/TestCoreLaneMembership.java | 107 +++++++++++++
.../storage/relational/TestJDBCBackend.java | 1 +
.../relational/service/TestJobMetaService.java | 48 ------
.../service/TestJobMetaServiceValidation.java | 78 ++++++++++
docs/how-to-test.md | 23 +++
14 files changed, 759 insertions(+), 83 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 30d5754e27..98dd113bb7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -206,6 +206,8 @@ jobs:
-x :catalogs-contrib:catalog-jdbc-clickhouse:test
-x :catalogs-contrib:catalog-jdbc-hologres:test
-x :catalogs-contrib:catalog-jdbc-oceanbase:test
+ # Core's tests run in the core-test job, one lane per job;
:core:test is deprecated.
+ -x :core:test
)
if [ "${{ needs.changes.outputs.mcp_server_changes }}" != "true" ];
then
@@ -223,13 +225,156 @@ jobs:
./gradlew "${gradle_args[@]}"
- - name: Fetch base branch for coverage diff
+ - name: Upload coverage data
if: github.event_name == 'pull_request'
+ uses: actions/upload-artifact@v7
+ with:
+ name: jacoco-build
+ # build.gradle.kts anchors the artifact at the repository root, so
the report paths keep
+ # their module prefix, which jacoco_report.py uses to name modules.
+ path: |
+ build.gradle.kts
+ **/build/reports/jacoco/test/jacocoTestReport.xml
+ if-no-files-found: ignore
+ retention-days: 1
+
+ - name: Upload unit tests report
+ uses: actions/upload-artifact@v7
+ if: failure()
+ with:
+ name: unit test report
+ path: |
+ build/reports
+ catalogs-contrib/**/*.log
+ catalogs-contrib/**/*.tar
+ catalogs/**/*.log
+ catalogs/**/*.tar
+
+ core-test:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ java-version: [ 17 ]
+ # One job per core test lane, so each lane's results and coverage
inputs are inspectable on
+ # their own. Lanes are defined in core/build.gradle.kts.
+ lane: [ unit, h2, mysql, postgresql ]
+ include:
+ - lane: unit
+ task: coreUnitTest
+ skip-docker-tests: true
+ - lane: h2
+ task: coreH2Test
+ skip-docker-tests: true
+ - lane: mysql
+ task: coreMySQLTest
+ skip-docker-tests: false
+ - lane: postgresql
+ task: corePostgreSQLTest
+ skip-docker-tests: false
+ timeout-minutes: 120
+ needs: changes
+ if: >-
+ needs.changes.outputs.source_changes == 'true' &&
+ needs.changes.outputs.maintenance_module_only_changes != 'true'
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup-java-toolchains
+ with:
+ java-version: ${{ matrix.java-version }}
+
+ - name: Free up disk space
+ run: |
+ dev/ci/util_free_space.sh
+
+ - name: Run core ${{ matrix.lane }} lane
+ run: |
+ ./gradlew :core:${{ matrix.task }} \
+ -PskipWeb=true \
+ -PskipITs \
+ -PskipDockerTests=${{ matrix.skip-docker-tests }}
+
+ - name: Validate core test evidence
+ run: |
+ junit_xml="$(find "core/build/test-results/${{ matrix.task }}" \
+ -type f -name 'TEST-*.xml' -print -quit)"
+ test -n "${junit_xml}"
+ test -s "${junit_xml}"
+ test -s "build/reports/tests/core/${{ matrix.task }}/index.html"
+ test -s "core/build/jacoco/${{ matrix.task }}.exec"
+
+ - name: Upload core test evidence
+ uses: actions/upload-artifact@v7
+ with:
+ name: core-${{ matrix.lane }}-test-evidence
+ path: |
+ core/build/test-results/${{ matrix.task }}
+ build/reports/tests/core/${{ matrix.task }}
+ core/build/jacoco/${{ matrix.task }}.exec
+ if-no-files-found: error
+ retention-days: 1
+
+ - name: Upload core test report
+ uses: actions/upload-artifact@v7
+ if: failure()
+ with:
+ name: core test report ${{ matrix.lane }}
+ path: |
+ build/reports/tests/core
+ core/build/test-results
+
+ coverage:
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ needs: [ changes, build, core-test ]
+ if: >-
+ always() &&
+ github.event_name == 'pull_request' &&
+ needs.build.result == 'success' &&
+ (needs.changes.outputs.maintenance_module_only_changes == 'true' ||
+ needs['core-test'].result == 'success')
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup-java-toolchains
+ if: needs.changes.outputs.maintenance_module_only_changes != 'true'
+ with:
+ java-version: 17
+
+ - name: Fetch base branch for coverage diff
run: git fetch origin ${{ github.base_ref }} --depth=1
+ - name: Download coverage data
+ uses: actions/download-artifact@v4
+ with:
+ pattern: jacoco-*
+ merge-multiple: true
+
+ - name: Download core test evidence
+ if: needs.changes.outputs.maintenance_module_only_changes != 'true'
+ uses: actions/download-artifact@v4
+ with:
+ pattern: core-*-test-evidence
+ path: core-test-evidence
+
+ - name: Generate combined core coverage
+ if: needs.changes.outputs.maintenance_module_only_changes != 'true'
+ run: |
+ mkdir -p core/build/jacoco
+ for task in coreUnitTest coreH2Test coreMySQLTest
corePostgreSQLTest; do
+ exec_file="$(find core-test-evidence -path
"*/core/build/jacoco/${task}.exec" -print -quit)"
+ test -n "${exec_file}"
+ install -m 0644 "${exec_file}" "core/build/jacoco/${task}.exec"
+ done
+ ./gradlew \
+ :core:jacocoTestReport \
+ -PcoreSuiteCoverage=true \
+ -PskipWeb=true
+ test -s core/build/reports/jacoco/test/jacocoTestReport.xml
+
- name: Generate Coverage Report
id: coverage
- if: github.event_name == 'pull_request'
run: |
python3 dev/ci/jacoco_report.py \
--base-ref "${{ github.base_ref }}" \
@@ -240,11 +385,11 @@ jobs:
--output coverage-report.md
- name: Save PR number
- if: github.event_name == 'pull_request' &&
steps.coverage.outputs.has_reports == 'true'
+ if: steps.coverage.outputs.has_reports == 'true'
run: echo "${{ github.event.pull_request.number }}" > pr-number.txt
- name: Upload Coverage Report
- if: github.event_name == 'pull_request' &&
steps.coverage.outputs.has_reports == 'true'
+ if: steps.coverage.outputs.has_reports == 'true'
uses: actions/upload-artifact@v7
with:
name: coverage-report
@@ -253,19 +398,7 @@ jobs:
pr-number.txt
- name: Output Coverage Info
- if: github.event_name == 'pull_request' &&
steps.coverage.outputs.has_reports == 'true'
+ if: steps.coverage.outputs.has_reports == 'true'
run: |
echo "Total coverage ${{ steps.coverage.outputs.coverage-overall }}"
echo "Changed Files coverage ${{
steps.coverage.outputs.coverage-changed-files }}"
-
- - name: Upload unit tests report
- uses: actions/upload-artifact@v7
- if: failure()
- with:
- name: unit test report
- path: |
- build/reports
- catalogs-contrib/**/*.log
- catalogs-contrib/**/*.tar
- catalogs/**/*.log
- catalogs/**/*.tar
diff --git a/build.gradle.kts b/build.gradle.kts
index 0161f01f72..ea7cd68e2b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -514,9 +514,14 @@ allprojects {
val dockerTest = project.rootProject.extra["dockerTest"] as? Boolean ?:
false
param.environment("dockerTest", dockerTest.toString())
+ // Must stay inside doFirst: tasks such as :core's database lanes set
this flag in their own
+ // configuration action, which may run after this configureEach action
registers.
+ val includeDockerTaggedTests =
+
param.extensions.extraProperties.properties["includeDockerTaggedTests"] as?
Boolean
+ ?: dockerTest
val dorisMultiVersion = project.hasProperty("dorisMultiVersionTest")
param.useJUnitPlatform {
- if (!dockerTest) {
+ if (!includeDockerTaggedTests) {
excludeTags("gravitino-docker-test")
}
if (!dorisMultiVersion) {
@@ -1019,7 +1024,17 @@ subprojects {
val extraArgs = project.property("extraJvmArgs") as List<String>
jvmArgs = listOf("-Xmx4G") + extraArgs
useJUnitPlatform()
- finalizedBy(tasks.getByName("jacocoTestReport"))
+ val isCoreSuiteTask =
+ project.path == ":core" &&
+ name in setOf(
+ "coreUnitTest",
+ "coreH2Test",
+ "coreMySQLTest",
+ "corePostgreSQLTest"
+ )
+ if (!isCoreSuiteTask) {
+ finalizedBy(tasks.getByName("jacocoTestReport"))
+ }
}
}
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
index bdd8cbbfc1..2c846b6e48 100644
--- a/core/build.gradle.kts
+++ b/core/build.gradle.kts
@@ -105,11 +105,109 @@ artifacts {
add("testArtifacts", testJar)
}
-tasks.test {
- val policiesDocument = rootProject.file("docs/policies.md")
- inputs.file(policiesDocument)
- systemProperty("gravitino.repositoryRoot", rootProject.projectDir.path)
+// Core's tests run in four lanes: coreUnitTest, plus
coreH2Test/coreMySQLTest/corePostgreSQLTest.
+// A class joins a backend lane through its @CoreBackend.* annotation; see
CoreBackend.java.
+// Backend name -> the JUnit tag that admits a class to that lane. Keep in
sync with CoreBackend.
+val coreBackendTestTags =
+ linkedMapOf(
+ "h2" to "gravitino-core-h2-test",
+ "mysql" to "gravitino-core-mysql-test",
+ "postgresql" to "gravitino-core-postgresql-test"
+ )
+val coreTestBackendProperty = "gravitino.core.test.backend"
+
+fun registerCoreTestTask(
+ taskName: String,
+ backend: String? = null
+) = tasks.register<Test>(taskName) {
+ group = "verification"
+ description =
+ if (backend == null) {
+ "Runs core unit tests."
+ } else {
+ "Runs core database tests against $backend."
+ }
+
+ testClassesDirs = sourceSets["test"].output.classesDirs
+ classpath = sourceSets["test"].runtimeClasspath
+
+ reports.html.outputLocation.set(
+ rootProject.layout.buildDirectory.dir("reports/tests/core/$taskName")
+ )
+
+ useJUnitPlatform {
+ if (backend == null) {
+ // Whatever carries no backend tag (and no Docker tag) is the unit suite.
+ excludeTags(*coreBackendTestTags.values.toTypedArray(),
"gravitino-docker-test")
+ } else {
+ val ownBackendTag =
+ coreBackendTestTags[backend]
+ ?: throw GradleException("Unsupported core test backend: $backend")
+ // Plain tag include, applied by JUnit at discovery time, so classes not
tagged for this
+ // backend never show up in this lane's JUnit XML. A class tagged for
several backends
+ // runs under each of them.
+ includeTags(ownBackendTag)
+ }
+ }
+ if (backend != null) {
+ systemProperty(coreTestBackendProperty, backend)
+ // Read by the root setTestEnvironment inside doFirst, i.e. at execution
time, after every
+ // configuration action has run - so it doesn't depend on
register/configureEach ordering.
+ extensions.extraProperties["includeDockerTaggedTests"] = true
+
+ // Database tests mutate process-wide state and must remain sequential
within each lane.
+ maxParallelForks = 1
+ systemProperty("junit.jupiter.execution.parallel.enabled", "false")
+
+ if (backend != "h2") {
+ doFirst {
+ if (rootProject.extra["dockerTest"] != true) {
+ throw GradleException(
+ "$path requires Docker; use -PskipDockerTests=false with Docker
running."
+ )
+ }
+ }
+ }
+ }
+}
+
+registerCoreTestTask("coreUnitTest")
+registerCoreTestTask("coreH2Test", "h2")
+registerCoreTestTask("coreMySQLTest", "mysql")
+registerCoreTestTask("corePostgreSQLTest", "postgresql")
+
+val coreSuiteCoverage =
+
providers.gradleProperty("coreSuiteCoverage").map(String::toBoolean).orElse(false)
+val coreSuiteTaskNames =
+ listOf("coreUnitTest", "coreH2Test", "coreMySQLTest", "corePostgreSQLTest")
+val coreSuiteExecutionData =
+ coreSuiteTaskNames.map { layout.buildDirectory.file("jacoco/$it.exec") }
+tasks.named<JacocoReport>("jacocoTestReport") {
+ if (coreSuiteCoverage.get()) {
+ dependsOn(tasks.named("classes"))
+ executionData.setFrom(coreSuiteExecutionData)
+ }
+}
+
+// :core:test is kept for IDEs and local habit, and is deprecated: run a lane
instead. CI skips it
+// (`build` passes `-x :core:test`) and runs each lane in its own `core-test`
job.
+// TestRestrictedRegoGoldenFile reads docs/policies.md. It is a unit test, so
only coreUnitTest and
+// the legacy :core:test run it; the database lanes don't need the input.
+listOf("coreUnitTest", "test").forEach {
+ tasks.named<Test>(it) {
+ inputs.file(rootProject.file("docs/policies.md"))
+ systemProperty("gravitino.repositoryRoot", rootProject.projectDir.path)
+ }
+}
+
+tasks.test {
+ doFirst {
+ logger.warn(
+ "WARNING: :core:test is deprecated and does not correspond to any CI
lane. Use " +
+ "coreUnitTest, coreH2Test, coreMySQLTest, or corePostgreSQLTest
instead."
+ )
+ }
val testMode = project.properties["testMode"] as? String ?: "embedded"
if (testMode == "embedded") {
environment("GRAVITINO_HOME", project.rootDir.path)
diff --git
a/core/src/test/java/org/apache/gravitino/stats/storage/TestJdbcPartitionStatisticStorageIT.java
b/core/src/test/java/org/apache/gravitino/stats/storage/TestJdbcPartitionStatisticStorageIT.java
index b1cac18a98..38d421bd54 100644
---
a/core/src/test/java/org/apache/gravitino/stats/storage/TestJdbcPartitionStatisticStorageIT.java
+++
b/core/src/test/java/org/apache/gravitino/stats/storage/TestJdbcPartitionStatisticStorageIT.java
@@ -58,6 +58,7 @@ import
org.apache.gravitino.stats.PartitionStatisticsModification;
import org.apache.gravitino.stats.PartitionStatisticsUpdate;
import org.apache.gravitino.stats.StatisticValue;
import org.apache.gravitino.stats.StatisticValues;
+import org.apache.gravitino.storage.relational.CoreBackend;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
@@ -93,7 +94,8 @@ public class TestJdbcPartitionStatisticStorageIT {
/**
* Abstract base class containing all test logic. Each database-specific
test class extends this
- * and implements the database setup.
+ * and implements the database setup. Each subclass carries the tag of the
one backend lane it
+ * runs in.
*/
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract static class BaseJdbcPartitionStatisticStorageTest {
@@ -585,6 +587,7 @@ public class TestJdbcPartitionStatisticStorageIT {
/** MySQL-specific tests using Docker container. */
@Nested
+ @CoreBackend.MySQL
@Tag("gravitino-docker-test")
static class MySQLTest extends BaseJdbcPartitionStatisticStorageTest {
@@ -655,6 +658,7 @@ public class TestJdbcPartitionStatisticStorageIT {
/** PostgreSQL-specific tests using Docker container. */
@Nested
+ @CoreBackend.PostgreSQL
@Tag("gravitino-docker-test")
static class PostgreSQLTest extends BaseJdbcPartitionStatisticStorageTest {
@@ -728,6 +732,7 @@ public class TestJdbcPartitionStatisticStorageIT {
/** H2-specific tests using embedded in-memory database. */
@Nested
+ @CoreBackend.H2
static class H2Test extends BaseJdbcPartitionStatisticStorageTest {
private static final String H2_JDBC_URL =
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 896fe90ca4..f49c550f4e 100644
---
a/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
+++
b/core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java
@@ -52,6 +52,7 @@ import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -95,6 +96,8 @@ import org.apache.gravitino.meta.TableEntity;
import org.apache.gravitino.meta.TopicEntity;
import org.apache.gravitino.meta.UserEntity;
import org.apache.gravitino.rel.types.Type;
+import org.apache.gravitino.storage.relational.BackendTestSelector;
+import org.apache.gravitino.storage.relational.CoreBackend;
import org.apache.gravitino.storage.relational.RelationalBackend;
import org.apache.gravitino.storage.relational.RelationalEntityStore;
import org.apache.gravitino.storage.relational.RelationalGarbageCollector;
@@ -110,6 +113,7 @@ import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
[email protected]
abstract class AbstractEntityStorageTest {
protected static final Logger LOG =
LoggerFactory.getLogger(AbstractEntityStorageTest.class);
@@ -119,14 +123,18 @@ abstract class AbstractEntityStorageTest {
protected static final String H2_FILE = DB_DIR + ".mv.db";
static Object[][] storageProvider() {
- return new Object[][] {
- {"h2", true},
- {"h2", false},
- {"mysql", true},
- {"mysql", false},
- {"postgresql", true},
- {"postgresql", false}
- };
+ Object[][] backends =
+ new Object[][] {
+ {"h2", true},
+ {"h2", false},
+ {"mysql", true},
+ {"mysql", false},
+ {"postgresql", true},
+ {"postgresql", false}
+ };
+ return Arrays.stream(backends)
+ .filter(arguments -> BackendTestSelector.isSelected((String)
arguments[0]))
+ .toArray(Object[][]::new);
}
@AfterEach
diff --git
a/core/src/test/java/org/apache/gravitino/storage/TestBackendTestSelector.java
b/core/src/test/java/org/apache/gravitino/storage/TestBackendTestSelector.java
new file mode 100644
index 0000000000..7eb4760f57
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/TestBackendTestSelector.java
@@ -0,0 +1,84 @@
+/*
+ * 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;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Optional;
+import org.apache.gravitino.storage.relational.BackendTestSelector;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.ResourceAccessMode;
+import org.junit.jupiter.api.parallel.ResourceLock;
+import org.junit.jupiter.api.parallel.Resources;
+
+/** Tests backend selection for the core database test suites. */
+@ResourceLock(value = Resources.SYSTEM_PROPERTIES, mode =
ResourceAccessMode.READ_WRITE)
+public class TestBackendTestSelector {
+
+ private static final String BACKEND_PROPERTY = "gravitino.core.test.backend";
+
+ private Optional<String> originalBackend = Optional.empty();
+
+ @BeforeEach
+ void saveAndClearBackendProperty() {
+ originalBackend =
Optional.ofNullable(System.getProperty(BACKEND_PROPERTY));
+ System.clearProperty(BACKEND_PROPERTY);
+ }
+
+ @AfterEach
+ void restoreBackendProperty() {
+ System.clearProperty(BACKEND_PROPERTY);
+ originalBackend.ifPresent(value -> System.setProperty(BACKEND_PROPERTY,
value));
+ }
+
+ @Test
+ void testAbsentSelectionPreservesLegacyBehavior() {
+ assertEquals(Optional.empty(), BackendTestSelector.selectedBackend());
+ assertTrue(BackendTestSelector.isSelected("h2"));
+ assertTrue(BackendTestSelector.isSelected("mysql"));
+ assertTrue(BackendTestSelector.isSelected("postgresql"));
+ }
+
+ @Test
+ void testSelectionIsNormalizedAndValidated() {
+ System.setProperty(BACKEND_PROPERTY, " MySQL ");
+
+ assertEquals(Optional.of("mysql"), BackendTestSelector.selectedBackend());
+ assertTrue(BackendTestSelector.isSelected("MYSQL"));
+ assertFalse(BackendTestSelector.isSelected("h2"));
+
+ System.setProperty(BACKEND_PROPERTY, "unsupported");
+ assertThrows(IllegalArgumentException.class,
BackendTestSelector::selectedBackend);
+ }
+
+ @Test
+ void testStorageProviderUsesSelectedBackend() {
+ System.setProperty(BACKEND_PROPERTY, "postgresql");
+
+ assertArrayEquals(
+ new Object[][] {{"postgresql", true}, {"postgresql", false}},
+ AbstractEntityStorageTest.storageProvider());
+ }
+}
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 9961e30bb1..d442fb3113 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
@@ -38,6 +38,7 @@ import java.nio.file.Paths;
import java.util.ArrayList;
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;
@@ -109,6 +110,14 @@ public class BackendTestExtension
@Override
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
@@ -122,19 +131,25 @@ public class BackendTestExtension
"Running tests with H2 backend only. Set env var 'dockerTest=true'
to include all backends.");
}
- return backendsToTest.stream().map(BackendInvocationContext::new);
+ return backendsToTest.stream()
+ .map(backendType -> new BackendInvocationContext(testMethodName,
backendType));
}
private static class BackendInvocationContext implements
TestTemplateInvocationContext {
+ private final String testMethodName;
private final String backendType;
- public BackendInvocationContext(String backendType) {
+ public BackendInvocationContext(String testMethodName, String backendType)
{
+ this.testMethodName = testMethodName;
this.backendType = backendType;
}
@Override
public String getDisplayName(int invocationIndex) {
- return String.format("[%s Backend]", backendType.toUpperCase());
+ // No trailing "()" here: @TestTemplate methods can declare parameters
(e.g. an injected
+ // DatabaseTestContext), and a hardcoded empty parameter list would
misrepresent the
+ // method's actual signature in JUnit XML/HTML reports.
+ return String.format("%s[%s Backend]", testMethodName,
backendType.toUpperCase());
}
@Override
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestSelector.java
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestSelector.java
new file mode 100644
index 0000000000..3f794dbea8
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/BackendTestSelector.java
@@ -0,0 +1,71 @@
+/*
+ * 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.util.Locale;
+import java.util.Optional;
+import java.util.Set;
+
+/** Selects a single JDBC backend for the core database test suites. */
+public final class BackendTestSelector {
+
+ private static final String BACKEND_PROPERTY = "gravitino.core.test.backend";
+ private static final Set<String> SUPPORTED_BACKENDS = Set.of("h2", "mysql",
"postgresql");
+
+ private BackendTestSelector() {}
+
+ /**
+ * Returns the selected backend, or an empty value when the legacy
all-applicable-backends
+ * behavior should be used.
+ *
+ * @return the normalized selected backend
+ * @throws IllegalArgumentException if the configured backend is unsupported
+ */
+ public static Optional<String> selectedBackend() {
+ String configuredBackend = System.getProperty(BACKEND_PROPERTY);
+ if (configuredBackend == null) {
+ return Optional.empty();
+ }
+
+ return Optional.of(validate(configuredBackend));
+ }
+
+ /**
+ * Returns whether a backend should run under the current selection.
+ *
+ * @param backend backend to test
+ * @return true when no backend is selected or the backend matches the
selection
+ * @throws IllegalArgumentException if either backend value is unsupported
+ */
+ public static boolean isSelected(String backend) {
+ String normalizedBackend = validate(backend);
+ return selectedBackend().map(normalizedBackend::equals).orElse(true);
+ }
+
+ private static String validate(String backend) {
+ String normalizedBackend = backend.trim().toLowerCase(Locale.ROOT);
+ if (!SUPPORTED_BACKENDS.contains(normalizedBackend)) {
+ throw new IllegalArgumentException(
+ String.format(
+ "Unsupported core test backend '%s'; expected one of %s",
+ backend, SUPPORTED_BACKENDS));
+ }
+ return normalizedBackend;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/CoreBackend.java
b/core/src/test/java/org/apache/gravitino/storage/relational/CoreBackend.java
new file mode 100644
index 0000000000..893bea308f
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/CoreBackend.java
@@ -0,0 +1,86 @@
+/*
+ * 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;
+import org.junit.jupiter.api.Tag;
+
+/**
+ * Annotations that choose which core database test lane(s) a class runs in.
Each lane task ({@code
+ * coreH2Test}, {@code coreMySQLTest}, {@code corePostgreSQLTest}) includes
only classes with its
+ * tag; stack annotations to join several lanes, or use {@link All}. A class
with none of them is a
+ * unit test and runs in {@code coreUnitTest}. {@code TestCoreLaneMembership}
fails if a class
+ * carries {@code gravitino-docker-test} but no backend tag, since it would
run in no lane.
+ *
+ * <p>The lane, not the class, decides whether Docker is needed: MySQL and
PostgreSQL require it and
+ * H2 does not, so a class tagged for H2 must not need a container itself. The
tag strings must stay
+ * in sync with {@code coreBackendTestTags} in {@code core/build.gradle.kts}.
+ */
+public final class CoreBackend {
+
+ /** The JUnit tag that selects the H2 lane. */
+ public static final String H2_TAG = "gravitino-core-h2-test";
+
+ /** The JUnit tag that selects the MySQL lane. */
+ public static final String MYSQL_TAG = "gravitino-core-mysql-test";
+
+ /** The JUnit tag that selects the PostgreSQL lane. */
+ public static final String POSTGRESQL_TAG = "gravitino-core-postgresql-test";
+
+ private CoreBackend() {}
+
+ /** Pins a core database test class to the H2 lane ({@code coreH2Test}). */
+ @Documented
+ @Inherited
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @Tag(H2_TAG)
+ public @interface H2 {}
+
+ /** Pins a core database test class to the MySQL lane ({@code
coreMySQLTest}). */
+ @Documented
+ @Inherited
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @Tag(MYSQL_TAG)
+ public @interface MySQL {}
+
+ /** Pins a core database test class to the PostgreSQL lane ({@code
corePostgreSQLTest}). */
+ @Documented
+ @Inherited
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @Tag(POSTGRESQL_TAG)
+ public @interface PostgreSQL {}
+
+ /** Runs a core database test class under every backend lane. */
+ @Documented
+ @Inherited
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ @Tag(H2_TAG)
+ @Tag(MYSQL_TAG)
+ @Tag(POSTGRESQL_TAG)
+ public @interface All {}
+}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/TestCoreLaneMembership.java
b/core/src/test/java/org/apache/gravitino/storage/relational/TestCoreLaneMembership.java
new file mode 100644
index 0000000000..a3c02117bf
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/TestCoreLaneMembership.java
@@ -0,0 +1,107 @@
+/*
+ * 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.assertTrue;
+
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.commons.support.ReflectionSupport;
+
+/**
+ * Fails when a core test class runs in no lane: it carries {@code
gravitino-docker-test}, which
+ * keeps it out of {@code coreUnitTest}, but no {@link CoreBackend} tag, which
keeps it out of every
+ * database lane. Nothing else notices a class that leaves every lane at once.
+ */
+class TestCoreLaneMembership {
+
+ private static final String DOCKER_TAG = "gravitino-docker-test";
+
+ private static final Set<String> BACKEND_TAGS =
+ Set.of(CoreBackend.H2_TAG, CoreBackend.MYSQL_TAG,
CoreBackend.POSTGRESQL_TAG);
+
+ @Test
+ void everyDockerTaggedTestClassHasABackendLane() {
+ URL coreTestClasses = codeLocation(TestCoreLaneMembership.class);
+ List<String> orphans =
+ ReflectionSupport.findAllClassesInPackage(
+ "org.apache.gravitino",
+ c -> coreTestClasses.equals(codeLocation(c)) &&
declaresTests(c),
+ name -> true)
+ .stream()
+ .filter(
+ c -> {
+ Set<String> tags = effectiveTags(c);
+ return tags.contains(DOCKER_TAG)
+ && tags.stream().noneMatch(BACKEND_TAGS::contains);
+ })
+ .map(Class::getName)
+ .sorted()
+ .collect(Collectors.toList());
+
+ assertTrue(
+ orphans.isEmpty(),
+ "These classes carry gravitino-docker-test but no @CoreBackend.* tag,
so they run in no "
+ + "core test lane. Add @CoreBackend.H2/MySQL/PostgreSQL or
@CoreBackend.All: "
+ + orphans);
+ }
+
+ /**
+ * Whether JUnit would run {@code c}: concrete, with at least one {@code
@Test}-style method (all
+ * meta-annotated with {@link Testable}), own or inherited.
+ */
+ private static boolean declaresTests(Class<?> c) {
+ return !Modifier.isAbstract(c.getModifiers())
+ && !ReflectionSupport.findMethods(
+ c,
+ m -> AnnotationSupport.isAnnotated(m, Testable.class),
+ HierarchyTraversalMode.TOP_DOWN)
+ .isEmpty();
+ }
+
+ /**
+ * Own and inherited tags, plus the enclosing classes' tags for non-static
{@code @Nested}
+ * classes, which JUnit applies to them. Static member classes are
discovered as top-level classes
+ * and don't inherit their enclosing class's tags.
+ */
+ private static Set<String> effectiveTags(Class<?> c) {
+ Set<String> tags = new HashSet<>();
+ for (Class<?> k = c;
+ k != null;
+ k = Modifier.isStatic(k.getModifiers()) ? null :
k.getEnclosingClass()) {
+ AnnotationSupport.findRepeatableAnnotations(k, Tag.class).forEach(t ->
tags.add(t.value()));
+ }
+ return tags;
+ }
+
+ private static URL codeLocation(Class<?> c) {
+ return c.getProtectionDomain().getCodeSource() == null
+ ? null
+ : c.getProtectionDomain().getCodeSource().getLocation();
+ }
+}
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 5c878525c2..41d08dff85 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
@@ -84,6 +84,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExtendWith;
[email protected]
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@ExtendWith({
BackendTestExtension.class,
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaService.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaService.java
index 9ddbfe51c4..abd5802a53 100644
---
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaService.java
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaService.java
@@ -35,7 +35,6 @@ import
org.apache.gravitino.storage.relational.TestJDBCBackend;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.NamespaceUtil;
import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestTemplate;
public class TestJobMetaService extends TestJDBCBackend {
@@ -492,51 +491,4 @@ public class TestJobMetaService extends TestJDBCBackend {
.withFinishedAt(oldJob.finishedAt())
.build()));
}
-
- @Test
- public void
testUpdateJobWithMalformedIdentifierThrowsNoSuchEntityException() {
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .updateJob(NameIdentifierUtil.ofJob(METALAKE_NAME, "invalid"),
e -> e));
-
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .updateJob(
- NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX), e -> e));
- }
-
- @Test
- public void testGetJobWithMalformedIdentifierThrowsNoSuchEntityException() {
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .getJobByIdentifier(NameIdentifierUtil.ofJob(METALAKE_NAME,
"invalid")));
-
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .getJobByIdentifier(
- NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX)));
- }
-
- @Test
- public void
testDeleteJobWithMalformedIdentifierThrowsNoSuchEntityException() {
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .deleteJob(NameIdentifierUtil.ofJob(METALAKE_NAME,
"invalid")));
-
- Assertions.assertThrows(
- NoSuchEntityException.class,
- () ->
- JobMetaService.getInstance()
- .deleteJob(NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX)));
- }
}
diff --git
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaServiceValidation.java
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaServiceValidation.java
new file mode 100644
index 0000000000..2d7f6ea617
--- /dev/null
+++
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestJobMetaServiceValidation.java
@@ -0,0 +1,78 @@
+/*
+ * 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.service;
+
+import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.job.JobHandle;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/** Tests job identifier validation that does not require a JDBC backend. */
+public class TestJobMetaServiceValidation {
+
+ private static final String METALAKE_NAME = "metalake_test_job_meta_service";
+
+ @Test
+ void testUpdateJobWithMalformedIdentifierThrowsNoSuchEntityException() {
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .updateJob(NameIdentifierUtil.ofJob(METALAKE_NAME, "invalid"),
e -> e));
+
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .updateJob(
+ NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX), e -> e));
+ }
+
+ @Test
+ void testGetJobWithMalformedIdentifierThrowsNoSuchEntityException() {
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .getJobByIdentifier(NameIdentifierUtil.ofJob(METALAKE_NAME,
"invalid")));
+
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .getJobByIdentifier(
+ NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX)));
+ }
+
+ @Test
+ void testDeleteJobWithMalformedIdentifierThrowsNoSuchEntityException() {
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .deleteJob(NameIdentifierUtil.ofJob(METALAKE_NAME,
"invalid")));
+
+ Assertions.assertThrows(
+ NoSuchEntityException.class,
+ () ->
+ JobMetaService.getInstance()
+ .deleteJob(NameIdentifierUtil.ofJob(METALAKE_NAME,
JobHandle.JOB_ID_PREFIX)));
+ }
+}
diff --git a/docs/how-to-test.md b/docs/how-to-test.md
index 373a510e06..2e56705488 100644
--- a/docs/how-to-test.md
+++ b/docs/how-to-test.md
@@ -35,6 +35,29 @@ To run the unit tests, run the following command:
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:
+
+| 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 |
+
+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:
+
+```shell
+./gradlew :core:coreH2Test --tests
'org.apache.gravitino.storage.relational.service.TestYourService'
+```
+
+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.
+
## Run the Integration Tests
Gravitino has two modes to run the integration tests, the default `embedded`
mode and `deploy` mode.