This is an automated email from the ASF dual-hosted git repository.
ostinru pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudberry-pxf.git
The following commit(s) were added to refs/heads/main by this push:
new d9bded07 Run JDBC tests in Testcontainers (MVP) (#91)
d9bded07 is described below
commit d9bded07dcfc8d3cba334bfee8fd655330944f55
Author: Nikolay Antonov <[email protected]>
AuthorDate: Fri Apr 3 11:24:33 2026 +0500
Run JDBC tests in Testcontainers (MVP) (#91)
* Run JDBC test in Testcontainers
* both Ubuntu 22.04 and Rocky Linux 9
---
.github/workflows/pxf-ci.yml | 148 +++++++++-
automation/Makefile | 24 +-
automation/pom.xml | 36 ++-
.../applications/CloudberryApplication.java | 319 +++++++++++++++++++++
.../automation/applications/PXFApplication.java | 105 +++++++
.../applications/RegressApplication.java | 97 +++++++
.../ClasspathDockerContainerBuilder.java | 131 +++++++++
.../testcontainers/PXFCloudberryContainer.java | 245 ++++++++++++++++
.../resources/testcontainers/pxf-cbdb/Dockerfile | 79 +++++
.../pxf-cbdb/script/build_cloudberry.sh | 211 ++++++++++++++
.../testcontainers/pxf-cbdb/script/build_pxf.sh | 71 +++++
.../testcontainers/pxf-cbdb/script/entrypoint.sh | 299 +++++++++++++++++++
.../testcontainers/pxf-cbdb/script/pxf-env.sh | 80 ++++++
.../testcontainers/pxf-cbdb/script/utils.sh | 61 ++++
.../pxf/automation/AbstractTestcontainersTest.java | 139 +++++++++
.../pxf/automation/features/jdbc/JdbcTest.java | 91 +++---
16 files changed, 2098 insertions(+), 38 deletions(-)
diff --git a/.github/workflows/pxf-ci.yml b/.github/workflows/pxf-ci.yml
index 1195d060..ff6fc006 100644
--- a/.github/workflows/pxf-ci.yml
+++ b/.github/workflows/pxf-ci.yml
@@ -198,6 +198,39 @@ jobs:
path: /tmp/singlecluster-rocky9-image.tar
retention-days: 1
+ build-pxf-cbdb-testcontainer-image:
+ name: Build PXF-CBDB Testcontainer Image (${{ matrix.distro }})
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ include:
+ - distro: ubuntu
+ base_image:
apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest
+ image_tag: pxf/cbdb-testcontainer:1
+ - distro: rocky9
+ base_image: apache/incubator-cloudberry:cbdb-build-rocky9-latest
+ image_tag: pxf/cbdb-testcontainer-rocky9:1
+ steps:
+ - name: Checkout PXF source
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Build pxf-cbdb testcontainer image
+ run: |
+ docker build \
+ --build-arg BASE_IMAGE=${{ matrix.base_image }} \
+ -t ${{ matrix.image_tag }} \
+ automation/src/main/resources/testcontainers/pxf-cbdb
+ docker save ${{ matrix.image_tag }} >
/tmp/pxf-cbdb-testcontainer-${{ matrix.distro }}.tar
+
+ - name: Upload pxf-cbdb testcontainer image
+ uses: actions/upload-artifact@v4
+ with:
+ name: pxf-cbdb-testcontainer-image-${{ matrix.distro }}
+ path: /tmp/pxf-cbdb-testcontainer-${{ matrix.distro }}.tar
+ retention-days: 1
+
# Stage 2: Parallel test jobs using matrix strategy
pxf-test:
name: Test PXF - ${{ matrix.test_group }}
@@ -541,10 +574,123 @@ jobs:
exit 1
fi
+
+ # Stage 2c: Testcontainers-based tests
+ pxf-testcontainer-test:
+ name: "TC Test - ${{ matrix.tc_group }} (${{ matrix.use_fdw == 'true' &&
'fdw' || 'external-table' }}, ${{ matrix.distro }})"
+ needs: [build-pxf-cbdb-testcontainer-image]
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ tc_group:
+ - 'pxf-jdbc'
+ use_fdw:
+ - 'false'
+ - 'true'
+ distro:
+ - 'ubuntu'
+ - 'rocky9'
+ steps:
+ - name: Checkout PXF source
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+ submodules: true
+
+ - name: Set up JDK ${{ env.JAVA_VERSION }}
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: ${{ env.JAVA_VERSION }}
+
+ - name: Download pxf-cbdb testcontainer image
+ uses: actions/download-artifact@v4
+ with:
+ name: pxf-cbdb-testcontainer-image-${{ matrix.distro }}
+ path: /tmp
+
+ - name: Load pxf-cbdb testcontainer image
+ run: |
+ docker load < /tmp/pxf-cbdb-testcontainer-${{ matrix.distro }}.tar
+
+ - name: Build PXF stage artifacts (no unit tests)
+ run: |
+ make -C server stage-notest
+
+ - name: Run Testcontainers tests - ${{ matrix.tc_group }} (${{
matrix.use_fdw == 'true' && 'fdw' || 'external-table' }}, ${{ matrix.distro }})
+ id: run_test
+ continue-on-error: true
+ timeout-minutes: 120
+ working-directory: automation
+ env:
+ PXF_HOME: ${{ github.workspace }}/server/build/stage
+ run: |
+ make test-tc TC_GROUP=${{ matrix.tc_group }} USE_FDW=${{
matrix.use_fdw }} DISTRO=${{ matrix.distro }}
+
+ - name: Collect artifacts and generate stats
+ if: always()
+ id: collect_artifacts
+ run: |
+ mkdir -p artifacts/logs
+ TC_GROUP="${{ matrix.tc_group }}"
+ TEST_MODE="${{ matrix.use_fdw == 'true' && 'fdw' || 'external-table'
}}"
+ TEST_RESULT="${{ steps.run_test.outcome }}"
+
+ # Copy test artifacts
+ cp -r automation/automation_logs/* artifacts/ 2>/dev/null || true
+
+ TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
+ for xml in automation/target/surefire-reports/TEST-*.xml; do
+ if [ -f "$xml" ]; then
+ tests=$(grep -oP 'tests="\K\d+' "$xml" 2>/dev/null | head -1 ||
echo "0")
+ failures=$(grep -oP 'failures="\K\d+' "$xml" 2>/dev/null | head
-1 || echo "0")
+ errors=$(grep -oP 'errors="\K\d+' "$xml" 2>/dev/null | head -1
|| echo "0")
+ skipped=$(grep -oP 'skipped="\K\d+' "$xml" 2>/dev/null | head -1
|| echo "0")
+ TOTAL=$((TOTAL + tests))
+ FAILED=$((FAILED + failures + errors))
+ SKIPPED=$((SKIPPED + skipped))
+ fi
+ done
+ PASSED=$((TOTAL - FAILED - SKIPPED))
+
+ cat > artifacts/test_stats.json <<EOF
+ {
+ "group": "$TC_GROUP:$TEST_MODE:${{ matrix.distro }}",
+ "result": "$TEST_RESULT",
+ "total": $TOTAL,
+ "passed": $PASSED,
+ "failed": $FAILED,
+ "skipped": $SKIPPED
+ }
+ EOF
+
+ echo "failed_count=$FAILED" >> $GITHUB_OUTPUT
+ echo "skipped_count=$SKIPPED" >> $GITHUB_OUTPUT
+ echo "Test stats for tc:$TC_GROUP ($TEST_MODE, ${{ matrix.distro
}}): total=$TOTAL, passed=$PASSED, failed=$FAILED, skipped=$SKIPPED"
+
+ - name: Upload test artifacts
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: test-results-tc-${{ matrix.tc_group }}-${{ matrix.use_fdw ==
'true' && 'fdw' || 'external-table' }}-${{ matrix.distro }}
+ path: artifacts/**
+ if-no-files-found: ignore
+ retention-days: 7
+
+ - name: Check test result
+ if: always()
+ run: |
+ FAILED_COUNT="${{ steps.collect_artifacts.outputs.failed_count || 0
}}"
+ if [ "${{ steps.run_test.outcome }}" == "failure" ] || [
"$FAILED_COUNT" -gt 0 ]; then
+ echo "Testcontainer test ${{ matrix.tc_group }} (${{
matrix.test_mode }}, ${{ matrix.distro }}) failed (Failures: $FAILED_COUNT)"
+ exit 1
+ fi
+
# Stage 3: Summary job
test-summary:
name: Test Summary
- needs: [pxf-test, pxf-test-rocky9]
+ needs: [pxf-test, pxf-test-rocky9, pxf-testcontainer-test]
if: always()
runs-on: ubuntu-latest
steps:
diff --git a/automation/Makefile b/automation/Makefile
index a475ce95..8a1d168c 100755
--- a/automation/Makefile
+++ b/automation/Makefile
@@ -22,6 +22,8 @@ ifneq "$(GROUP)" ""
MAVEN_TEST_OPTS+= -Dgroups=$(GROUP)
endif
+EXCLUDED_GROUPS ?=
+
MAVEN_TEST_OPTS+= -Djava.awt.headless=true -DuseFDW=$(USE_FDW)
-Duser.timezone=UTC
ifneq "$(OFFLINE)" "true"
@@ -94,7 +96,11 @@ MVN=mvn
all: test
check-env:
- @if [ -z "$(PXF_HOME)" ]; then echo 'ERROR: PXF_HOME must be set'; exit
1; fi
+ @if [ -z "$(PXF_HOME)" ]; then \
+ echo 'ERROR: PXF_HOME must be set'; \
+ echo 'Example: export PXF_HOME="$(abspath
../server/build/stage)"'; \
+ exit 1; \
+ fi
symlink_pxf_jars: check-env
@if [ -d "$(PXF_HOME)/application" ]; then \
@@ -118,7 +124,7 @@ symlink_pxf_jars: check-env
fi
test: check-env clean-logs symlink_pxf_jars sync_cloud_configs
sync_jdbc_config pxf_regress
- $(MVN) $(MAVEN_TEST_OPTS) ${MAVEN_DEBUG_OPTS} test
+ $(MVN) $(MAVEN_TEST_OPTS) ${MAVEN_DEBUG_OPTS}
-DexcludedGroups=testcontainers$${EXCLUDED_GROUPS:+,$$EXCLUDED_GROUPS} test
clean: clean-logs
$(MVN) $(MAVEN_TEST_OPTS) clean
@@ -245,6 +251,20 @@ else
@ls
src/test/java/org/apache/cloudberry/pxf/automation/features/*/*Test.java | sed
's/.*\///g' | sed 's/\.java//g' | awk '{print "* ", $$1}'
endif
+# Run Testcontainers-based tests.
+# Usage:
+# make test-tc => run all testcontainers tests
(Ubuntu)
+# make test-tc TC_GROUP=pxf-jdbc => run only pxf-jdbc group
+# make test-tc DISTRO=rocky9 => run with Rocky Linux 9 base image
+.PHONY: test-tc
+test-tc: check-env symlink_pxf_jars pxf_regress
+ $(MVN) -B -e -Djava.awt.headless=true -Duser.timezone=UTC \
+ -DuseFDW=$(USE_FDW) \
+ -Dpxf.test.distro=$(or $(DISTRO),$(PXF_TEST_DISTRO),ubuntu) \
+ -Dgroups=$(or $(TC_GROUP),testcontainers) \
+ -DexcludedGroups= \
+ test
+
.PHONY: pxf_regress
pxf_regress:
$(MAKE) -C pxf_regress
diff --git a/automation/pom.xml b/automation/pom.xml
index e294cac0..0c11adb0 100644
--- a/automation/pom.xml
+++ b/automation/pom.xml
@@ -76,6 +76,12 @@
</plugins>
<resources>
+ <resource>
+ <directory>src/main/resources</directory>
+ <includes>
+ <include>**/*</include>
+ </includes>
+ </resource>
<resource>
<directory>src/test/resources</directory>
<includes>
@@ -177,6 +183,30 @@
<version>4.2.0</version>
</dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <version>2.0.3</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.17.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.17.0</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-compress</artifactId>
+ <version>1.26.2</version>
+ </dependency>
+
<dependency>
<groupId>org.jsystemtest</groupId>
<artifactId>jsystemCore</artifactId>
@@ -262,19 +292,19 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
- <version>2.14.3</version>
+ <version>2.20.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
- <version>2.14.3</version>
+ <version>2.20.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
- <version>2.14.3</version>
+ <version>2.20</version>
</dependency>
<dependency>
diff --git
a/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/CloudberryApplication.java
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/CloudberryApplication.java
new file mode 100644
index 00000000..5150a6fd
--- /dev/null
+++
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/CloudberryApplication.java
@@ -0,0 +1,319 @@
+package org.apache.cloudberry.pxf.automation.applications;
+
+/*
+ * 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.
+ */
+
+import com.google.common.collect.Lists;
+import org.apache.cloudberry.pxf.automation.structures.tables.basic.Table;
+import
org.apache.cloudberry.pxf.automation.structures.tables.pxf.ExternalTable;
+import
org.apache.cloudberry.pxf.automation.testcontainers.PXFCloudberryContainer;
+import org.postgresql.copy.CopyManager;
+import org.postgresql.core.BaseConnection;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * TestObject that provides methods to work with Cloudberry DB
+ */
+public class CloudberryApplication implements AutoCloseable {
+
+ private static final int MAX_RETRIES = 10;
+ private static final long RETRY_INTERVAL_MS = 5_000;
+
+ private final PXFCloudberryContainer container;
+ private final String jdbcUrl;
+ private final String userName;
+ private Connection connection;
+ private Statement statement;
+
+ public CloudberryApplication(PXFCloudberryContainer container) {
+ this.container = container;
+ this.jdbcUrl = getCloudberryMappedJdbcUrl();
+ this.userName = container.getCloudberryUser();
+ }
+
+ public CloudberryApplication(PXFCloudberryContainer container, String
dbName) {
+ this.container = container;
+ this.jdbcUrl = getCloudberryMappedJdbcUrl(dbName);
+ this.userName = container.getCloudberryUser();
+ }
+
+ public void connect() throws Exception {
+ if (statement != null) {
+ return;
+ }
+ Properties props = new Properties();
+ if (userName != null) {
+ props.setProperty("user", userName);
+ }
+
+ Exception lastException = null;
+ for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ Class.forName("org.postgresql.Driver");
+ connection = DriverManager.getConnection(jdbcUrl, props);
+ statement = connection.createStatement();
+ System.out.println("[CloudberryApplication] Connected to " +
jdbcUrl);
+ return;
+ } catch (Exception e) {
+ lastException = e;
+ System.out.println("[CloudberryApplication] Connection attempt
" + attempt + " failed: " + e.getMessage());
+ Thread.sleep(RETRY_INTERVAL_MS);
+ }
+ }
+ throw new RuntimeException("Failed to connect to CBDB at " + jdbcUrl +
" after " + MAX_RETRIES + " attempts", lastException);
+ }
+
+ public String getCloudberryMappedJdbcUrl() {
+ return getCloudberryMappedJdbcUrl("pxfautomation");
+ }
+
+ public String getCloudberryMappedJdbcUrl(String dbName) {
+ return "jdbc:postgresql://localhost:" +
container.getCloudberryMappedPort() + "/" + dbName;
+ }
+
+ public String getCloudberryInternalJdbcUrl() {
+ return getCloudberryInternalJdbcUrl("pxfautomation");
+ }
+
+ public String getCloudberryInternalJdbcUrl(String dbName) {
+ return "jdbc:postgresql://localhost:" +
container.getCloudberryInternalPort() + "/" + dbName;
+ }
+
+
+ /**
+ * Drops (if exists) and creates the table, then verifies it exists.
+ */
+ public void createTableAndVerify(Table table) throws Exception {
+ dropTable(table, true);
+ runQuery(table.constructCreateStmt());
+ if (!checkTableExists(table)) {
+ throw new RuntimeException("Table " + table.getName() + " does not
exist after creation");
+ }
+ }
+
+ public void dropTable(Table table, boolean cascade) throws Exception {
+ runQuery(table.constructDropStmt(cascade), true);
+ if (table instanceof ExternalTable) {
+ String dropForeign = String.format("DROP FOREIGN TABLE IF EXISTS
%s%s",
+ table.getFullName(), cascade ? " CASCADE" : "");
+ runQuery(dropForeign, true);
+ }
+ }
+
+ /**
+ * Loads data from a file into a table using PostgreSQL COPY protocol.
+ * Uses {@link CopyManager} over JDBC instead of psql over SSH.
+ */
+ public void copyFromFile(Table table, File path, String delimiter, String
nullChar, boolean csv) throws Exception {
+ StringBuilder copyCmd = new StringBuilder();
+ copyCmd.append("COPY ").append(table.getName()).append(" FROM STDIN");
+
+ String copyParams = buildCopyParams(delimiter, nullChar, csv);
+ if (!copyParams.isEmpty()) {
+ copyCmd.append(" ").append(copyParams);
+ }
+
+ CopyManager copyManager = new
CopyManager(connection.unwrap(BaseConnection.class));
+ try (BufferedReader reader = new BufferedReader(new FileReader(path)))
{
+ long rows = copyManager.copyIn(copyCmd.toString(), reader);
+ System.out.println("[CloudberryApplication] COPY loaded " + rows +
" rows into " + table.getName());
+ }
+ }
+
+ /**
+ * Inserts rows from a source Table (in-memory data) into the target table.
+ */
+ public void insertData(Table source, Table target) throws Exception {
+ List<List<String>> data = source.getData();
+ if (data == null || data.isEmpty()) {
+ return;
+ }
+
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < data.size(); i++) {
+ List<String> row = data.get(i);
+ sb.append("(");
+ for (int j = 0; j < row.size(); j++) {
+ sb.append("E'").append(row.get(j)).append("'");
+ if (j < row.size() - 1) {
+ sb.append(",");
+ }
+ }
+ sb.append(")");
+ if (i < data.size() - 1) {
+ sb.append(",");
+ }
+ }
+
+ String query = "INSERT INTO " + target.getName() + " VALUES " +
sb.toString();
+ runQuery(query);
+ }
+
+ public void runQuery(String sql) throws Exception {
+ runQuery(sql, false);
+ }
+
+ public void runQuery(String sql, boolean ignoreFail) throws Exception {
+ try {
+ statement.execute(sql);
+ } catch (SQLException e) {
+ if (!ignoreFail) {
+ throw e;
+ }
+ }
+ }
+
+ public void createDatabase(String dbName) throws Exception {
+ try {
+ runQuery("CREATE DATABASE " + dbName);
+
+ runQuery("ALTER DATABASE " + dbName + " SET bytea_output TO
'escape'", true);
+
+ // This GUC has a default value of 1 in PG12 and thus the columns
of type REAL display one digit extra.
+ // So to keep the behavior consistent with previous version, we're
setting this GUC value to 0.
+ runQuery("ALTER DATABASE " + dbName + " SET extra_float_digits=0",
true);
+ } catch (Exception e) {
+ if (!e.getMessage().contains("already exists")) {
+ throw e;
+ }
+ }
+ }
+
+ public void createExtension(String extensionName, boolean ignoreFail)
throws Exception {
+ runQuery("CREATE EXTENSION IF NOT EXISTS " + extensionName,
ignoreFail);
+ }
+
+
+ public void createTestFDW(boolean ignoreFail) throws Exception {
+ runQuery("DROP FOREIGN DATA WRAPPER IF EXISTS test_pxf_fdw CASCADE",
ignoreFail);
+ runQuery("CREATE FOREIGN DATA WRAPPER test_pxf_fdw HANDLER
pxf_fdw_handler " +
+ "VALIDATOR pxf_fdw_validator OPTIONS (protocol 'test',
mpp_execute 'all segments')", ignoreFail);
+ }
+
+ public void createSystemFDW(boolean ignoreFail) throws Exception {
+ runQuery("DROP FOREIGN DATA WRAPPER IF EXISTS system_pxf_fdw CASCADE",
ignoreFail);
+ runQuery("CREATE FOREIGN DATA WRAPPER system_pxf_fdw HANDLER
pxf_fdw_handler " +
+ "VALIDATOR pxf_fdw_validator OPTIONS (protocol 'system',
mpp_execute 'all segments')", ignoreFail);
+ }
+ public void createForeignServers(boolean ignoreFail) throws Exception {
+ List<String> servers = Lists.newArrayList(
+ "default_hdfs",
+ "default_hive",
+ "db-hive_jdbc", // Needed for JdbcHiveTest
+ "default_hbase",
+ "default_jdbc", // Needed for JdbcHiveTest and other JdbcTest
which refers to the default server.
+ "database_jdbc",
+ "db-session-params_jdbc",
+ "default_file",
+ "default_s3",
+ "default_gs",
+ "default_abfss",
+ "default_wasbs",
+ "s3_s3",
+ "s3-invalid_s3",
+ "s3-non-existent_s3",
+ "hdfs-non-secure_hdfs",
+ "hdfs-secure_hdfs",
+ "hdfs-ipa_hdfs",
+ "default_test",
+ "default_system");
+
+ for (String server : servers) {
+ String foreignServerName = server.replace("-", "_");
+ String pxfServerName = server.substring(0,
server.lastIndexOf("_")); // strip protocol at the end
+ String fdwName = server.substring(server.lastIndexOf("_") + 1) +
"_pxf_fdw"; // strip protocol at the end
+ runQuery(String.format("CREATE SERVER IF NOT EXISTS %s FOREIGN
DATA WRAPPER %s OPTIONS(config '%s')",
+ foreignServerName, fdwName, pxfServerName), ignoreFail);
+ runQuery(String.format("CREATE USER MAPPING IF NOT EXISTS FOR
CURRENT_USER SERVER %s", foreignServerName),
+ ignoreFail);
+ }
+ }
+
+ public boolean checkDatabaseExists(String dbName) throws Exception {
+ ResultSet rs = statement.executeQuery(
+ "SELECT 1 FROM pg_database WHERE datname = '" + dbName + "'");
+ return rs.next();
+ }
+
+ public boolean checkTableExists(Table table) throws Exception {
+ DatabaseMetaData meta = connection.getMetaData();
+ String schema = table.getSchema();
+ if (schema == null) {
+ schema = "public";
+ }
+ ResultSet rs = meta.getTables(null, schema, table.getName(), null);
+ return rs.next();
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public PXFCloudberryContainer getContainer() {
+ return container;
+ }
+
+ @Override
+ public void close() throws Exception {
+ if (statement != null) {
+ try { statement.close(); } catch (Exception ignored) {}
+ statement = null;
+ }
+ if (connection != null) {
+ try { connection.close(); } catch (Exception ignored) {}
+ connection = null;
+ }
+ }
+
+ private String buildCopyParams(String delimiter, String nullChar, boolean
csv) {
+ StringBuilder params = new StringBuilder();
+ if (csv) {
+ params.append("CSV ");
+ }
+ if (delimiter != null) {
+ params.append("DELIMITER
E'").append(stripEQuote(delimiter)).append("' ");
+ }
+ if (nullChar != null) {
+ params.append("NULL E'").append(stripEQuote(nullChar)).append("'
");
+ }
+ return params.toString().trim();
+ }
+
+ private static String stripEQuote(String value) {
+ if (value.startsWith("E'") && value.endsWith("'")) {
+ return value.substring(2, value.length() - 1);
+ }
+ if (value.startsWith("'") && value.endsWith("'")) {
+ return value.substring(1, value.length() - 1);
+ }
+ return value;
+ }
+
+}
\ No newline at end of file
diff --git
a/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/PXFApplication.java
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/PXFApplication.java
new file mode 100644
index 00000000..ea67fb28
--- /dev/null
+++
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/PXFApplication.java
@@ -0,0 +1,105 @@
+package org.apache.cloudberry.pxf.automation.applications;
+
+/*
+ * 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.
+ */
+
+import
org.apache.cloudberry.pxf.automation.testcontainers.PXFCloudberryContainer;
+import org.testcontainers.containers.Container.ExecResult;
+
+import java.io.IOException;
+
+/**
+ * Manages PXF server configuration inside the container.
+ * Writes config files (jdbc-site.xml, s3-site.xml, etc.) and restarts the PXF
process.
+ */
+public class PXFApplication {
+
+ private static final String SCRIPTS_PREFIX =
+
"/home/gpadmin/workspace/cloudberry-pxf/automation/src/main/resources/testcontainers/pxf-cbdb/script";
+
+ private final PXFCloudberryContainer container;
+
+ public PXFApplication(PXFCloudberryContainer container) {
+ this.container = container;
+ }
+
+ public void configureJdbcServers() throws IOException,
InterruptedException {
+ System.out.println("[PXFApplication] Configuring JDBC servers
(database, db-session-params, db-hive)...");
+
+ String script = String.join("\n",
+ "set -e",
+ "source " + SCRIPTS_PREFIX + "/pxf-env.sh",
+ "PXF_BASE_SERVERS=${PXF_BASE}/servers",
+ "TEMPLATES_DIR=${PXF_HOME}/templates",
+
+ "mkdir -p ${PXF_BASE_SERVERS}/database",
+ "cp ${TEMPLATES_DIR}/jdbc-site.xml
${PXF_BASE_SERVERS}/database/",
+ "sed -i
's|YOUR_DATABASE_JDBC_DRIVER_CLASS_NAME|org.postgresql.Driver|'
${PXF_BASE_SERVERS}/database/jdbc-site.xml",
+ "sed -i
's|YOUR_DATABASE_JDBC_URL|jdbc:postgresql://localhost:7000/pxfautomation|'
${PXF_BASE_SERVERS}/database/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_USER||'
${PXF_BASE_SERVERS}/database/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_PASSWORD||'
${PXF_BASE_SERVERS}/database/jdbc-site.xml",
+ "cp ${PXF_BASE_SERVERS}/database/jdbc-site.xml
${PXF_BASE_SERVERS}/database/testuser-user.xml",
+ "sed -i 's|pxfautomation|template1|'
${PXF_BASE_SERVERS}/database/testuser-user.xml",
+ "cp
/home/gpadmin/workspace/cloudberry-pxf/automation/src/test/resources/report.sql
${PXF_BASE_SERVERS}/database/",
+
+ "mkdir -p ${PXF_BASE_SERVERS}/db-session-params",
+ "cp ${TEMPLATES_DIR}/jdbc-site.xml
${PXF_BASE_SERVERS}/db-session-params/",
+ "sed -i
's|YOUR_DATABASE_JDBC_DRIVER_CLASS_NAME|org.postgresql.Driver|'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+ "sed -i
's|YOUR_DATABASE_JDBC_URL|jdbc:postgresql://localhost:7000/pxfautomation|'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_USER||'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_PASSWORD||'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+ "sed -i
's|</configuration>|<property><name>jdbc.session.property.client_min_messages</name><value>debug1</value></property></configuration>|'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+ "sed -i
's|</configuration>|<property><name>jdbc.session.property.default_statistics_target</name><value>123</value></property></configuration>|'
${PXF_BASE_SERVERS}/db-session-params/jdbc-site.xml",
+
+ "mkdir -p ${PXF_BASE_SERVERS}/db-hive",
+ "cp ${TEMPLATES_DIR}/jdbc-site.xml
${PXF_BASE_SERVERS}/db-hive/",
+ "sed -i
's|YOUR_DATABASE_JDBC_DRIVER_CLASS_NAME|org.apache.hive.jdbc.HiveDriver|'
${PXF_BASE_SERVERS}/db-hive/jdbc-site.xml",
+ "sed -i
's|YOUR_DATABASE_JDBC_URL|jdbc:hive2://localhost:10000/default|'
${PXF_BASE_SERVERS}/db-hive/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_USER||'
${PXF_BASE_SERVERS}/db-hive/jdbc-site.xml",
+ "sed -i 's|YOUR_DATABASE_JDBC_PASSWORD||'
${PXF_BASE_SERVERS}/db-hive/jdbc-site.xml",
+ "cp
/home/gpadmin/workspace/cloudberry-pxf/automation/src/test/resources/hive-report.sql
${PXF_BASE_SERVERS}/db-hive/"
+ );
+
+ ExecResult result = container.execInContainer("bash", "-l", "-c",
script);
+ if (result.getExitCode() != 0) {
+ throw new RuntimeException(
+ "JDBC server configuration failed (exit " +
result.getExitCode() + "):\n"
+ + result.getStdout() + "\n" + result.getStderr());
+ }
+
+ restartPxf();
+
+ System.out.println("[PXFApplication] JDBC servers configured and PXF
restarted");
+ }
+
+ public void restartPxf() throws IOException, InterruptedException {
+ String script = String.join("\n",
+ "set -e",
+ "source " + SCRIPTS_PREFIX + "/pxf-env.sh",
+ "$PXF_HOME/bin/pxf restart"
+ );
+ ExecResult result = container.execInContainer("bash", "-l", "-c",
script);
+ if (result.getExitCode() != 0) {
+ throw new RuntimeException(
+ "PXF restart failed (exit " + result.getExitCode() + "):\n"
+ + result.getStdout() + "\n" + result.getStderr());
+ }
+ System.out.println("[PXFApplication] PXF restarted");
+ }
+}
diff --git
a/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/RegressApplication.java
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/RegressApplication.java
new file mode 100644
index 00000000..654bae0c
--- /dev/null
+++
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/applications/RegressApplication.java
@@ -0,0 +1,97 @@
+package org.apache.cloudberry.pxf.automation.applications;
+
+/*
+ * 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.
+ */
+
+import
org.apache.cloudberry.pxf.automation.testcontainers.PXFCloudberryContainer;
+import org.testcontainers.containers.Container.ExecResult;
+
+/**
+ * Runs {@code pxf_regress} SQL tests inside the TestContainers-managed
container.
+ * Replaces the SSH-based {@code Regress} system object.
+ */
+public class RegressApplication {
+
+ private static final String REGRESS_DIR =
"/home/gpadmin/workspace/cloudberry-pxf/automation/pxf_regress";
+ private static final String SQL_REPO_DIR =
"/home/gpadmin/workspace/cloudberry-pxf/automation/sqlrepo";
+ private static final String DB_NAME = "pxfautomation";
+ /** Written by {@code pxf_regress} under each test directory when
comparisons fail. */
+ private static final String REGRESSION_DIFFS_FILE = "regression.diffs";
+
+ private final PXFCloudberryContainer container;
+
+ public RegressApplication(PXFCloudberryContainer container) {
+ this.container = container;
+ }
+
+ /**
+ * Runs a SQL test using {@code pxf_regress} inside the container.
+ *
+ * @param sqlTestPath relative path under {@code sqlrepo/}, e.g. {@code
"features/jdbc/single_fragment"}
+ * @throws Exception if the test fails or the command errors out
+ */
+ public void runSqlTest(String sqlTestPath) throws Exception {
+ System.out.println("[RegressApplication] Running SQL test: " +
sqlTestPath);
+
+ String command = String.join(" ",
+ "cd " + SQL_REPO_DIR + " &&",
+ "GPHOME=${GPHOME:-/usr/local/cloudberry-db}",
+ "PATH=\"${GPHOME}/bin:$PATH\"",
+ "PGHOST=localhost",
+ "PGPORT=7000",
+ "PGDATABASE=" + DB_NAME,
+ REGRESS_DIR + "/pxf_regress",
+ sqlTestPath);
+
+ ExecResult result = container.execInContainer("bash", "-l", "-c",
command);
+ String output = result.getStdout();
+ if (!output.isEmpty()) {
+ System.out.println(output);
+ }
+ String errOutput = result.getStderr();
+ if (errOutput != null && !errOutput.isEmpty()) {
+ System.err.println(errOutput);
+ }
+
+ if (result.getExitCode() != 0) {
+ printPxfRegressDiffsToStdout(sqlTestPath);
+ throw new RuntimeException(
+ "pxf_regress FAILED for '" + sqlTestPath + "' (exit " +
result.getExitCode() + "):\n" + output);
+ }
+ System.out.println("[RegressApplication] Test passed: " + sqlTestPath);
+ }
+
+ /**
+ * Prints the aggregated diff file produced by {@code pxf_regress} (if
present) to stdout.
+ */
+ private void printPxfRegressDiffsToStdout(String sqlTestPath) throws
Exception {
+ String diffsPath = SQL_REPO_DIR + "/" + sqlTestPath + "/" +
REGRESSION_DIFFS_FILE;
+ ExecResult cat = container.execInContainer("cat", diffsPath);
+ String diffText = cat.getStdout();
+ if (cat.getExitCode() == 0 && diffText != null && !diffText.isEmpty())
{
+ System.out.println();
+ System.out.println("===== pxf_regress " + REGRESSION_DIFFS_FILE +
" =====");
+ System.out.println(diffText);
+ return;
+ }
+ System.out.println();
+ System.out.println("[RegressApplication] No readable " +
REGRESSION_DIFFS_FILE + " at " + diffsPath
+ + " (cat exit " + cat.getExitCode() + ")");
+ }
+}
\ No newline at end of file
diff --git
a/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/ClasspathDockerContainerBuilder.java
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/ClasspathDockerContainerBuilder.java
new file mode 100644
index 00000000..fcfd4548
--- /dev/null
+++
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/ClasspathDockerContainerBuilder.java
@@ -0,0 +1,131 @@
+package org.apache.cloudberry.pxf.automation.testcontainers;
+
+/*
+ * 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.
+ */
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ *
+ */
+public class ClasspathDockerContainerBuilder {
+
+ private ClasspathDockerContainerBuilder() {
+ }
+
+ /**
+ * Builds named Docker image from resources in the classpath.
+ *
+ * @param imageName - name of the image to build
+ * @param resourceDirectory - resource path to Dockerfile's folder
+ * @param resources - list of files to copy (relative to resourceDirectory)
+ */
+ public static void ensureImageExists(String imageName, String
resourceDirectory, String[] resources) {
+ ensureImageExists(imageName, resourceDirectory, resources, new
String[0]);
+ }
+
+ /**
+ * Builds named Docker image from resources in the classpath with optional
build arguments.
+ *
+ * @param imageName - name of the image to build
+ * @param resourceDirectory - resource path to Dockerfile's folder
+ * @param resources - list of files to copy (relative to resourceDirectory)
+ * @param buildArgs - docker --build-arg values in "KEY=VALUE" format
+ */
+ public static void ensureImageExists(String imageName, String
resourceDirectory,
+ String[] resources, String[]
buildArgs) {
+ if (imageExists(imageName)) {
+ System.out.println("=== Image '" + imageName + "' already exists
locally, skip build ===");
+ return;
+ }
+ try {
+ Path contextDir = Files.createTempDirectory("tc-docker-context-");
+ for (String resource : resources) {
+ Path target = contextDir.resolve(resource);
+ Files.createDirectories(target.getParent());
+ try (InputStream is = ClasspathDockerContainerBuilder.class
+ .getClassLoader()
+ .getResourceAsStream(resourceDirectory + "/" +
resource)) {
+ if (is == null) {
+ throw new IllegalStateException("Classpath resource
not found: " + resource);
+ }
+ Files.copy(is, target,
StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+ dockerBuild(contextDir.toFile(), imageName, buildArgs);
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to prepare Docker build context
from classpath", e);
+ }
+ }
+
+ private static boolean imageExists(String imageName) {
+ try {
+ Process process = new ProcessBuilder("docker", "image", "inspect",
imageName)
+ .redirectErrorStream(true)
+ .start();
+ int exitCode = process.waitFor();
+ return exitCode == 0;
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Failed to check Docker image
existence: " + imageName, e);
+ }
+ }
+
+ private static void dockerBuild(File contextDir, String tag, String[]
buildArgs) {
+ System.out.println("=== docker build -t " + tag + " " + contextDir + "
===");
+ try {
+ List<String> cmd = new ArrayList<>(Arrays.asList("docker",
"build", "-t", tag));
+ for (String arg : buildArgs) {
+ cmd.add("--build-arg");
+ cmd.add(arg);
+ }
+ cmd.add(".");
+ ProcessBuilder pb = new ProcessBuilder(cmd)
+ .directory(contextDir)
+ .redirectErrorStream(true);
+ Process process = pb.start();
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(process.getInputStream(),
StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ System.out.println(line);
+ }
+ }
+ int exitCode = process.waitFor();
+ if (exitCode != 0) {
+ throw new RuntimeException(
+ "docker build failed for '" + tag + "' (exit " +
exitCode + "). "
+ + "Context dir: " +
contextDir.getAbsolutePath());
+ }
+ System.out.println("=== Image '" + tag + "' built successfully
===");
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Failed to build Docker image '" + tag
+ "'", e);
+ }
+ }
+}
diff --git
a/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/PXFCloudberryContainer.java
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/PXFCloudberryContainer.java
new file mode 100644
index 00000000..b5bbb4cc
--- /dev/null
+++
b/automation/src/main/java/org/apache/cloudberry/pxf/automation/testcontainers/PXFCloudberryContainer.java
@@ -0,0 +1,245 @@
+package org.apache.cloudberry.pxf.automation.testcontainers;
+
+/*
+ * 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.
+ */
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.async.ResultCallback;
+import com.github.dockerjava.api.command.ExecCreateCmdResponse;
+import com.github.dockerjava.api.model.Frame;
+import org.testcontainers.DockerClientFactory;
+import org.testcontainers.containers.BindMode;
+import org.testcontainers.containers.GenericContainer;
+import org.testcontainers.containers.Network;
+import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+import org.testcontainers.utility.MountableFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * PXF + Cloudberry colocated testcontainer.
+ *
+ * Cloudberry is built during image creation.
+ * Demo cluster and PXF are initialised at runtime via {@code entrypoint.sh}.
+ *
+ * Use {@link #getInstance()} to get a singleton that is started once per
+ * automation JVM. The container shares a Docker {@link Network} with other
+ * test containers so they can communicate by hostname.
+ */
+public class PXFCloudberryContainer extends
GenericContainer<PXFCloudberryContainer> {
+
+ private static final Map<String, String> BASE_IMAGES = new HashMap<>();
+ static {
+ BASE_IMAGES.put("ubuntu",
"apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest");
+ BASE_IMAGES.put("rocky9",
"apache/incubator-cloudberry:cbdb-build-rocky9-latest");
+ }
+
+ public static final int CLOUDBERRY_PORT = 7000;
+ public static final int PXF_PORT = 5888;
+ public static final String CLOUDBERRY_USER = "gpadmin";
+
+ private static final String CONTAINER_GRADLE_RO_CACHE =
"/home/gpadmin/.gradle-host-cache";
+ private static final String CONTAINER_REPO_DIR =
"/home/gpadmin/workspace/cloudberry-pxf";
+ private static final String CONTAINER_SCRIPT_DIR =
+ CONTAINER_REPO_DIR +
"/automation/src/main/resources/testcontainers/pxf-cbdb/script";
+
+ /* files required by `server`/`fdw`/`external-table` Makefiles. */
+ private static final String[] HOST_ROOT_FILES = {"version", "api_version",
"common.mk"};
+
+ private static final Network network = Network.newNetwork();
+ private static PXFCloudberryContainer instance;
+
+ private PXFCloudberryContainer(String imageName, String repoPath) {
+ super(DockerImageName.parse(imageName));
+ Path root = Paths.get(repoPath).toAbsolutePath().normalize();
+
+ withNetwork(network)
+ .withNetworkAliases("mdw")
+ .withExposedPorts(CLOUDBERRY_PORT, PXF_PORT)
+ .withCommand("tail", "-f", "/dev/null")
+ .withCreateContainerCmdModifier(cmd -> cmd.withHostName("mdw"))
+ .waitingFor(new AbstractWaitStrategy() {
+ @Override
+ protected void waitUntilReady() {
+ // No-op: we shouldn't wait for processes to run here
+ // will start applications with entrypoint.sh
+ }
+ })
+ .withStartupTimeout(Duration.ofMinutes(25))
+ // Copy directories to the container at runtime:
+ .withCopyToContainer(
+
MountableFile.forHostPath(root.resolve("external-table").toString()),
+ CONTAINER_REPO_DIR + "/external-table")
+ .withCopyToContainer(
+ MountableFile.forHostPath(root.resolve("fdw").toString()),
+ CONTAINER_REPO_DIR + "/fdw")
+ .withCopyToContainer(
+
MountableFile.forHostPath(root.resolve("server").toString()),
+ CONTAINER_REPO_DIR + "/server")
+ .withCopyToContainer(
+
MountableFile.forHostPath(root.resolve("automation").toString()),
+ CONTAINER_REPO_DIR + "/automation");
+ // Copy required files to the container at runtime:
+ for (String name : HOST_ROOT_FILES) {
+ withCopyToContainer(
+ MountableFile.forHostPath(root.resolve(name).toString()),
+ CONTAINER_REPO_DIR + "/" + name);
+ }
+
+ // mount /home/username/.gradle/caches to the container to speed up
build
+ Path hostGradleCache = Paths.get(System.getProperty("user.home"),
".gradle", "caches");
+ boolean hasHostGradleCache = Files.exists(hostGradleCache,
LinkOption.NOFOLLOW_LINKS);
+ if (hasHostGradleCache) {
+ withFileSystemBind(hostGradleCache.toString(),
CONTAINER_GRADLE_RO_CACHE, BindMode.READ_ONLY);
+ withEnv("GRADLE_RO_DEP_CACHE", CONTAINER_GRADLE_RO_CACHE);
+ }
+ }
+
+ private static String resolveDistro() {
+ String prop = System.getProperty("pxf.test.distro");
+ if (prop != null && !prop.isEmpty()) return prop;
+ String env = System.getenv("PXF_TEST_DISTRO");
+ if (env != null && !env.isEmpty()) return env;
+ return "ubuntu";
+ }
+
+ /**
+ * Returns a singleton container, starting it and running the environment
+ * setup on first access. Thread-safe.
+ */
+ public static synchronized PXFCloudberryContainer getInstance() {
+ if (instance == null) {
+ String repo = resolveProperty("pxf.test.repo.path",
findRepoPath());
+ String distro = resolveDistro();
+ String imageName = "pxf/cbdb-testcontainer-" + distro + ":1";
+ String baseImage = BASE_IMAGES.getOrDefault(distro,
BASE_IMAGES.get("ubuntu"));
+
+ ClasspathDockerContainerBuilder.ensureImageExists(
+ imageName,
+ "testcontainers/pxf-cbdb/",
+ new String[]{
+ "Dockerfile",
+ "script/build_cloudberry.sh"
+ },
+ new String[]{"BASE_IMAGE=" + baseImage});
+
+ instance = new PXFCloudberryContainer(imageName, repo);
+ instance.start();
+ Runtime.getRuntime().addShutdownHook(new Thread(instance::stop));
+
+ try {
+ instance.runEntrypoint();
+ instance.waitingFor(Wait.forListeningPorts(CLOUDBERRY_PORT,
PXF_PORT));
+ } catch (Exception e) {
+ instance.stop();
+ instance = null;
+ throw new RuntimeException("Failed to initialize PXF
container", e);
+ }
+ }
+ return instance;
+ }
+
+
+ private void runEntrypoint() throws IOException, InterruptedException {
+ logger().info("Running entrypoint.sh inside container (this takes
several minutes)...");
+ int exitCode = execInContainerWithLiveOutput(
+ "bash", "-l", "-c", CONTAINER_SCRIPT_DIR + "/entrypoint.sh
2>&1");
+ if (exitCode != 0) {
+ throw new RuntimeException("entrypoint.sh failed (exit " +
exitCode + ")");
+ }
+ logger().info("entrypoint.sh completed successfully");
+ }
+
+ private int execInContainerWithLiveOutput(String... command) throws
InterruptedException {
+ DockerClient client = DockerClientFactory.instance().client();
+ ExecCreateCmdResponse exec = client.execCreateCmd(getContainerId())
+ .withCmd(command)
+ .withAttachStdout(true)
+ .withAttachStderr(true)
+ .exec();
+
+ client.execStartCmd(exec.getId())
+ .exec(new ResultCallback.Adapter<Frame>() {
+ @Override
+ public void onNext(Frame frame) {
+ System.out.print(new String(frame.getPayload(),
StandardCharsets.UTF_8));
+ }
+ })
+ .awaitCompletion();
+
+ Long exitCode =
client.inspectExecCmd(exec.getId()).exec().getExitCodeLong();
+ return exitCode != null ? exitCode.intValue() : -1;
+ }
+
+ private static String resolveProperty(String key, String fallback) {
+ String value = System.getProperty(key);
+ return (value != null && !value.isEmpty()) ? value : fallback;
+ }
+
+ private static String findRepoPath() {
+ File dir = new File(System.getProperty("user.dir"));
+ for (int i = 0; i < 5; i++) {
+ if (new File(dir, "automation/pom.xml").exists()) {
+ return dir.getAbsolutePath();
+ }
+ dir = dir.getParentFile();
+ if (dir == null)
+ break;
+ }
+ throw new IllegalStateException(
+ "Cannot auto-detect cloudberry-pxf repo root. Set
-Dpxf.test.repo.path=...");
+ }
+
+
+ public Network getSharedNetwork() {
+ return network;
+ }
+
+ public int getCloudberryMappedPort() {
+ return getMappedPort(CLOUDBERRY_PORT);
+ }
+
+ public int getCloudberryInternalPort() {
+ return CLOUDBERRY_PORT;
+ }
+
+ public String getCloudberryUser() {
+ return CLOUDBERRY_USER;
+ }
+
+ public String getPxfInternalHost() {
+ return "localhost";
+ }
+
+ public int getPxfInternalPort() {
+ return PXF_PORT;
+ }
+
+}
diff --git a/automation/src/main/resources/testcontainers/pxf-cbdb/Dockerfile
b/automation/src/main/resources/testcontainers/pxf-cbdb/Dockerfile
new file mode 100644
index 00000000..64ebf620
--- /dev/null
+++ b/automation/src/main/resources/testcontainers/pxf-cbdb/Dockerfile
@@ -0,0 +1,79 @@
+# --------------------------------------------------------------------
+#
+# 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.
+#
+# --------------------------------------------------------------------
+ARG BASE_IMAGE=apache/incubator-cloudberry:cbdb-build-ubuntu22.04-latest
+FROM ${BASE_IMAGE}
+
+# Install Java 8 & 11: auto-detect OS package manager
+RUN if command -v apt-get >/dev/null 2>&1; then \
+ export DEBIAN_FRONTEND=noninteractive && \
+ sudo apt-get update && \
+ sudo apt-get install -y --no-install-recommends \
+ curl ca-certificates git unzip maven make \
+ locales wget lsb-release openssh-server iproute2 sudo \
+ openjdk-11-jdk-headless; \
+ elif command -v dnf >/dev/null 2>&1; then \
+ sudo dnf install -y --allowerasing \
+ curl ca-certificates wget maven unzip openssh-server iproute sudo
glibc-langpack-en glibc-locale-source \
+ java-11-openjdk-devel && \
+ sudo dnf clean all; \
+ fi
+
+# Install Gradle and warm wrapper cache (it will download GRADLE_VERSION again)
+# (version must match server/gradle/wrapper/gradle-wrapper.properties)
+ARG GRADLE_VERSION=7.6.6
+RUN curl -fsSL
"https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \
+ -o /tmp/gradle.zip && \
+ sudo unzip -q /tmp/gradle.zip -d /opt && \
+ sudo ln -s "/opt/gradle-${GRADLE_VERSION}/bin/gradle"
/usr/local/bin/gradle && \
+ rm /tmp/gradle.zip
+RUN cd /tmp && gradle init && gradle wrapper --gradle-version
${GRADLE_VERSION} && ./gradlew javaToolchains
+RUN rm -rf /tmp/gradle /tmp/gradlew /tmp/gradlew.bat /tmp/.gradle
+ENV GRADLE_HOME="/opt/gradle-${GRADLE_VERSION}"
+
+# Go toolchain for building pxf_regress inside the container
+ARG GO_VERSION=1.21.13
+RUN ARCH=$(uname -m) && \
+ case "$ARCH" in \
+ x86_64) GARCH=amd64 ;; \
+ aarch64|arm64) GARCH=arm64 ;; \
+ *) echo "unsupported arch: $ARCH" >&2; exit 1 ;; \
+ esac && \
+ curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${GARCH}.tar.gz" -o
/tmp/go.tgz && \
+ sudo rm -rf /usr/local/go && \
+ sudo tar -C /usr/local -xzf /tmp/go.tgz && \
+ rm /tmp/go.tgz
+ENV PATH="/usr/local/go/bin:${PATH}"
+
+# Env vars that scripts expect
+ENV GPHD_ROOT=/home/gpadmin/workspace/singlecluster
+ENV GPHOME=/usr/local/cloudberry-db
+
+RUN sudo mkdir -p /home/gpadmin/workspace && \
+ sudo chown -R gpadmin:gpadmin /home/gpadmin/workspace
+
+# Clone Cloudberry source (parametrized via build args)
+ARG CLOUDBERRY_REPO=https://github.com/apache/cloudberry.git
+ARG CLOUDBERRY_BRANCH=main
+RUN git clone --depth 1 -b ${CLOUDBERRY_BRANCH} \
+ ${CLOUDBERRY_REPO} /home/gpadmin/workspace/cloudberry
+
+# Copy and run the build script (demo cluster is created at runtime)
+COPY script/build_cloudberry.sh /tmp/build_cloudberry.sh
+RUN bash /tmp/build_cloudberry.sh
\ No newline at end of file
diff --git
a/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_cloudberry.sh
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_cloudberry.sh
new file mode 100755
index 00000000..7ad3204a
--- /dev/null
+++
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_cloudberry.sh
@@ -0,0 +1,211 @@
+# --------------------------------------------------------------------
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed
+# with this work for additional information regarding copyright
+# ownership. The ASF licenses this file to You under the Apache
+# License, Version 2.0 (the "License"); you may not use this file
+# except in compliance with the License. You may obtain a copy of the
+# License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied. See the License for the specific language governing
+# permissions and limitations under the License.
+#
+# --------------------------------------------------------------------
+# Build Cloudberry from source — works on both Ubuntu and Rocky/RHEL
+
+# Install sudo & git
+if command -v apt-get >/dev/null 2>&1; then
+ sudo apt update && sudo apt install -y sudo git
+elif command -v dnf >/dev/null 2>&1; then
+ sudo dnf install -y sudo git
+fi
+
+# Required configuration
+## Add Cloudberry environment setup to .bashrc
+echo -e '\n# Add Cloudberry entries
+if [ -f /usr/local/cloudberry-db/cloudberry-env.sh ]; then
+ source /usr/local/cloudberry-db/cloudberry-env.sh
+fi
+## US English with UTF-8 character encoding
+export LANG=en_US.UTF-8
+' >> /home/gpadmin/.bashrc
+## Set up SSH for passwordless access
+mkdir -p /home/gpadmin/.ssh
+if [ ! -f /home/gpadmin/.ssh/id_rsa ]; then
+ ssh-keygen -t rsa -b 2048 -C 'apache-cloudberry-dev' -f
/home/gpadmin/.ssh/id_rsa -N ""
+fi
+cat /home/gpadmin/.ssh/id_rsa.pub >> /home/gpadmin/.ssh/authorized_keys
+## Set proper SSH directory permissions
+chmod 700 /home/gpadmin/.ssh
+chmod 600 /home/gpadmin/.ssh/authorized_keys
+chmod 644 /home/gpadmin/.ssh/id_rsa.pub
+
+# Configure system settings
+sudo tee /etc/security/limits.d/90-db-limits.conf << 'EOF'
+## Core dump file size limits for gpadmin
+gpadmin soft core unlimited
+gpadmin hard core unlimited
+## Open file limits for gpadmin
+gpadmin soft nofile 524288
+gpadmin hard nofile 524288
+## Process limits for gpadmin
+gpadmin soft nproc 131072
+gpadmin hard nproc 131072
+EOF
+
+# Verify resource limits
+ulimit -a
+
+# Install basic system packages
+if command -v apt-get >/dev/null 2>&1; then
+ sudo apt update
+ sudo apt install -y bison \
+ bzip2 \
+ cmake \
+ curl \
+ flex \
+ gcc \
+ g++ \
+ iproute2 \
+ iputils-ping \
+ language-pack-en \
+ locales \
+ libapr1-dev \
+ libbz2-dev \
+ libcurl4-gnutls-dev \
+ libevent-dev \
+ libkrb5-dev \
+ libipc-run-perl \
+ libldap2-dev \
+ libpam0g-dev \
+ libprotobuf-dev \
+ libreadline-dev \
+ libssl-dev \
+ libuv1-dev \
+ liblz4-dev \
+ libxerces-c-dev \
+ libxml2-dev \
+ libyaml-dev \
+ libzstd-dev \
+ libperl-dev \
+ make \
+ pkg-config \
+ protobuf-compiler \
+ python3-dev \
+ python3-pip \
+ python3-setuptools \
+ rsync \
+ libsnappy-dev
+elif command -v dnf >/dev/null 2>&1; then
+ sudo dnf install -y \
+ bison \
+ bzip2 \
+ cmake \
+ curl \
+ flex \
+ gcc \
+ gcc-c++ \
+ iproute \
+ iputils \
+ glibc-langpack-en \
+ glibc-locale-source \
+ apr-devel \
+ bzip2-devel \
+ libcurl-devel \
+ libevent-devel \
+ krb5-devel \
+ perl-IPC-Run \
+ openldap-devel \
+ pam-devel \
+ protobuf-devel \
+ readline-devel \
+ openssl-devel \
+ libuv-devel \
+ lz4-devel \
+ xerces-c-devel \
+ libxml2-devel \
+ libyaml-devel \
+ libzstd-devel \
+ perl-devel \
+ make \
+ pkgconfig \
+ protobuf-compiler \
+ python3-devel \
+ python3-pip \
+ python3-setuptools \
+ rsync \
+ snappy-devel
+fi
+
+# Continue as gpadmin user
+
+
+# Prepare the build environment for Apache Cloudberry
+sudo rm -rf /usr/local/cloudberry-db
+sudo chmod a+w /usr/local
+mkdir -p /usr/local/cloudberry-db
+sudo chown -R gpadmin:gpadmin /usr/local/cloudberry-db
+
+# Set up xerces-c paths:
+# - Ubuntu: installed via libxerces-c-dev into /usr/include/xercesc
+# - Rocky9: pre-built in the base image at /usr/local/xerces-c/
+if command -v apt-get >/dev/null 2>&1; then
+ XERCES_INCLUDES=/usr/include/xercesc
+else
+ XERCES_INCLUDES=/usr/local/xerces-c/include
+ # Copy shared libs so the installed cloudberry-db can find them at runtime
+ mkdir -p /usr/local/cloudberry-db/lib
+ cp -v /usr/local/xerces-c/lib/libxerces-c.so \
+ /usr/local/xerces-c/lib/libxerces-c-3.*.so \
+ /usr/local/cloudberry-db/lib/
+ # Register the lib path so configure test programs can load the .so at
runtime
+ echo /usr/local/cloudberry-db/lib | sudo tee
/etc/ld.so.conf.d/cloudberry-xerces.conf
+ sudo ldconfig
+ export CPPFLAGS="${CPPFLAGS:-} -I/usr/local/xerces-c/include"
+ export LDFLAGS="${LDFLAGS:-} -L/usr/local/cloudberry-db/lib"
+fi
+
+# Run configure
+cd ~/workspace/cloudberry
+./configure --prefix=/usr/local/cloudberry-db \
+ --disable-external-fts \
+ --enable-debug \
+ --enable-cassert \
+ --enable-debug-extensions \
+ --enable-gpcloud \
+ --enable-ic-proxy \
+ --enable-mapreduce \
+ --enable-orafce \
+ --enable-orca \
+ --disable-pax \
+ --disable-pxf \
+ --enable-tap-tests \
+ --with-gssapi \
+ --with-ldap \
+ --with-libxml \
+ --with-lz4 \
+ --with-pam \
+ --with-perl \
+ --with-pgport=5432 \
+ --with-python \
+ --with-pythonsrc-ext \
+ --with-ssl=openssl \
+ --with-uuid=e2fs \
+ --with-includes=/usr/include/xercesc
+
+# Build and install Cloudberry and its contrib modules
+make -j$(nproc) -C ~/workspace/cloudberry
+make -j$(nproc) -C ~/workspace/cloudberry/contrib
+make install -C ~/workspace/cloudberry
+make install -C ~/workspace/cloudberry/contrib
+
+# Verify the installation
+/usr/local/cloudberry-db/bin/postgres --gp-version
+/usr/local/cloudberry-db/bin/postgres --version
+ldd /usr/local/cloudberry-db/bin/postgres
diff --git
a/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_pxf.sh
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_pxf.sh
new file mode 100755
index 00000000..923775dd
--- /dev/null
+++ b/automation/src/main/resources/testcontainers/pxf-cbdb/script/build_pxf.sh
@@ -0,0 +1,71 @@
+#!/bin/bash
+# --------------------------------------------------------------------
+#
+# 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.
+#
+# --------------------------------------------------------------------
+# Build and install PXF — works on both Ubuntu and Rocky/RHEL
+
+# Auto-detect Java 11 path
+if [ -d /usr/lib/jvm/java-11-openjdk-amd64 ]; then
+ JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-11-openjdk-amd64}
+elif [ -d /usr/lib/jvm/java-11-openjdk-arm64 ]; then
+ JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-11-openjdk-arm64}
+else
+ JAVA_HOME=${JAVA_HOME:-/usr/lib/jvm/java-11-openjdk}
+fi
+export PATH=$JAVA_HOME/bin:$PATH
+export GPHOME=/usr/local/cloudberry-db
+source /usr/local/cloudberry-db/cloudberry-env.sh
+export PATH=$GPHOME/bin:$PATH
+
+# Install Java 11 JDK and Maven
+if command -v apt-get >/dev/null 2>&1; then
+ sudo apt update
+ sudo apt install -y openjdk-11-jdk-headless maven
+elif command -v dnf >/dev/null 2>&1; then
+ sudo dnf install -y java-11-openjdk-devel maven
+fi
+
+cd /home/gpadmin/workspace/cloudberry-pxf
+
+# Ensure gpadmin owns the source directory
+sudo chown -R gpadmin:gpadmin /home/gpadmin/workspace/cloudberry-pxf
+sudo chown -R gpadmin:gpadmin /usr/local/cloudberry-db
+
+export PXF_HOME=/usr/local/pxf
+sudo mkdir -p "$PXF_HOME"
+sudo chmod -R a+rwX "$PXF_HOME"
+
+# Build and Install PXF
+cd /home/gpadmin/workspace/cloudberry-pxf
+make -C external-table install
+make -C fdw install
+make -C server install-server
+
+# Set up PXF environment
+export PXF_BASE=$HOME/pxf-base
+export PATH=$PXF_HOME/bin:$PATH
+rm -rf "$PXF_BASE"
+mkdir -p "$PXF_BASE"
+
+# Initialize PXF
+pxf prepare
+pxf start
+
+# Verify PXF is running
+pxf status
diff --git
a/automation/src/main/resources/testcontainers/pxf-cbdb/script/entrypoint.sh
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/entrypoint.sh
new file mode 100755
index 00000000..388880c6
--- /dev/null
+++ b/automation/src/main/resources/testcontainers/pxf-cbdb/script/entrypoint.sh
@@ -0,0 +1,299 @@
+#!/bin/bash
+# --------------------------------------------------------------------
+#
+# 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.
+#
+# --------------------------------------------------------------------
+set -euo pipefail
+set -x
+
+log() { echo "[entrypoint][$(date '+%F %T')] $*"; }
+die() { log "ERROR $*"; exit 1; }
+
+ROOT_DIR=/home/gpadmin/workspace
+REPO_DIR=${ROOT_DIR}/cloudberry-pxf
+PXF_SCRIPTS=${REPO_DIR}/automation/src/main/resources/testcontainers/pxf-cbdb/script
+source "${PXF_SCRIPTS}/utils.sh"
+
+# --------------------------------------------------------------------
+# OS detection: "deb" (Ubuntu/Debian) or "rpm" (Rocky/RHEL/CentOS)
+# --------------------------------------------------------------------
+if command -v apt-get >/dev/null 2>&1; then
+ OS_FAMILY="deb"
+else
+ OS_FAMILY="rpm"
+fi
+
+detect_java_paths() {
+ if [ "$OS_FAMILY" = "deb" ]; then
+ case "$(uname -m)" in
+ aarch64|arm64) JAVA_BUILD=/usr/lib/jvm/java-11-openjdk-arm64; ;;
+ *) JAVA_BUILD=/usr/lib/jvm/java-11-openjdk-amd64; ;;
+ esac
+ else
+ JAVA_BUILD=/usr/lib/jvm/java-11-openjdk
+ fi
+ export JAVA_BUILD
+}
+
+setup_locale_and_packages() {
+ log "install locales"
+ log "install base packages and locales"
+ if [ "$OS_FAMILY" = "deb" ]; then
+ sudo locale-gen en_US.UTF-8 ru_RU.CP1251 ru_RU.UTF-8
+ sudo update-locale LANG=en_US.UTF-8
+ else
+ sudo localedef -c -i en_US -f UTF-8 en_US.UTF-8 || true
+ sudo localedef -c -i ru_RU -f UTF-8 ru_RU.UTF-8 || true
+ fi
+ sudo localedef -c -i ru_RU -f CP1251 ru_RU.CP1251 || true
+ export LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8
+}
+
+setup_ssh() {
+ log "configure ssh"
+ # Rocky 9 / RHEL 9 enforces system-wide crypto-policies that override
sshd_config
+ # settings for algorithm negotiation. The automation test framework uses the
+ # Ganymed SSH-2 (ch.ethz.ssh2) library which only supports older KEX
algorithms
+ # (diffie-hellman-group-exchange-sha1, diffie-hellman-group14-sha1, etc.).
+ # Downgrade to the LEGACY crypto policy so sshd accepts these algorithms.
+ if [ "$OS_FAMILY" = "rpm" ] && command -v update-crypto-policies >/dev/null
2>&1; then
+ log "setting LEGACY crypto policy for SSH compatibility"
+ sudo update-crypto-policies --set LEGACY 2>/dev/null || true
+ fi
+ sudo ssh-keygen -A
+ sudo bash -c 'echo "PasswordAuthentication yes" >> /etc/ssh/sshd_config'
+ sudo mkdir -p /etc/ssh/sshd_config.d
+ sudo bash -c 'cat >/etc/ssh/sshd_config.d/pxf-automation.conf <<EOF
+KexAlgorithms
+diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha1,diffie-hellman-group1-sha1
+HostKeyAlgorithms +ssh-rsa,ssh-dss
+PubkeyAcceptedAlgorithms +ssh-rsa,ssh-dss
+EOF'
+ if [ "$OS_FAMILY" = "deb" ]; then
+ sudo usermod -a -G sudo gpadmin
+ else
+ sudo usermod -a -G wheel gpadmin 2>/dev/null || true
+ fi
+ echo "gpadmin:cbdb@123" | sudo chpasswd
+ echo "gpadmin ALL=(ALL) NOPASSWD: ALL" | sudo tee -a
/etc/sudoers >/dev/null
+ echo "root ALL=(ALL) NOPASSWD: ALL" | sudo tee -a
/etc/sudoers >/dev/null
+
+ mkdir -p /home/gpadmin/.ssh
+ sudo chown -R gpadmin:gpadmin /home/gpadmin/.ssh
+ if [ ! -f /home/gpadmin/.ssh/id_rsa ]; then
+ sudo -u gpadmin ssh-keygen -q -t rsa -b 4096 -m PEM -C gpadmin -f
/home/gpadmin/.ssh/id_rsa -N ""
+ fi
+ sudo -u gpadmin bash -lc 'cat /home/gpadmin/.ssh/id_rsa.pub >>
/home/gpadmin/.ssh/authorized_keys'
+ sudo -u gpadmin chmod 0600 /home/gpadmin/.ssh/authorized_keys
+ ssh-keyscan -t rsa mdw cdw localhost 2>/dev/null >
/home/gpadmin/.ssh/known_hosts || true
+ sudo rm -rf /run/nologin
+ sudo mkdir -p /var/run/sshd && sudo chmod 0755 /var/run/sshd
+ # Ensure privilege separation user exists (required by Rocky 9 sshd)
+ id sshd &>/dev/null || sudo useradd -r -d /var/empty/sshd -s /sbin/nologin
sshd 2>/dev/null || true
+ sudo mkdir -p /var/empty/sshd && sudo chmod 0755 /var/empty/sshd
+ sudo /usr/sbin/sshd -E /tmp/sshd.log || die "Failed to start sshd, check
/tmp/sshd.log"
+ sleep 1
+ if ! ss -tlnp | grep -q ':22 '; then
+ log "ERROR: sshd is not listening on port 22"
+ cat /tmp/sshd.log 2>/dev/null || true
+ sudo /usr/sbin/sshd -D -e &
+ sleep 1
+ if ! ss -tlnp | grep -q ':22 '; then
+ die "sshd failed to bind to port 22"
+ fi
+ fi
+ log "sshd is running on port 22"
+}
+
+relax_pg_hba() {
+ local
pg_hba=/home/gpadmin/workspace/cloudberry/gpAux/gpdemo/datadirs/qddir/demoDataDir-1/pg_hba.conf
+ if [ -f "${pg_hba}" ] && ! grep -q "127.0.0.1/32 trust" "${pg_hba}"; then
+ cat >> "${pg_hba}" <<'EOF'
+host all all ::1/128 trust
+host all all 0.0.0.0/0 trust
+EOF
+ source /usr/local/cloudberry-db/cloudberry-env.sh >/dev/null 2>&1 || true
+ GPPORT=${GPPORT:-7000}
+
COORDINATOR_DATA_DIRECTORY=/home/gpadmin/workspace/cloudberry/gpAux/gpdemo/datadirs/qddir/demoDataDir-1
+ gpstop -u || true
+ fi
+}
+
+setup_cloudberry() {
+ log "cleanup stale gpdemo data and PG locks"
+ rm -rf /home/gpadmin/workspace/cloudberry/gpAux/gpdemo/datadirs
+ rm -f /tmp/.s.PGSQL.700*
+}
+
+create_demo_cluster() {
+ log "set up Cloudberry demo cluster"
+ source /usr/local/cloudberry-db/cloudberry-env.sh
+ make create-demo-cluster -C ~/workspace/cloudberry
+ source ~/workspace/cloudberry/gpAux/gpdemo/gpdemo-env.sh
+ psql -P pager=off template1 -c 'SELECT * from gp_segment_configuration'
+ psql template1 -c 'SELECT version()'
+}
+
+build_pxf() {
+ log "build PXF"
+ "${PXF_SCRIPTS}/build_pxf.sh"
+}
+
+build_pxf_regress() {
+ log "build pxf_regress (linux)"
+ export PATH="/usr/local/go/bin:${PATH}"
+ make -C "${REPO_DIR}/automation/pxf_regress" clean pxf_regress
+}
+
+configure_pxf() {
+ log "configure PXF"
+ source "${PXF_SCRIPTS}/pxf-env.sh"
+ export PATH="$PXF_HOME/bin:$PATH"
+ export PXF_JVM_OPTS="-Xmx512m -Xms256m"
+ export PXF_HOST=localhost
+ echo "JAVA_HOME=${JAVA_BUILD}" >> "$PXF_BASE/conf/pxf-env.sh"
+ sed -i 's/# server.address=localhost/server.address=0.0.0.0/'
"$PXF_BASE/conf/pxf-application.properties"
+ echo -e "\npxf.profile.dynamic.regex=test:.*" >>
"$PXF_BASE/conf/pxf-application.properties"
+ cp -v "$PXF_HOME"/templates/{hdfs,mapred,yarn,core,hbase,hive}-site.xml
"$PXF_BASE/servers/default"
+ for server_dir in "$PXF_BASE/servers/default"
"$PXF_BASE/servers/default-no-impersonation"; do
+ if [ ! -d "$server_dir" ]; then
+ cp -r "$PXF_BASE/servers/default" "$server_dir"
+ fi
+ if [ ! -f "$server_dir/pxf-site.xml" ]; then
+ cat > "$server_dir/pxf-site.xml" <<'XML'
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+</configuration>
+XML
+ fi
+ done
+ if ! grep -q "pxf.service.user.name"
"$PXF_BASE/servers/default-no-impersonation/pxf-site.xml"; then
+ sed -i 's#</configuration># <property>\n
<name>pxf.service.user.name</name>\n <value>foobar</value>\n </property>\n
<property>\n <name>pxf.service.user.impersonation</name>\n
<value>false</value>\n </property>\n</configuration>#'
"$PXF_BASE/servers/default-no-impersonation/pxf-site.xml"
+ fi
+
+ # Configure pxf-profiles.xml for Parquet and test profiles
+ cat > "$PXF_BASE/conf/pxf-profiles.xml" <<'EOF'
+<?xml version="1.0" encoding="UTF-8"?>
+<profiles>
+ <profile>
+ <name>pxf:parquet</name>
+ <description>Profile for reading and writing Parquet
files</description>
+ <plugins>
+
<fragmenter>org.apache.cloudberry.pxf.plugins.hdfs.HdfsDataFragmenter</fragmenter>
+
<accessor>org.apache.cloudberry.pxf.plugins.hdfs.ParquetFileAccessor</accessor>
+
<resolver>org.apache.cloudberry.pxf.plugins.hdfs.ParquetResolver</resolver>
+ </plugins>
+ </profile>
+ <profile>
+ <name>test:text</name>
+ <description>Test profile for text files</description>
+ <plugins>
+
<fragmenter>org.apache.cloudberry.pxf.plugins.hdfs.HdfsDataFragmenter</fragmenter>
+
<accessor>org.apache.cloudberry.pxf.plugins.hdfs.LineBreakAccessor</accessor>
+
<resolver>org.apache.cloudberry.pxf.plugins.hdfs.StringPassResolver</resolver>
+ </plugins>
+ </profile>
+</profiles>
+EOF
+
+ cat > "$PXF_HOME/conf/pxf-profiles.xml" <<'EOF'
+<?xml version="1.0" encoding="UTF-8"?>
+<profiles>
+ <profile>
+ <name>pxf:parquet</name>
+ <description>Profile for reading and writing Parquet
files</description>
+ <plugins>
+
<fragmenter>org.apache.cloudberry.pxf.plugins.hdfs.HdfsDataFragmenter</fragmenter>
+
<accessor>org.apache.cloudberry.pxf.plugins.hdfs.ParquetFileAccessor</accessor>
+
<resolver>org.apache.cloudberry.pxf.plugins.hdfs.ParquetResolver</resolver>
+ </plugins>
+ </profile>
+ <profile>
+ <name>test:text</name>
+ <description>Test profile for text files</description>
+ <plugins>
+
<fragmenter>org.apache.cloudberry.pxf.plugins.hdfs.HdfsDataFragmenter</fragmenter>
+
<accessor>org.apache.cloudberry.pxf.plugins.hdfs.LineBreakAccessor</accessor>
+
<resolver>org.apache.cloudberry.pxf.plugins.hdfs.StringPassResolver</resolver>
+ </plugins>
+ </profile>
+</profiles>
+EOF
+
+ # Configure S3 settings
+ mkdir -p "$PXF_BASE/servers/s3" "$PXF_HOME/servers/s3"
+
+ for s3_site in "$PXF_BASE/servers/s3/s3-site.xml"
"$PXF_BASE/servers/default/s3-site.xml" "$PXF_HOME/servers/s3/s3-site.xml"; do
+ mkdir -p "$(dirname "$s3_site")"
+ cat > "$s3_site" <<'EOF'
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+ <property>
+ <name>fs.s3a.endpoint</name>
+ <value>http://localhost:9000</value>
+ </property>
+ <property>
+ <name>fs.s3a.access.key</name>
+ <value>admin</value>
+ </property>
+ <property>
+ <name>fs.s3a.secret.key</name>
+ <value>password</value>
+ </property>
+ <property>
+ <name>fs.s3a.path.style.access</name>
+ <value>true</value>
+ </property>
+ <property>
+ <name>fs.s3a.connection.ssl.enabled</name>
+ <value>false</value>
+ </property>
+ <property>
+ <name>fs.s3a.impl</name>
+ <value>org.apache.hadoop.fs.s3a.S3AFileSystem</value>
+ </property>
+ <property>
+ <name>fs.s3a.aws.credentials.provider</name>
+ <value>org.apache.hadoop.fs.s3a.SimpleAWSCredentialsProvider</value>
+ </property>
+</configuration>
+EOF
+ done
+ mkdir -p /home/gpadmin/.aws/
+ cat > "/home/gpadmin/.aws/credentials" <<'EOF'
+[default]
+aws_access_key_id = admin
+aws_secret_access_key = password
+EOF
+
+}
+
+main() {
+ detect_java_paths
+ setup_locale_and_packages
+ setup_ssh
+ setup_cloudberry
+ create_demo_cluster
+ relax_pg_hba
+ build_pxf
+ build_pxf_regress
+ configure_pxf
+ health_check
+ log "entrypoint finished; environment ready for tests"
+}
+
+main "$@"
diff --git
a/automation/src/main/resources/testcontainers/pxf-cbdb/script/pxf-env.sh
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/pxf-env.sh
new file mode 100755
index 00000000..694ccac5
--- /dev/null
+++ b/automation/src/main/resources/testcontainers/pxf-cbdb/script/pxf-env.sh
@@ -0,0 +1,80 @@
+#!/bin/bash
+# --------------------------------------------------------------------
+#
+# 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.
+#
+# --------------------------------------------------------------------
+
+# Centralized environment for Cloudberry + PXF
+
+# --------------------------------------------------------------------
+# Architecture-aware Java selections (auto-detect OS)
+# --------------------------------------------------------------------
+if [ -d /usr/lib/jvm/java-11-openjdk-amd64 ] || [ -d
/usr/lib/jvm/java-11-openjdk-arm64 ]; then
+ # Debian/Ubuntu: paths include architecture suffix
+ case "$(uname -m)" in
+ aarch64|arm64)
+ JAVA_BUILD=${JAVA_BUILD:-/usr/lib/jvm/java-11-openjdk-arm64}
+ ;;
+ *)
+ JAVA_BUILD=${JAVA_BUILD:-/usr/lib/jvm/java-11-openjdk-amd64}
+ ;;
+ esac
+else
+ # RHEL/Rocky: architecture-independent symlinks
+ JAVA_BUILD=${JAVA_BUILD:-/usr/lib/jvm/java-11-openjdk}
+fi
+
+# --------------------------------------------------------------------
+# Core paths
+# --------------------------------------------------------------------
+export GPHOME=${GPHOME:-/usr/local/cloudberry-db}
+export PXF_HOME=${PXF_HOME:-/usr/local/pxf}
+export PXF_BASE=${PXF_BASE:-/home/gpadmin/pxf-base}
+export GPHD_ROOT=${GPHD_ROOT:-/home/gpadmin/workspace/singlecluster}
+export
PATH="$GPHD_ROOT/bin:$JAVA_BUILD/bin:/usr/local/go/bin:$GPHOME/bin:$PXF_HOME/bin:$PATH"
+export COMMON_JAVA_OPTS=${COMMON_JAVA_OPTS:-}
+
+# --------------------------------------------------------------------
+# Database defaults
+# --------------------------------------------------------------------
+export PGHOST=${PGHOST:-localhost}
+export PGPORT=${PGPORT:-7000}
+export
COORDINATOR_DATA_DIRECTORY=${COORDINATOR_DATA_DIRECTORY:-/home/gpadmin/workspace/cloudberry/gpAux/gpdemo/datadirs/qddir/demoDataDir-1}
+# set cloudberry timezone utc
+export PGTZ=UTC
+
+# --------------------------------------------------------------------
+# Minio defaults
+# --------------------------------------------------------------------
+export AWS_ACCESS_KEY_ID=admin
+export AWS_SECRET_ACCESS_KEY=password
+export PROTOCOL=minio
+export ACCESS_KEY_ID=admin
+export SECRET_ACCESS_KEY=password
+
+# --------------------------------------------------------------------
+# PXF defaults
+# --------------------------------------------------------------------
+export PXF_JVM_OPTS=${PXF_JVM_OPTS:-"-Xmx512m -Xms256m"}
+export PXF_HOST=${PXF_HOST:-localhost}
+
+# Source Cloudberry env and demo cluster if present
+[ -f "$GPHOME/cloudberry-env.sh" ] && source "$GPHOME/cloudberry-env.sh"
+[ -f "/home/gpadmin/workspace/cloudberry/gpAux/gpdemo/gpdemo-env.sh" ] &&
source /home/gpadmin/workspace/cloudberry/gpAux/gpdemo/gpdemo-env.sh
+
+echo "[pxf-env] loaded (JAVA_BUILD=${JAVA_BUILD})"
diff --git
a/automation/src/main/resources/testcontainers/pxf-cbdb/script/utils.sh
b/automation/src/main/resources/testcontainers/pxf-cbdb/script/utils.sh
new file mode 100755
index 00000000..f2bec881
--- /dev/null
+++ b/automation/src/main/resources/testcontainers/pxf-cbdb/script/utils.sh
@@ -0,0 +1,61 @@
+#!/bin/bash
+# --------------------------------------------------------------------
+#
+# 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.
+#
+# --------------------------------------------------------------------
+
+# Shared health-check helpers for entrypoint and run_tests
+set -euo pipefail
+
+# Fallback log/die in case caller didn't define them
+log() { echo "[utils][$(date '+%F %T')] $*"; }
+die() { log "ERROR $*"; exit 1; }
+
+wait_port() {
+ local host="$1" port="$2" retries="${3:-10}" sleep_sec="${4:-2}"
+ local i
+ for i in $(seq 1 "${retries}"); do
+ if (echo >/dev/tcp/"${host}"/"${port}") >/dev/null 2>&1; then
+ return 0
+ fi
+ sleep "${sleep_sec}"
+ done
+ return 1
+}
+
+check_pxf() {
+ if ! curl -sf http://localhost:5888/actuator/health >/dev/null 2>&1; then
+ die "PXF actuator health endpoint not responding"
+ fi
+}
+
+check_cloudberry() {
+ # shellcheck disable=SC1091
+ source /usr/local/cloudberry-db/cloudberry-env.sh >/dev/null 2>&1 || true
+ local port="${PGPORT:-7000}"
+ if ! psql -p "${port}" -d postgres -tAc "SELECT 1" >/dev/null 2>&1; then
+ die "Cloudberry is not responding on port ${port}"
+ fi
+}
+
+health_check() {
+ log "sanity check PXF and Cloudberry"
+ check_pxf
+ check_cloudberry
+ log "all components healthy: PXF, Cloudberry"
+}
diff --git
a/automation/src/test/java/org/apache/cloudberry/pxf/automation/AbstractTestcontainersTest.java
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/AbstractTestcontainersTest.java
new file mode 100644
index 00000000..bda4f5de
--- /dev/null
+++
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/AbstractTestcontainersTest.java
@@ -0,0 +1,139 @@
+package org.apache.cloudberry.pxf.automation;
+
+/*
+ * 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.
+ */
+
+import listeners.CustomAutomationLogger;
+import listeners.FDWSkipTestAnalyzer;
+import org.apache.cloudberry.pxf.automation.applications.CloudberryApplication;
+import org.apache.cloudberry.pxf.automation.applications.PXFApplication;
+import org.apache.cloudberry.pxf.automation.applications.RegressApplication;
+import
org.apache.cloudberry.pxf.automation.testcontainers.PXFCloudberryContainer;
+import org.apache.cloudberry.pxf.automation.utils.system.FDWUtils;
+import org.apache.cloudberry.pxf.automation.utils.system.ProtocolUtils;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Listeners;
+import reporters.CustomAutomationReport;
+
+@Listeners({CustomAutomationLogger.class, CustomAutomationReport.class,
FDWSkipTestAnalyzer.class})
+public class AbstractTestcontainersTest {
+
+ private static boolean sharedEnvironmentInitialized;
+
+ protected final String pxfHost = "localhost";
+ protected final String pxfPort = "5888";
+ protected PXFCloudberryContainer container;
+ protected CloudberryApplication cloudberry;
+ protected RegressApplication regress;
+
+ @BeforeClass(alwaysRun = true)
+ public final void doInit() throws Exception {
+ // redirect "doInit" logs to log file
+
CustomAutomationLogger.redirectStdoutStreamToFile(getClass().getSimpleName(),
"doInit");
+
+ try {
+ container = PXFCloudberryContainer.getInstance();
+
+ try (CloudberryApplication bootstrap = new
CloudberryApplication(container, "postgres")) {
+ bootstrap.connect();
+ createTestDatabases(bootstrap);
+ }
+
+ cloudberry = new CloudberryApplication(container);
+ cloudberry.connect();
+ cloudberry.createExtension("pxf", false);
+ cloudberry.createExtension("pxf_fdw", false);
+
+ if (!sharedEnvironmentInitialized) {
+ // Ensure PXF JDBC server configs exist for
SERVER=database/db-session-params tests.
+ new PXFApplication(container).configureJdbcServers();
+ if (FDWUtils.useFDW) {
+ cloudberry.createTestFDW(true);
+ cloudberry.createSystemFDW(true);
+ cloudberry.createForeignServers(true);
+ }
+ sharedEnvironmentInitialized = true;
+ }
+
+ regress = new RegressApplication(container);
+
+ // run users before class
+ beforeClass();
+ } finally {
+ CustomAutomationLogger.revertStdoutStream();
+ }
+
+ }
+
+ @AfterClass(alwaysRun = true)
+ public final void clean() throws Exception {
+ if (ProtocolUtils.getPxfTestKeepData().equals("true")) {
+ return;
+ }
+
CustomAutomationLogger.redirectStdoutStreamToFile(getClass().getSimpleName(),
"clean");
+ try {
+ if (cloudberry != null) {
+ cloudberry.close();
+ }
+ } finally {
+ CustomAutomationLogger.revertStdoutStream();
+ }
+ }
+
+ /**
+ * clean up after the class finished
+ *
+ * @throws Exception
+ */
+ protected void afterClass() throws Exception {
+ }
+
+ /**
+ * Preparations needed before the class starting
+ *
+ * @throws Exception
+ */
+ protected void beforeClass() throws Exception {
+ }
+
+ /**
+ * clean up after the test method had finished
+ *
+ * @throws Exception
+ */
+ protected void afterMethod() throws Exception {
+ }
+
+ /**
+ * Preparations needed before the test method starting
+ *
+ * @throws Exception
+ */
+ protected void beforeMethod() throws Exception {
+ }
+
+
+ private void createTestDatabases(CloudberryApplication bootstrap) throws
Exception {
+ bootstrap.createDatabase("pxfautomation");
+ bootstrap.createDatabase("pxfautomation_encoding");
+ bootstrap.runQuery("SELECT 1");
+ System.out.println("[" + getClass().getSimpleName() + "] Test
databases created");
+ }
+}
diff --git
a/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcTest.java
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcTest.java
index 4ec3be33..d52c850c 100755
---
a/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcTest.java
+++
b/automation/src/test/java/org/apache/cloudberry/pxf/automation/features/jdbc/JdbcTest.java
@@ -1,9 +1,30 @@
package org.apache.cloudberry.pxf.automation.features.jdbc;
+/*
+ * 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.
+ */
+
import java.io.File;
import annotations.FailsWithFDW;
import annotations.WorksWithFDW;
+import org.apache.cloudberry.pxf.automation.AbstractTestcontainersTest;
+import org.apache.cloudberry.pxf.automation.applications.CloudberryApplication;
import org.apache.cloudberry.pxf.automation.structures.tables.basic.Table;
import
org.apache.cloudberry.pxf.automation.structures.tables.pxf.ExternalTable;
import
org.apache.cloudberry.pxf.automation.structures.tables.utils.TableFactory;
@@ -11,13 +32,12 @@ import org.testng.annotations.Test;
import org.apache.cloudberry.pxf.automation.enums.EnumPartitionType;
-import org.apache.cloudberry.pxf.automation.features.BaseFeature;
-
@WorksWithFDW
-public class JdbcTest extends BaseFeature {
+public class JdbcTest extends AbstractTestcontainersTest {
private static final String POSTGRES_DRIVER_CLASS =
"org.postgresql.Driver";
- private static final String GPDB_PXF_AUTOMATION_DB_JDBC =
"jdbc:postgresql://";
+ private static final String localDataResourcesFolder =
"src/test/resources/data";
+
private static final String[] TYPES_TABLE_FIELDS = new String[]{
"t1 text",
"t2 text",
@@ -61,6 +81,8 @@ public class JdbcTest extends BaseFeature {
"count int",
"max int"};
+ private CloudberryApplication gpdb;
+
private ExternalTable pxfJdbcSingleFragment;
private ExternalTable pxfJdbcDateWideRangeOn;
private ExternalTable pxfJdbcDateWideRangeOff;
@@ -88,9 +110,14 @@ public class JdbcTest extends BaseFeature {
@Override
protected void beforeClass() throws Exception {
+ gpdb = cloudberry; // alias
prepareData();
}
+ private void runSqlTest(String sqlTestPath) throws Exception {
+ regress.runSqlTest(sqlTestPath);
+ }
+
protected void prepareData() throws Exception {
prepareTypesData();
prepareSingleFragment();
@@ -193,7 +220,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypes.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcSingleFragment.setHost(pxfHost);
pxfJdbcSingleFragment.setPort(pxfPort);
@@ -207,7 +234,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypes.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() +
":" + gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
13,
"USD:UAH",
"1",
@@ -226,7 +253,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypes.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() +
":" + gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
2,
"1:6",
"1",
@@ -245,7 +272,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypes.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() +
":" + gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
11,
"2015-03-06:2015-03-20",
"1:DAY",
@@ -302,7 +329,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbWritableTargetTable.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), null);
pxfJdbcWritable.setHost(pxfHost);
pxfJdbcWritable.setPort(pxfPort);
@@ -314,7 +341,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
dateTimeWritableTargetTableWithDateWideRangeOn.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), null);
pxfJdbcDateTimeWritableWithDateWideRangeOn.setHost(pxfHost);
pxfJdbcDateTimeWritableWithDateWideRangeOn.setPort(pxfPort);
@@ -326,7 +353,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
dateTimeWritableTargetTableWithDateWideRangeOff.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), null);
pxfJdbcDateTimeWritableWithDateWideRangeOff.setHost(pxfHost);
pxfJdbcDateTimeWritableWithDateWideRangeOff.setPort(pxfPort);
@@ -338,7 +365,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS_SMALL,
gpdbWritableTargetTableNoBatch.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), "BATCH_SIZE=1");
pxfJdbcWritableNoBatch.setHost(pxfHost);
pxfJdbcWritableNoBatch.setPort(pxfPort);
@@ -349,7 +376,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS_SMALL,
gpdbWritableTargetTablePool.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), "POOL_SIZE=2");
pxfJdbcWritablePool.setHost(pxfHost);
pxfJdbcWritablePool.setPort(pxfPort);
@@ -362,7 +389,7 @@ public class JdbcTest extends BaseFeature {
COLUMNS_TABLE_FIELDS,
gpdbNativeTableColumns.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcColumns.setHost(pxfHost);
pxfJdbcColumns.setPort(pxfPort);
@@ -375,7 +402,7 @@ public class JdbcTest extends BaseFeature {
COLUMNS_TABLE_FIELDS_IN_DIFFERENT_ORDER_SUBSET,
gpdbNativeTableColumns.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcColumnProjectionSubset.setHost(pxfHost);
pxfJdbcColumnProjectionSubset.setPort(pxfPort);
@@ -388,7 +415,7 @@ public class JdbcTest extends BaseFeature {
COLUMNS_TABLE_FIELDS_SUPERSET,
gpdbNativeTableColumns.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcColumnProjectionSuperset.setHost(pxfHost);
pxfJdbcColumnProjectionSuperset.setPort(pxfPort);
@@ -401,7 +428,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypes.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName(), "FETCH_SIZE=0");
pxfJdbcSingleFragment.setHost(pxfHost);
pxfJdbcSingleFragment.setPort(pxfPort);
@@ -414,7 +441,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypesWithDateWideRange.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcDateWideRangeOn.setHost(pxfHost);
pxfJdbcDateWideRangeOn.setPort(pxfPort);
@@ -426,7 +453,7 @@ public class JdbcTest extends BaseFeature {
TYPES_TABLE_FIELDS,
gpdbNativeTableTypesWithDateWideRange.getName(),
POSTGRES_DRIVER_CLASS,
- GPDB_PXF_AUTOMATION_DB_JDBC + gpdb.getMasterHost() + ":" +
gpdb.getPort() + "/pxfautomation",
+ gpdb.getCloudberryInternalJdbcUrl("pxfautomation"),
gpdb.getUserName());
pxfJdbcDateWideRangeOff.setHost(pxfHost);
pxfJdbcDateWideRangeOff.setPort(pxfPort);
@@ -461,22 +488,22 @@ public class JdbcTest extends BaseFeature {
gpdb.createTableAndVerify(pxfJdbcNamedQuery);
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void singleFragmentTable() throws Exception {
runSqlTest("features/jdbc/single_fragment");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void multipleFragmentsTables() throws Exception {
runSqlTest("features/jdbc/multiple_fragments");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void readServerConfig() throws Exception {
runSqlTest("features/jdbc/server_config");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void readViewSessionParams() throws Exception {
runSqlTest("features/jdbc/session_params");
}
@@ -485,7 +512,7 @@ public class JdbcTest extends BaseFeature {
// All the Writable Tests are failing with this Error:
// ERROR: PXF server error : class java.io.DataInputStream cannot be cast
to class
// [B (java.io.DataInputStream and [B are in module java.base of loader
'bootstrap')
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcWritableTable() throws Exception {
runSqlTest("features/jdbc/writable");
}
@@ -494,44 +521,44 @@ public class JdbcTest extends BaseFeature {
// All the Writable Tests are failing with this Error:
// ERROR: PXF server error : class java.io.DataInputStream cannot be cast
to class
// [B (java.io.DataInputStream and [B are in module java.base of loader
'bootstrap')
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcWritableTableWithDateWideRange() throws Exception {
runSqlTest("features/jdbc/writable_date_wide_range");
}
@FailsWithFDW
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcWritableTableNoBatch() throws Exception {
runSqlTest("features/jdbc/writable_nobatch");
}
@FailsWithFDW
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcWritableTablePool() throws Exception {
runSqlTest("features/jdbc/writable_pool");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcColumns() throws Exception {
runSqlTest("features/jdbc/columns");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcColumnProjection() throws Exception {
runSqlTest("features/jdbc/column_projection");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcReadableTableNoBatch() throws Exception {
runSqlTest("features/jdbc/readable_nobatch");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcReadableTableWithDateWideRange() throws Exception {
runSqlTest("features/jdbc/readable_date_wide_range");
}
- @Test(groups = {"features", "gpdb", "security", "jdbc"})
+ @Test(groups = {"testcontainers", "pxf-jdbc"})
public void jdbcNamedQuery() throws Exception {
runSqlTest("features/jdbc/named_query");
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]