snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
40
test/s3/parquet/.gitignore
vendored
Normal file
40
test/s3/parquet/.gitignore
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
# Python virtual environment
|
||||
venv/
|
||||
.venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Test artifacts
|
||||
*.log
|
||||
test_run.log
|
||||
weed-test.log
|
||||
|
||||
# SeaweedFS data directories
|
||||
filerldb2/
|
||||
idx/
|
||||
dat/
|
||||
*.idx
|
||||
*.dat
|
||||
|
||||
# Temporary test files
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
390
test/s3/parquet/Makefile
Normal file
390
test/s3/parquet/Makefile
Normal file
@@ -0,0 +1,390 @@
|
||||
# Makefile for S3 Parquet Integration Tests
|
||||
# This Makefile provides targets for running comprehensive S3 Parquet tests with PyArrow
|
||||
|
||||
# Default values
|
||||
SEAWEEDFS_BINARY ?= weed
|
||||
S3_PORT ?= 8333
|
||||
TEST_TIMEOUT ?= 15m
|
||||
ACCESS_KEY ?= some_access_key1
|
||||
SECRET_KEY ?= some_secret_key1
|
||||
BUCKET_NAME ?= test-parquet-bucket
|
||||
ENABLE_SSE_S3 ?= false
|
||||
|
||||
# Python configuration
|
||||
PYTHON ?= python3
|
||||
VENV_DIR ?= .venv
|
||||
PYTHON_TEST_SCRIPT ?= s3_parquet_test.py
|
||||
|
||||
# Test directory
|
||||
TEST_DIR := $(shell pwd)
|
||||
SEAWEEDFS_ROOT := $(shell cd ../../../ && pwd)
|
||||
|
||||
# Colors for output
|
||||
RED := \033[0;31m
|
||||
GREEN := \033[0;32m
|
||||
YELLOW := \033[1;33m
|
||||
NC := \033[0m # No Color
|
||||
|
||||
.PHONY: all build-weed check-binary check-python ci-test clean debug-logs debug-status help manual-start manual-stop setup-python start-seaweedfs start-seaweedfs-ci stop-seaweedfs stop-seaweedfs-safe test test-cross-fs test-cross-fs-with-server test-implicit-dir test-implicit-dir-with-server test-native-s3 test-native-s3-with-server test-native-s3-with-sse test-quick test-sse-s3-compat test-with-server
|
||||
|
||||
all: test
|
||||
|
||||
# Build SeaweedFS binary (GitHub Actions compatible)
|
||||
build-weed:
|
||||
@echo "Building SeaweedFS binary..."
|
||||
@cd $(SEAWEEDFS_ROOT)/weed && go install -buildvcs=false
|
||||
@echo "✅ SeaweedFS binary built successfully"
|
||||
|
||||
help:
|
||||
@echo "SeaweedFS S3 Parquet Integration Tests"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " test - Run full S3 Parquet integration tests (small and large files)"
|
||||
@echo " test-with-server - Run full tests with automatic server management (CI compatible)"
|
||||
@echo " test-quick - Run quick tests with small files only (sets TEST_QUICK=1)"
|
||||
@echo " test-implicit-dir - Test implicit directory fix for s3fs compatibility"
|
||||
@echo " test-implicit-dir-with-server - Test implicit directory fix with server management"
|
||||
@echo " test-native-s3 - Test PyArrow's native S3 filesystem (assumes server running)"
|
||||
@echo " test-native-s3-with-server - Test PyArrow's native S3 filesystem with server management"
|
||||
@echo " test-native-s3-with-sse - Test PyArrow's native S3 with SSE-S3 encryption enabled"
|
||||
@echo " test-cross-fs - Test cross-filesystem compatibility (s3fs ↔ PyArrow native)"
|
||||
@echo " test-cross-fs-with-server - Test cross-filesystem compatibility with server management"
|
||||
@echo " test-sse-s3-compat - Comprehensive SSE-S3 compatibility test (multipart uploads)"
|
||||
@echo " setup-python - Setup Python virtual environment and install dependencies"
|
||||
@echo " check-python - Check if Python and required packages are available"
|
||||
@echo " start-seaweedfs - Start SeaweedFS server for testing"
|
||||
@echo " start-seaweedfs-ci - Start SeaweedFS server (CI-safe version)"
|
||||
@echo " stop-seaweedfs - Stop SeaweedFS server"
|
||||
@echo " stop-seaweedfs-safe - Stop SeaweedFS server (CI-safe version)"
|
||||
@echo " clean - Clean up test artifacts"
|
||||
@echo " check-binary - Check if SeaweedFS binary exists"
|
||||
@echo " build-weed - Build SeaweedFS binary"
|
||||
@echo ""
|
||||
@echo "Configuration:"
|
||||
@echo " SEAWEEDFS_BINARY=$(SEAWEEDFS_BINARY)"
|
||||
@echo " S3_PORT=$(S3_PORT)"
|
||||
@echo " BUCKET_NAME=$(BUCKET_NAME)"
|
||||
@echo " ENABLE_SSE_S3=$(ENABLE_SSE_S3)"
|
||||
@echo " PYTHON=$(PYTHON)"
|
||||
|
||||
check-binary:
|
||||
@if ! command -v $(SEAWEEDFS_BINARY) > /dev/null 2>&1; then \
|
||||
echo "$(RED)Error: SeaweedFS binary '$(SEAWEEDFS_BINARY)' not found in PATH$(NC)"; \
|
||||
echo "Please build SeaweedFS first by running 'make' in the root directory"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)SeaweedFS binary found: $$(which $(SEAWEEDFS_BINARY))$(NC)"
|
||||
|
||||
check-python:
|
||||
@if ! command -v $(PYTHON) > /dev/null 2>&1; then \
|
||||
echo "$(RED)Error: Python '$(PYTHON)' not found$(NC)"; \
|
||||
echo "Please install Python 3.8 or later"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "$(GREEN)Python found: $$(which $(PYTHON)) ($$($(PYTHON) --version))$(NC)"
|
||||
|
||||
setup-python: check-python
|
||||
@echo "$(YELLOW)Setting up Python virtual environment...$(NC)"
|
||||
@if [ ! -d "$(VENV_DIR)" ]; then \
|
||||
$(PYTHON) -m venv $(VENV_DIR); \
|
||||
echo "$(GREEN)Virtual environment created$(NC)"; \
|
||||
fi
|
||||
@echo "$(YELLOW)Installing Python dependencies...$(NC)"
|
||||
@$(VENV_DIR)/bin/pip install --upgrade pip > /dev/null
|
||||
@$(VENV_DIR)/bin/pip install -r requirements.txt
|
||||
@echo "$(GREEN)Python dependencies installed successfully$(NC)"
|
||||
|
||||
start-seaweedfs-ci: check-binary
|
||||
@echo "$(YELLOW)Starting SeaweedFS server for Parquet testing...$(NC)"
|
||||
|
||||
# Clean up any existing processes first (CI-safe) - aggressive cleanup
|
||||
@echo "Aggressively cleaning up any existing processes on S3 port $(S3_PORT) and master port 9333..."
|
||||
@if command -v lsof >/dev/null 2>&1; then \
|
||||
for attempt in 1 2 3; do \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -9 $$pid 2>/dev/null || true; done; \
|
||||
lsof -ti :9333 2>/dev/null | head -5 | while read pid; do kill -9 $$pid 2>/dev/null || true; done; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
fi
|
||||
@sleep 3
|
||||
|
||||
# Create necessary directories
|
||||
@mkdir -p /tmp/seaweedfs-test-parquet
|
||||
|
||||
# Clean up any old server logs
|
||||
@rm -f /tmp/seaweedfs-parquet-*.log || true
|
||||
|
||||
# Start weed mini with embedded S3
|
||||
@echo "Starting weed mini with embedded S3..."
|
||||
@if [ "$(ENABLE_SSE_S3)" = "true" ]; then \
|
||||
echo " SSE-S3 encryption: ENABLED"; \
|
||||
printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}],"buckets":[{"name":"$(BUCKET_NAME)","encryption":{"sseS3":{"enabled":true}}}]}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" > /tmp/seaweedfs-parquet-s3.json; \
|
||||
else \
|
||||
echo " SSE-S3 encryption: DISABLED"; \
|
||||
printf '{"identities":[{"name":"%s","credentials":[{"accessKey":"%s","secretKey":"%s"}],"actions":["Admin","Read","Write"]}]}' "$(ACCESS_KEY)" "$(ACCESS_KEY)" "$(SECRET_KEY)" > /tmp/seaweedfs-parquet-s3.json; \
|
||||
fi
|
||||
@$(SEAWEEDFS_BINARY) mini \
|
||||
-dir=/tmp/seaweedfs-test-parquet \
|
||||
-ip.bind=0.0.0.0 \
|
||||
-s3.port=$(S3_PORT) \
|
||||
-s3.config=/tmp/seaweedfs-parquet-s3.json \
|
||||
> /tmp/seaweedfs-parquet-mini.log 2>&1 & echo $$! > /tmp/weed-mini.pid
|
||||
@echo "Waiting for S3 service to be fully ready (max 90 seconds)..."
|
||||
@bash -c 'for i in $$(seq 1 90); do \
|
||||
if curl -s -H "Authorization: AWS4-HMAC-SHA256 Credential=$(ACCESS_KEY)" http://localhost:$(S3_PORT)/ > /dev/null 2>&1; then \
|
||||
echo "✅ S3 service is ready"; \
|
||||
sleep 2; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo "ERROR S3 service failed to start within 90 seconds"; \
|
||||
echo "=== Server log output ==="; \
|
||||
cat /tmp/seaweedfs-parquet-mini.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1'
|
||||
|
||||
start-seaweedfs: check-binary
|
||||
@echo "$(YELLOW)Starting SeaweedFS server for Parquet testing...$(NC)"
|
||||
@# Use port-based cleanup for consistency and safety
|
||||
@echo "Cleaning up any existing processes..."
|
||||
@lsof -ti :$(S3_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@sleep 2
|
||||
@$(MAKE) start-seaweedfs-ci
|
||||
|
||||
stop-seaweedfs:
|
||||
@echo "$(YELLOW)Stopping SeaweedFS server...$(NC)"
|
||||
@# Use port-based cleanup for consistency and safety
|
||||
@if [ -f /tmp/weed-mini.pid ]; then \
|
||||
echo "Stopping weed mini..."; \
|
||||
kill $$(cat /tmp/weed-mini.pid) || true; \
|
||||
rm -f /tmp/weed-mini.pid; \
|
||||
fi
|
||||
@lsof -ti :$(S3_PORT) 2>/dev/null | xargs -r kill -TERM || true
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server stopped$(NC)"
|
||||
|
||||
# CI-safe server stop that's more conservative
|
||||
stop-seaweedfs-safe:
|
||||
@echo "$(YELLOW)Safely stopping SeaweedFS server...$(NC)"
|
||||
@# Use port-based cleanup which is safer in CI
|
||||
@if command -v lsof >/dev/null 2>&1; then \
|
||||
echo "Using lsof for port-based cleanup..."; \
|
||||
lsof -ti :$(S3_PORT) 2>/dev/null | head -5 | while read pid; do kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
else \
|
||||
echo "lsof not available, using netstat approach..."; \
|
||||
netstat -tlnp 2>/dev/null | grep :$(S3_PORT) | awk '{print $$7}' | cut -d/ -f1 | head -5 | while read pid; do [ "$$pid" != "-" ] && kill -TERM $$pid 2>/dev/null || true; done; \
|
||||
fi
|
||||
@sleep 2
|
||||
@echo "$(GREEN)SeaweedFS server safely stopped$(NC)"
|
||||
|
||||
clean:
|
||||
@echo "$(YELLOW)Cleaning up Parquet test artifacts...$(NC)"
|
||||
@rm -rf /tmp/seaweedfs-test-parquet-*
|
||||
@rm -f /tmp/seaweedfs-parquet-*.log
|
||||
@rm -f /tmp/seaweedfs-parquet-s3.json
|
||||
@rm -f s3_parquet_test_errors_*.log
|
||||
@rm -rf $(VENV_DIR)
|
||||
@echo "$(GREEN)Parquet test cleanup completed$(NC)"
|
||||
|
||||
# Test with automatic server management (GitHub Actions compatible)
|
||||
test-with-server: build-weed setup-python
|
||||
@echo "🚀 Starting Parquet integration tests with automated server management..."
|
||||
@echo "Starting SeaweedFS cluster..."
|
||||
@if $(MAKE) start-seaweedfs-ci > weed-test.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully"; \
|
||||
echo "Running Parquet integration tests..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) $(PYTHON_TEST_SCRIPT) || exit 1; \
|
||||
echo "✅ All tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test.log 2>/dev/null || echo "No startup log available"; \
|
||||
echo "=== System information ==="; \
|
||||
ps aux | grep -E "weed|make" | grep -v grep || echo "No relevant processes found"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Run tests assuming SeaweedFS is already running
|
||||
test: setup-python
|
||||
@echo "$(YELLOW)Running Parquet integration tests...$(NC)"
|
||||
@echo "$(YELLOW)Assuming SeaweedFS is already running on localhost:$(S3_PORT)$(NC)"
|
||||
@S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) $(PYTHON_TEST_SCRIPT)
|
||||
|
||||
# Run quick tests with small files only
|
||||
test-quick: setup-python
|
||||
@echo "$(YELLOW)Running quick Parquet tests (small files only)...$(NC)"
|
||||
@echo "$(YELLOW)Assuming SeaweedFS is already running on localhost:$(S3_PORT)$(NC)"
|
||||
@S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
TEST_QUICK=1 \
|
||||
$(VENV_DIR)/bin/$(PYTHON) $(PYTHON_TEST_SCRIPT)
|
||||
|
||||
# Test implicit directory fix for s3fs compatibility
|
||||
test-implicit-dir: setup-python
|
||||
@echo "$(YELLOW)Running implicit directory fix tests...$(NC)"
|
||||
@echo "$(YELLOW)Assuming SeaweedFS is already running on localhost:$(S3_PORT)$(NC)"
|
||||
@S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=test-implicit-dir \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_implicit_directory_fix.py
|
||||
|
||||
# Test implicit directory fix with automatic server management
|
||||
test-implicit-dir-with-server: build-weed setup-python
|
||||
@echo "🚀 Starting implicit directory fix tests with automated server management..."
|
||||
@echo "Starting SeaweedFS cluster..."
|
||||
@if $(MAKE) start-seaweedfs-ci > weed-test.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully"; \
|
||||
echo "Running implicit directory fix tests..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=test-implicit-dir \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_implicit_directory_fix.py || exit 1; \
|
||||
echo "✅ All tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Debug targets
|
||||
debug-logs:
|
||||
@echo "$(YELLOW)=== Mini Log ===$(NC)"
|
||||
@tail -n 50 /tmp/seaweedfs-parquet-mini.log || echo "No mini log found"
|
||||
|
||||
debug-status:
|
||||
@echo "$(YELLOW)=== Process Status ===$(NC)"
|
||||
@ps aux | grep -E "(weed|seaweedfs)" | grep -v grep || echo "No SeaweedFS processes found"
|
||||
@echo "$(YELLOW)=== Port Status ===$(NC)"
|
||||
@netstat -an | grep -E "($(S3_PORT))" || echo "No ports in use"
|
||||
|
||||
# Manual test targets for development
|
||||
manual-start: start-seaweedfs
|
||||
@echo "$(GREEN)SeaweedFS with S3 is now running for manual testing$(NC)"
|
||||
@echo "You can now run Parquet tests manually"
|
||||
@echo "Run 'make manual-stop' when finished"
|
||||
|
||||
manual-stop: stop-seaweedfs clean
|
||||
|
||||
# Test PyArrow's native S3 filesystem
|
||||
test-native-s3: setup-python
|
||||
@echo "$(YELLOW)Running PyArrow native S3 filesystem tests...$(NC)"
|
||||
@echo "$(YELLOW)Assuming SeaweedFS is already running on localhost:$(S3_PORT)$(NC)"
|
||||
@S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_pyarrow_native_s3.py
|
||||
|
||||
# Test PyArrow's native S3 filesystem with automatic server management
|
||||
test-native-s3-with-server: build-weed setup-python
|
||||
@echo "🚀 Starting PyArrow native S3 filesystem tests with automated server management..."
|
||||
@echo "Starting SeaweedFS cluster..."
|
||||
@if $(MAKE) start-seaweedfs-ci > weed-test.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully"; \
|
||||
echo "Running PyArrow native S3 filesystem tests..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_pyarrow_native_s3.py || exit 1; \
|
||||
echo "✅ All tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Test cross-filesystem compatibility (s3fs ↔ PyArrow native S3)
|
||||
test-cross-fs: setup-python
|
||||
@echo "$(YELLOW)Running cross-filesystem compatibility tests...$(NC)"
|
||||
@echo "$(YELLOW)Assuming SeaweedFS is already running on localhost:$(S3_PORT)$(NC)"
|
||||
@S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_cross_filesystem_compatibility.py
|
||||
|
||||
# Test cross-filesystem compatibility with automatic server management
|
||||
test-cross-fs-with-server: build-weed setup-python
|
||||
@echo "🚀 Starting cross-filesystem compatibility tests with automated server management..."
|
||||
@echo "Starting SeaweedFS cluster..."
|
||||
@if $(MAKE) start-seaweedfs-ci > weed-test.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully"; \
|
||||
echo "Running cross-filesystem compatibility tests..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_cross_filesystem_compatibility.py || exit 1; \
|
||||
echo "✅ All tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Test PyArrow's native S3 filesystem compatibility with SSE-S3 enabled backend
|
||||
# (For encryption-specific validation, use test-sse-s3-compat)
|
||||
test-native-s3-with-sse: build-weed setup-python
|
||||
@echo "🚀 Testing PyArrow native S3 compatibility with SSE-S3 enabled backend..."
|
||||
@echo "Starting SeaweedFS cluster with SSE-S3 enabled..."
|
||||
@if $(MAKE) start-seaweedfs-ci ENABLE_SSE_S3=true > weed-test-sse.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully with SSE-S3"; \
|
||||
echo "Running PyArrow native S3 filesystem tests with SSE-S3..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_pyarrow_native_s3.py || exit 1; \
|
||||
echo "✅ All SSE-S3 tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster with SSE-S3"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test-sse.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# Comprehensive SSE-S3 compatibility test
|
||||
test-sse-s3-compat: build-weed setup-python
|
||||
@echo "🚀 Starting comprehensive SSE-S3 compatibility tests..."
|
||||
@echo "Starting SeaweedFS cluster with SSE-S3 enabled..."
|
||||
@if $(MAKE) start-seaweedfs-ci ENABLE_SSE_S3=true > weed-test-sse-compat.log 2>&1; then \
|
||||
echo "✅ SeaweedFS cluster started successfully with SSE-S3"; \
|
||||
echo "Running comprehensive SSE-S3 compatibility tests..."; \
|
||||
trap '$(MAKE) -C $(TEST_DIR) stop-seaweedfs-safe || true' EXIT; \
|
||||
S3_ENDPOINT_URL=http://localhost:$(S3_PORT) \
|
||||
S3_ACCESS_KEY=$(ACCESS_KEY) \
|
||||
S3_SECRET_KEY=$(SECRET_KEY) \
|
||||
BUCKET_NAME=$(BUCKET_NAME) \
|
||||
$(VENV_DIR)/bin/$(PYTHON) test_sse_s3_compatibility.py || exit 1; \
|
||||
echo "✅ All SSE-S3 compatibility tests completed successfully"; \
|
||||
else \
|
||||
echo "❌ Failed to start SeaweedFS cluster with SSE-S3"; \
|
||||
echo "=== Server startup logs ==="; \
|
||||
tail -100 weed-test-sse-compat.log 2>/dev/null || echo "No startup log available"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
# CI/CD targets
|
||||
ci-test: test-with-server
|
||||
|
||||
312
test/s3/parquet/README.md
Normal file
312
test/s3/parquet/README.md
Normal file
@@ -0,0 +1,312 @@
|
||||
# PyArrow Parquet S3 Compatibility Tests
|
||||
|
||||
This directory contains tests for PyArrow Parquet compatibility with SeaweedFS S3 API, including the implicit directory detection fix.
|
||||
|
||||
## Overview
|
||||
|
||||
**Status**: ✅ **All PyArrow methods work correctly with SeaweedFS**
|
||||
|
||||
SeaweedFS implements implicit directory detection to improve compatibility with s3fs and PyArrow. When PyArrow writes datasets using `write_dataset()`, it may create directory markers that can confuse s3fs. SeaweedFS now handles these correctly by returning 404 for HEAD requests on implicit directories (directories with children), forcing s3fs to use LIST-based discovery.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Running the Example Script
|
||||
|
||||
```bash
|
||||
# Start SeaweedFS server
|
||||
make start-seaweedfs-ci
|
||||
|
||||
# Run the example script
|
||||
python3 example_pyarrow_native.py
|
||||
|
||||
# Or with uv (if available)
|
||||
uv run example_pyarrow_native.py
|
||||
|
||||
# Stop the server when done
|
||||
make stop-seaweedfs-safe
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Setup Python environment
|
||||
make setup-python
|
||||
|
||||
# Run all tests with server (small and large files)
|
||||
make test-with-server
|
||||
|
||||
# Run quick tests with small files only (faster for development)
|
||||
make test-quick
|
||||
|
||||
# Run implicit directory fix tests
|
||||
make test-implicit-dir-with-server
|
||||
|
||||
# Run PyArrow native S3 filesystem tests
|
||||
make test-native-s3-with-server
|
||||
|
||||
# Run cross-filesystem compatibility tests (s3fs ↔ PyArrow native)
|
||||
make test-cross-fs-with-server
|
||||
|
||||
# Run SSE-S3 encryption tests
|
||||
make test-sse-s3-compat
|
||||
|
||||
# Clean up
|
||||
make clean
|
||||
```
|
||||
|
||||
### Using PyArrow with SeaweedFS
|
||||
|
||||
#### Option 1: Using s3fs (recommended for compatibility)
|
||||
|
||||
```python
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import pyarrow.dataset as pads
|
||||
import s3fs
|
||||
|
||||
# Configure s3fs
|
||||
fs = s3fs.S3FileSystem(
|
||||
key='your_access_key',
|
||||
secret='your_secret_key',
|
||||
endpoint_url='http://localhost:8333',
|
||||
use_ssl=False
|
||||
)
|
||||
|
||||
# Write dataset (creates directory structure)
|
||||
table = pa.table({'id': [1, 2, 3], 'value': ['a', 'b', 'c']})
|
||||
pads.write_dataset(table, 'bucket/dataset', filesystem=fs)
|
||||
|
||||
# Read dataset (all methods work!)
|
||||
dataset = pads.dataset('bucket/dataset', filesystem=fs) # ✅
|
||||
table = pq.read_table('bucket/dataset', filesystem=fs) # ✅
|
||||
dataset = pq.ParquetDataset('bucket/dataset', filesystem=fs) # ✅
|
||||
```
|
||||
|
||||
#### Option 2: Using PyArrow's native S3 filesystem (pure PyArrow)
|
||||
|
||||
```python
|
||||
import pyarrow as pa
|
||||
import pyarrow.parquet as pq
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.fs as pafs
|
||||
|
||||
# Configure PyArrow's native S3 filesystem
|
||||
s3 = pafs.S3FileSystem(
|
||||
access_key='your_access_key',
|
||||
secret_key='your_secret_key',
|
||||
endpoint_override='localhost:8333',
|
||||
scheme='http',
|
||||
allow_bucket_creation=True,
|
||||
allow_bucket_deletion=True
|
||||
)
|
||||
|
||||
# Write dataset
|
||||
table = pa.table({'id': [1, 2, 3], 'value': ['a', 'b', 'c']})
|
||||
pads.write_dataset(table, 'bucket/dataset', filesystem=s3)
|
||||
|
||||
# Read dataset (all methods work!)
|
||||
table = pq.read_table('bucket/dataset', filesystem=s3) # ✅
|
||||
dataset = pq.ParquetDataset('bucket/dataset', filesystem=s3) # ✅
|
||||
dataset = pads.dataset('bucket/dataset', filesystem=s3) # ✅
|
||||
```
|
||||
|
||||
## Test Files
|
||||
|
||||
### Main Test Suite
|
||||
- **`s3_parquet_test.py`** - Comprehensive PyArrow test suite
|
||||
- Tests 2 write methods × 5 read methods × 2 dataset sizes = 20 combinations
|
||||
- Uses s3fs library for S3 operations
|
||||
- All tests pass with the implicit directory fix ✅
|
||||
|
||||
### PyArrow Native S3 Tests
|
||||
- **`test_pyarrow_native_s3.py`** - PyArrow's native S3 filesystem tests
|
||||
- Tests PyArrow's built-in S3FileSystem (pyarrow.fs.S3FileSystem)
|
||||
- Pure PyArrow solution without s3fs dependency
|
||||
- Tests 3 read methods × 2 dataset sizes = 6 scenarios
|
||||
- All tests pass ✅
|
||||
|
||||
- **`test_sse_s3_compatibility.py`** - SSE-S3 encryption compatibility tests
|
||||
- Tests PyArrow native S3 with SSE-S3 server-side encryption
|
||||
- Tests 5 different file sizes (10 to 500,000 rows)
|
||||
- Verifies multipart upload encryption works correctly
|
||||
- All tests pass ✅
|
||||
|
||||
### Cross-Filesystem Compatibility Tests
|
||||
- **`test_cross_filesystem_compatibility.py`** - Verifies cross-compatibility between s3fs and PyArrow native S3
|
||||
- Tests write with s3fs → read with PyArrow native S3
|
||||
- Tests write with PyArrow native S3 → read with s3fs
|
||||
- Tests 2 directions × 3 read methods × 2 dataset sizes = 12 scenarios
|
||||
- Validates that files written by either filesystem can be read by the other
|
||||
- **All tests pass** ✅
|
||||
- See **`CROSS_FILESYSTEM_COMPATIBILITY.md`** for detailed test results and analysis
|
||||
|
||||
### Implicit Directory Tests
|
||||
- **`test_implicit_directory_fix.py`** - Specific tests for the implicit directory fix
|
||||
- Tests HEAD request behavior
|
||||
- Tests s3fs directory detection
|
||||
- Tests PyArrow dataset reading
|
||||
- All 6 tests pass ✅
|
||||
|
||||
### Examples
|
||||
- **`example_pyarrow_native.py`** - Simple standalone example
|
||||
- Demonstrates PyArrow's native S3 filesystem usage
|
||||
- Can be run with `uv run` or regular Python
|
||||
- Minimal dependencies (pyarrow, boto3)
|
||||
|
||||
### Configuration
|
||||
- **`Makefile`** - Build and test automation
|
||||
- **`requirements.txt`** - Python dependencies (pyarrow, s3fs, boto3)
|
||||
- **`.gitignore`** - Ignore patterns for test artifacts
|
||||
|
||||
## Documentation
|
||||
|
||||
### Technical Documentation
|
||||
- **`TEST_COVERAGE.md`** - Comprehensive test coverage documentation
|
||||
- Unit tests (Go): 17 test cases
|
||||
- Integration tests (Python): 6 test cases
|
||||
- End-to-end tests (Python): 20 test cases
|
||||
|
||||
- **`FINAL_ROOT_CAUSE_ANALYSIS.md`** - Deep technical analysis
|
||||
- Root cause of the s3fs compatibility issue
|
||||
- How the implicit directory fix works
|
||||
- Performance considerations
|
||||
|
||||
- **`CROSS_FILESYSTEM_COMPATIBILITY.md`** - Cross-filesystem compatibility test results ✅ **NEW**
|
||||
- Validates s3fs ↔ PyArrow native S3 interoperability
|
||||
- Confirms files written by either can be read by the other
|
||||
- Test methodology and detailed results
|
||||
|
||||
- **`MINIO_DIRECTORY_HANDLING.md`** - Comparison with MinIO
|
||||
- How MinIO handles directory markers
|
||||
- Differences in implementation approaches
|
||||
|
||||
## The Implicit Directory Fix
|
||||
|
||||
### Problem
|
||||
When PyArrow writes datasets with `write_dataset()`, it may create 0-byte directory markers. s3fs's `info()` method calls HEAD on these paths, and if HEAD returns 200 with size=0, s3fs incorrectly reports them as files instead of directories. This causes PyArrow to fail with "Parquet file size is 0 bytes".
|
||||
|
||||
### Solution
|
||||
SeaweedFS now returns 404 for HEAD requests on implicit directories (0-byte objects or directories with children, when requested without a trailing slash). This forces s3fs to fall back to LIST-based discovery, which correctly identifies directories by checking for children.
|
||||
|
||||
### Implementation
|
||||
The fix is implemented in `weed/s3api/s3api_object_handlers.go`:
|
||||
- `HeadObjectHandler` - Returns 404 for implicit directories
|
||||
- `hasChildren` - Helper function to check if a path has children
|
||||
|
||||
See the source code for detailed inline documentation.
|
||||
|
||||
### Test Coverage
|
||||
- **Unit tests** (Go): `weed/s3api/s3api_implicit_directory_test.go`
|
||||
- Run: `cd weed/s3api && go test -v -run TestImplicitDirectory`
|
||||
|
||||
- **Integration tests** (Python): `test_implicit_directory_fix.py`
|
||||
- Run: `cd test/s3/parquet && make test-implicit-dir-with-server`
|
||||
|
||||
- **End-to-end tests** (Python): `s3_parquet_test.py`
|
||||
- Run: `cd test/s3/parquet && make test-with-server`
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
```bash
|
||||
# Setup
|
||||
make setup-python # Create Python virtual environment and install dependencies
|
||||
make build-weed # Build SeaweedFS binary
|
||||
|
||||
# Testing
|
||||
make test # Run full tests (assumes server is already running)
|
||||
make test-with-server # Run full PyArrow test suite with server (small + large files)
|
||||
make test-quick # Run quick tests with small files only (assumes server is running)
|
||||
make test-implicit-dir-with-server # Run implicit directory tests with server
|
||||
make test-native-s3 # Run PyArrow native S3 tests (assumes server is running)
|
||||
make test-native-s3-with-server # Run PyArrow native S3 tests with server management
|
||||
make test-cross-fs # Run cross-filesystem compatibility tests (assumes server is running)
|
||||
make test-cross-fs-with-server # Run cross-filesystem compatibility tests with server management
|
||||
make test-sse-s3-compat # Run comprehensive SSE-S3 encryption compatibility tests
|
||||
|
||||
# Server Management
|
||||
make start-seaweedfs-ci # Start SeaweedFS in background (CI mode)
|
||||
make stop-seaweedfs-safe # Stop SeaweedFS gracefully
|
||||
make clean # Clean up all test artifacts
|
||||
|
||||
# Development
|
||||
make help # Show all available targets
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
The tests are automatically run in GitHub Actions on every push/PR that affects S3 or filer code:
|
||||
|
||||
**Workflow**: `.github/workflows/s3-parquet-tests.yml`
|
||||
|
||||
**Test Matrix**:
|
||||
- Python versions: 3.9, 3.11, 3.12
|
||||
- PyArrow integration tests (s3fs): 20 test combinations
|
||||
- PyArrow native S3 tests: 6 test scenarios ✅
|
||||
- Cross-filesystem compatibility tests: 12 test scenarios ✅ **NEW**
|
||||
- SSE-S3 encryption tests: 5 file sizes ✅
|
||||
- Implicit directory fix tests: 6 test scenarios
|
||||
- Go unit tests: 17 test cases
|
||||
|
||||
**Test Steps** (run for each Python version):
|
||||
1. Build SeaweedFS
|
||||
2. Run PyArrow Parquet integration tests (`make test-with-server`)
|
||||
3. Run implicit directory fix tests (`make test-implicit-dir-with-server`)
|
||||
4. Run PyArrow native S3 filesystem tests (`make test-native-s3-with-server`)
|
||||
5. Run cross-filesystem compatibility tests (`make test-cross-fs-with-server`) ✅ **NEW**
|
||||
6. Run SSE-S3 encryption compatibility tests (`make test-sse-s3-compat`)
|
||||
7. Run Go unit tests for implicit directory handling
|
||||
|
||||
**Triggers**:
|
||||
- Push/PR to master (when `weed/s3api/**` or `weed/filer/**` changes)
|
||||
- Manual trigger via GitHub UI (workflow_dispatch)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.8+
|
||||
- PyArrow 22.0.0+
|
||||
- s3fs 2024.12.0+
|
||||
- boto3 1.40.0+
|
||||
- SeaweedFS (latest)
|
||||
|
||||
## AWS S3 Compatibility
|
||||
|
||||
The implicit directory fix makes SeaweedFS behavior more compatible with AWS S3:
|
||||
- AWS S3 typically doesn't create directory markers for implicit directories
|
||||
- HEAD on "dataset" (when only "dataset/file.txt" exists) returns 404 on AWS
|
||||
- SeaweedFS now matches this behavior for implicit directories with children
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
✅ **Implicit directories with children** → 404 (forces LIST-based discovery)
|
||||
✅ **Empty files (0-byte, no children)** → 200 (legitimate empty file)
|
||||
✅ **Empty directories (no children)** → 200 (legitimate empty directory)
|
||||
✅ **Explicit directory requests (trailing slash)** → 200 (normal directory behavior)
|
||||
✅ **Versioned buckets** → Skip implicit directory check (versioned semantics)
|
||||
✅ **Regular files** → 200 (normal file behavior)
|
||||
|
||||
## Performance
|
||||
|
||||
The implicit directory check adds minimal overhead:
|
||||
- Only triggered for 0-byte objects or directories without trailing slash
|
||||
- Cost: One LIST operation with Limit=1 (~1-5ms)
|
||||
- No impact on regular file operations
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new tests:
|
||||
1. Add test cases to the appropriate test file
|
||||
2. Update TEST_COVERAGE.md
|
||||
3. Run the full test suite to ensure no regressions
|
||||
4. Update this README if adding new functionality
|
||||
|
||||
## References
|
||||
|
||||
- [PyArrow Documentation](https://arrow.apache.org/docs/python/parquet.html)
|
||||
- [s3fs Documentation](https://s3fs.readthedocs.io/)
|
||||
- [SeaweedFS S3 API](https://github.com/seaweedfs/seaweedfs/wiki/Amazon-S3-API)
|
||||
- [AWS S3 API Reference](https://docs.aws.amazon.com/AmazonS3/latest/API/)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: November 19, 2025
|
||||
**Status**: All tests passing ✅
|
||||
86
test/s3/parquet/debug_write_dataset.py
Normal file
86
test/s3/parquet/debug_write_dataset.py
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Debug script to understand what pads.write_dataset creates."""
|
||||
|
||||
import sys
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import s3fs
|
||||
|
||||
# Create a simple test table
|
||||
table = pa.table({'id': [1, 2, 3], 'value': [1.0, 2.0, 3.0]})
|
||||
|
||||
# Initialize S3 filesystem
|
||||
fs = s3fs.S3FileSystem(
|
||||
client_kwargs={'endpoint_url': 'http://localhost:8333'},
|
||||
key='some_access_key1',
|
||||
secret='some_secret_key1',
|
||||
use_listings_cache=False,
|
||||
)
|
||||
|
||||
# Create bucket
|
||||
if not fs.exists('test-bucket'):
|
||||
fs.mkdir('test-bucket')
|
||||
|
||||
# Write with pads.write_dataset
|
||||
test_path = 's3://test-bucket/test-write-simple/'
|
||||
print(f"Writing to: {test_path}")
|
||||
print(f"Table schema: {table.schema}")
|
||||
print(f"Table rows: {table.num_rows}")
|
||||
|
||||
try:
|
||||
pads.write_dataset(table, test_path, format='parquet', filesystem=fs)
|
||||
print("\n✓ Write succeeded")
|
||||
|
||||
# List all files recursively
|
||||
print(f"\nListing all files recursively under {test_path}:")
|
||||
import os
|
||||
base_path = 'test-bucket/test-write-simple'
|
||||
def list_recursive(path, indent=0):
|
||||
try:
|
||||
items = fs.ls(path, detail=False)
|
||||
for item in items:
|
||||
is_dir = fs.isdir(item)
|
||||
item_name = item.split('/')[-1] if '/' in item else item
|
||||
if is_dir:
|
||||
print(f"{' ' * indent}📁 {item_name}/")
|
||||
list_recursive(item, indent + 1)
|
||||
else:
|
||||
# Get file size
|
||||
try:
|
||||
info = fs.info(item)
|
||||
size = info.get('size', 0)
|
||||
print(f"{' ' * indent}📄 {item_name} ({size} bytes)")
|
||||
except:
|
||||
print(f"{' ' * indent}📄 {item_name}")
|
||||
except Exception as e:
|
||||
print(f"{' ' * indent}Error listing {path}: {e}")
|
||||
|
||||
list_recursive(base_path)
|
||||
|
||||
# Try to read back with different methods
|
||||
print(f"\n\nTrying to read back using different methods:")
|
||||
|
||||
# Method 1: pads.dataset with the same path
|
||||
print(f"\n1. pads.dataset('{test_path}'):")
|
||||
try:
|
||||
dataset = pads.dataset(test_path, format='parquet', filesystem=fs)
|
||||
result = dataset.to_table()
|
||||
print(f" ✓ Success: {result.num_rows} rows")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
# Method 2: pads.dataset with the dir containing parquet files
|
||||
print(f"\n2. pads.dataset without trailing slash:")
|
||||
test_path_no_slash = 's3://test-bucket/test-write-simple'
|
||||
try:
|
||||
dataset = pads.dataset(test_path_no_slash, format='parquet', filesystem=fs)
|
||||
result = dataset.to_table()
|
||||
print(f" ✓ Success: {result.num_rows} rows")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed: {e}")
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"✗ Error: {e}")
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
134
test/s3/parquet/example_pyarrow_native.py
Executable file
134
test/s3/parquet/example_pyarrow_native.py
Executable file
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# dependencies = [
|
||||
# "pyarrow>=22",
|
||||
# "boto3>=1.28.0",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
"""
|
||||
Simple example of using PyArrow's native S3 filesystem with SeaweedFS.
|
||||
|
||||
This is a minimal example demonstrating how to write and read Parquet files
|
||||
using PyArrow's built-in S3FileSystem without any additional dependencies
|
||||
like s3fs.
|
||||
|
||||
Usage:
|
||||
# Set environment variables
|
||||
export S3_ENDPOINT_URL=localhost:8333
|
||||
export S3_ACCESS_KEY=some_access_key1
|
||||
export S3_SECRET_KEY=some_secret_key1
|
||||
export BUCKET_NAME=test-parquet-bucket
|
||||
|
||||
# Run the script
|
||||
python3 example_pyarrow_native.py
|
||||
|
||||
# Or run with uv (if available)
|
||||
uv run example_pyarrow_native.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.fs as pafs
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
from parquet_test_utils import create_sample_table
|
||||
|
||||
# Configuration
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-parquet-bucket")
|
||||
S3_ENDPOINT_URL = os.getenv("S3_ENDPOINT_URL", "localhost:8333")
|
||||
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "some_secret_key1")
|
||||
|
||||
# Determine scheme from endpoint
|
||||
if S3_ENDPOINT_URL.startswith("http://"):
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL[7:]
|
||||
elif S3_ENDPOINT_URL.startswith("https://"):
|
||||
scheme = "https"
|
||||
endpoint = S3_ENDPOINT_URL[8:]
|
||||
else:
|
||||
scheme = "http" # Default to http for localhost
|
||||
endpoint = S3_ENDPOINT_URL
|
||||
|
||||
print(f"Connecting to S3 endpoint: {scheme}://{endpoint}")
|
||||
|
||||
# Initialize PyArrow's NATIVE S3 filesystem
|
||||
s3 = pafs.S3FileSystem(
|
||||
access_key=S3_ACCESS_KEY,
|
||||
secret_key=S3_SECRET_KEY,
|
||||
endpoint_override=endpoint,
|
||||
scheme=scheme,
|
||||
allow_bucket_creation=True,
|
||||
allow_bucket_deletion=True,
|
||||
)
|
||||
|
||||
print("✓ Connected to S3 endpoint")
|
||||
|
||||
|
||||
# Create bucket if needed (using boto3)
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=f"{scheme}://{endpoint}",
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name='us-east-1',
|
||||
)
|
||||
|
||||
try:
|
||||
s3_client.head_bucket(Bucket=BUCKET_NAME)
|
||||
print(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
except ClientError as e:
|
||||
if e.response['Error']['Code'] == '404':
|
||||
print(f"Creating bucket: {BUCKET_NAME}")
|
||||
s3_client.create_bucket(Bucket=BUCKET_NAME)
|
||||
print(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
else:
|
||||
raise
|
||||
except ImportError:
|
||||
print("Warning: boto3 not available, assuming bucket exists")
|
||||
|
||||
# Generate a unique filename
|
||||
filename = f"{BUCKET_NAME}/dataset-{secrets.token_hex(8)}/test.parquet"
|
||||
|
||||
print(f"\nWriting Parquet dataset to: {filename}")
|
||||
|
||||
# Write dataset
|
||||
table = create_sample_table(200_000)
|
||||
pads.write_dataset(
|
||||
table,
|
||||
filename,
|
||||
filesystem=s3,
|
||||
format="parquet",
|
||||
)
|
||||
|
||||
print(f"✓ Wrote {table.num_rows:,} rows")
|
||||
|
||||
# Read with pq.read_table
|
||||
print("\nReading with pq.read_table...")
|
||||
table_read = pq.read_table(filename, filesystem=s3)
|
||||
print(f"✓ Read {table_read.num_rows:,} rows")
|
||||
|
||||
# Read with pq.ParquetDataset
|
||||
print("\nReading with pq.ParquetDataset...")
|
||||
dataset = pq.ParquetDataset(filename, filesystem=s3)
|
||||
table_dataset = dataset.read()
|
||||
print(f"✓ Read {table_dataset.num_rows:,} rows")
|
||||
|
||||
# Read with pads.dataset
|
||||
print("\nReading with pads.dataset...")
|
||||
dataset_pads = pads.dataset(filename, filesystem=s3)
|
||||
table_pads = dataset_pads.to_table()
|
||||
print(f"✓ Read {table_pads.num_rows:,} rows")
|
||||
|
||||
print("\n✅ All operations completed successfully!")
|
||||
print(f"\nFile written to: {filename}")
|
||||
print("You can verify the file using the SeaweedFS S3 API or weed shell")
|
||||
|
||||
41
test/s3/parquet/parquet_test_utils.py
Normal file
41
test/s3/parquet/parquet_test_utils.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Shared utility functions for PyArrow Parquet tests.
|
||||
|
||||
This module provides common test utilities used across multiple test scripts
|
||||
to avoid code duplication and ensure consistency.
|
||||
"""
|
||||
|
||||
import pyarrow as pa
|
||||
|
||||
|
||||
def create_sample_table(num_rows: int = 5) -> pa.Table:
|
||||
"""Create a sample PyArrow table for testing.
|
||||
|
||||
Args:
|
||||
num_rows: Number of rows to generate (default: 5)
|
||||
|
||||
Returns:
|
||||
PyArrow Table with test data containing:
|
||||
- id: int64 sequential IDs (0 to num_rows-1)
|
||||
- name: string user names (user_0, user_1, ...)
|
||||
- value: float64 values (id * 1.5)
|
||||
- flag: bool alternating True/False based on even/odd id
|
||||
|
||||
Example:
|
||||
>>> table = create_sample_table(3)
|
||||
>>> print(table)
|
||||
pyarrow.Table
|
||||
id: int64
|
||||
name: string
|
||||
value: double
|
||||
flag: bool
|
||||
"""
|
||||
return pa.table(
|
||||
{
|
||||
"id": pa.array(range(num_rows), type=pa.int64()),
|
||||
"name": pa.array([f"user_{i}" for i in range(num_rows)], type=pa.string()),
|
||||
"value": pa.array([float(i) * 1.5 for i in range(num_rows)], type=pa.float64()),
|
||||
"flag": pa.array([i % 2 == 0 for i in range(num_rows)], type=pa.bool_()),
|
||||
}
|
||||
)
|
||||
|
||||
7
test/s3/parquet/requirements.txt
Normal file
7
test/s3/parquet/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
# Python dependencies for S3 Parquet tests
|
||||
# Install with: pip install -r requirements.txt
|
||||
|
||||
pyarrow>=10.0.0
|
||||
s3fs>=2023.12.0
|
||||
boto3>=1.28.0
|
||||
|
||||
421
test/s3/parquet/s3_parquet_test.py
Executable file
421
test/s3/parquet/s3_parquet_test.py
Executable file
@@ -0,0 +1,421 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for S3-compatible storage with PyArrow Parquet files.
|
||||
|
||||
This script tests different write methods (PyArrow write_dataset vs. pq.write_table to buffer)
|
||||
combined with different read methods (PyArrow dataset, direct s3fs read, buffered read) to
|
||||
identify which combinations work with large files that span multiple row groups.
|
||||
|
||||
This test specifically addresses issues with large tables using PyArrow where files span
|
||||
multiple row-groups (default row_group size is around 130,000 rows).
|
||||
|
||||
Requirements:
|
||||
- pyarrow>=22
|
||||
- s3fs>=2024.12.0
|
||||
|
||||
Environment Variables:
|
||||
S3_ENDPOINT_URL: S3 endpoint (default: http://localhost:8333)
|
||||
S3_ACCESS_KEY: S3 access key (default: some_access_key1)
|
||||
S3_SECRET_KEY: S3 secret key (default: some_secret_key1)
|
||||
BUCKET_NAME: S3 bucket name (default: test-parquet-bucket)
|
||||
TEST_QUICK: Run only small/quick tests (default: 0, set to 1 for quick mode)
|
||||
|
||||
Usage:
|
||||
# Run with default environment variables
|
||||
python3 s3_parquet_test.py
|
||||
|
||||
# Run with custom environment variables
|
||||
S3_ENDPOINT_URL=http://localhost:8333 \
|
||||
S3_ACCESS_KEY=mykey \
|
||||
S3_SECRET_KEY=mysecret \
|
||||
BUCKET_NAME=mybucket \
|
||||
python3 s3_parquet_test.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Tuple
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
try:
|
||||
import s3fs
|
||||
except ImportError:
|
||||
logging.error("s3fs not installed. Install with: pip install s3fs")
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
# Error log file
|
||||
ERROR_LOG_FILE = f"s3_parquet_test_errors_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
|
||||
# Configuration from environment variables with defaults
|
||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", "http://localhost:8333")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "some_secret_key1")
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-parquet-bucket")
|
||||
TEST_QUICK = os.getenv("TEST_QUICK", "0") == "1"
|
||||
|
||||
# Create randomized test directory
|
||||
TEST_RUN_ID = secrets.token_hex(8)
|
||||
TEST_DIR = f"{BUCKET_NAME}/parquet-tests/{TEST_RUN_ID}"
|
||||
|
||||
# Test file sizes
|
||||
TEST_SIZES = {
|
||||
"small": 5,
|
||||
"large": 200_000, # This will create multiple row groups
|
||||
}
|
||||
|
||||
# Filter to only small tests if quick mode is enabled
|
||||
if TEST_QUICK:
|
||||
TEST_SIZES = {"small": TEST_SIZES["small"]}
|
||||
logging.info("Quick test mode enabled - running only small tests")
|
||||
|
||||
|
||||
def create_sample_table(num_rows: int = 5) -> pa.Table:
|
||||
"""Create a sample PyArrow table for testing."""
|
||||
return pa.table({
|
||||
"id": pa.array(range(num_rows), type=pa.int64()),
|
||||
"name": pa.array([f"user_{i}" for i in range(num_rows)], type=pa.string()),
|
||||
"value": pa.array([float(i) * 1.5 for i in range(num_rows)], type=pa.float64()),
|
||||
"flag": pa.array([i % 2 == 0 for i in range(num_rows)], type=pa.bool_()),
|
||||
})
|
||||
|
||||
|
||||
def log_error(operation: str, short_msg: str) -> None:
|
||||
"""Log error details to file with full traceback."""
|
||||
with open(ERROR_LOG_FILE, "a") as f:
|
||||
f.write(f"\n{'='*80}\n")
|
||||
f.write(f"Operation: {operation}\n")
|
||||
f.write(f"Time: {datetime.now().isoformat()}\n")
|
||||
f.write(f"Message: {short_msg}\n")
|
||||
f.write("Full Traceback:\n")
|
||||
f.write(traceback.format_exc())
|
||||
f.write(f"{'='*80}\n")
|
||||
|
||||
|
||||
def init_s3fs() -> s3fs.S3FileSystem:
|
||||
"""Initialize and return S3FileSystem."""
|
||||
logging.info("Initializing S3FileSystem...")
|
||||
logging.info(f" Endpoint: {S3_ENDPOINT_URL}")
|
||||
logging.info(f" Bucket: {BUCKET_NAME}")
|
||||
try:
|
||||
fs = s3fs.S3FileSystem(
|
||||
client_kwargs={"endpoint_url": S3_ENDPOINT_URL},
|
||||
key=S3_ACCESS_KEY,
|
||||
secret=S3_SECRET_KEY,
|
||||
use_listings_cache=False,
|
||||
)
|
||||
logging.info("✓ S3FileSystem initialized successfully\n")
|
||||
return fs
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to initialize S3FileSystem")
|
||||
raise
|
||||
|
||||
|
||||
def ensure_bucket_exists(fs: s3fs.S3FileSystem) -> None:
|
||||
"""Ensure the test bucket exists."""
|
||||
try:
|
||||
if not fs.exists(BUCKET_NAME):
|
||||
logging.info(f"Creating bucket: {BUCKET_NAME}")
|
||||
fs.mkdir(BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
else:
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to create/check bucket")
|
||||
raise
|
||||
|
||||
|
||||
# Write Methods
|
||||
|
||||
def write_with_pads(table: pa.Table, path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str]:
|
||||
"""Write using pads.write_dataset with filesystem parameter."""
|
||||
try:
|
||||
pads.write_dataset(table, path, format="parquet", filesystem=fs)
|
||||
return True, "pads.write_dataset"
|
||||
except Exception as e:
|
||||
error_msg = f"pads.write_dataset: {type(e).__name__}"
|
||||
log_error("write_with_pads", error_msg)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
def write_with_buffer_and_s3fs(table: pa.Table, path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str]:
|
||||
"""Write using pq.write_table to buffer, then upload via s3fs."""
|
||||
try:
|
||||
buffer = io.BytesIO()
|
||||
pq.write_table(table, buffer)
|
||||
buffer.seek(0)
|
||||
with fs.open(path, "wb") as f:
|
||||
f.write(buffer.read())
|
||||
return True, "pq.write_table+s3fs.open"
|
||||
except Exception as e:
|
||||
error_msg = f"pq.write_table+s3fs.open: {type(e).__name__}"
|
||||
log_error("write_with_buffer_and_s3fs", error_msg)
|
||||
return False, error_msg
|
||||
|
||||
|
||||
# Read Methods
|
||||
|
||||
def get_parquet_files(path: str, fs: s3fs.S3FileSystem) -> list:
|
||||
"""
|
||||
Helper to discover all parquet files for a given path.
|
||||
|
||||
Args:
|
||||
path: S3 path (file or directory)
|
||||
fs: S3FileSystem instance
|
||||
|
||||
Returns:
|
||||
List of parquet file paths
|
||||
|
||||
Raises:
|
||||
ValueError: If no parquet files are found in a directory
|
||||
"""
|
||||
if fs.isdir(path):
|
||||
# Find all parquet files in the directory
|
||||
files = [f for f in fs.ls(path) if f.endswith('.parquet')]
|
||||
if not files:
|
||||
raise ValueError(f"No parquet files found in directory: {path}")
|
||||
return files
|
||||
else:
|
||||
# Single file path
|
||||
return [path]
|
||||
|
||||
|
||||
def read_with_pads_dataset(path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str, int]:
|
||||
"""Read using pads.dataset - handles both single files and directories."""
|
||||
try:
|
||||
# pads.dataset() should auto-discover parquet files in the directory
|
||||
dataset = pads.dataset(path, format="parquet", filesystem=fs)
|
||||
result = dataset.to_table()
|
||||
return True, "pads.dataset", result.num_rows
|
||||
except Exception as e:
|
||||
error_msg = f"pads.dataset: {type(e).__name__}"
|
||||
log_error("read_with_pads_dataset", error_msg)
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def read_direct_s3fs(path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str, int]:
|
||||
"""Read directly via s3fs.open() streaming."""
|
||||
try:
|
||||
# Get all parquet files (handles both single file and directory)
|
||||
parquet_files = get_parquet_files(path, fs)
|
||||
|
||||
# Read all parquet files and concatenate them
|
||||
tables = []
|
||||
for file_path in parquet_files:
|
||||
with fs.open(file_path, "rb") as f:
|
||||
table = pq.read_table(f)
|
||||
tables.append(table)
|
||||
|
||||
# Concatenate all tables into one
|
||||
if len(tables) == 1:
|
||||
result = tables[0]
|
||||
else:
|
||||
result = pa.concat_tables(tables)
|
||||
|
||||
return True, "s3fs.open+pq.read_table", result.num_rows
|
||||
except Exception as e:
|
||||
error_msg = f"s3fs.open+pq.read_table: {type(e).__name__}"
|
||||
log_error("read_direct_s3fs", error_msg)
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def read_buffered_s3fs(path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str, int]:
|
||||
"""Read via s3fs.open() into buffer, then pq.read_table."""
|
||||
try:
|
||||
# Get all parquet files (handles both single file and directory)
|
||||
parquet_files = get_parquet_files(path, fs)
|
||||
|
||||
# Read all parquet files and concatenate them
|
||||
tables = []
|
||||
for file_path in parquet_files:
|
||||
with fs.open(file_path, "rb") as f:
|
||||
buffer = io.BytesIO(f.read())
|
||||
buffer.seek(0)
|
||||
table = pq.read_table(buffer)
|
||||
tables.append(table)
|
||||
|
||||
# Concatenate all tables into one
|
||||
if len(tables) == 1:
|
||||
result = tables[0]
|
||||
else:
|
||||
result = pa.concat_tables(tables)
|
||||
|
||||
return True, "s3fs.open+BytesIO+pq.read_table", result.num_rows
|
||||
except Exception as e:
|
||||
error_msg = f"s3fs.open+BytesIO+pq.read_table: {type(e).__name__}"
|
||||
log_error("read_buffered_s3fs", error_msg)
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def read_with_parquet_dataset(path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str, int]:
|
||||
"""Read using pq.ParquetDataset - designed for directories."""
|
||||
try:
|
||||
# ParquetDataset is specifically designed to handle directories
|
||||
dataset = pq.ParquetDataset(path, filesystem=fs)
|
||||
result = dataset.read()
|
||||
return True, "pq.ParquetDataset", result.num_rows
|
||||
except Exception as e:
|
||||
error_msg = f"pq.ParquetDataset: {type(e).__name__}"
|
||||
log_error("read_with_parquet_dataset", error_msg)
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def read_with_pq_read_table(path: str, fs: s3fs.S3FileSystem) -> Tuple[bool, str, int]:
|
||||
"""Read using pq.read_table with filesystem parameter."""
|
||||
try:
|
||||
# pq.read_table() with filesystem should handle directories
|
||||
result = pq.read_table(path, filesystem=fs)
|
||||
return True, "pq.read_table+filesystem", result.num_rows
|
||||
except Exception as e:
|
||||
error_msg = f"pq.read_table+filesystem: {type(e).__name__}"
|
||||
log_error("read_with_pq_read_table", error_msg)
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def test_combination(
|
||||
fs: s3fs.S3FileSystem,
|
||||
test_name: str,
|
||||
write_func,
|
||||
read_func,
|
||||
num_rows: int,
|
||||
) -> Tuple[bool, str]:
|
||||
"""Test a specific write/read combination."""
|
||||
table = create_sample_table(num_rows=num_rows)
|
||||
path = f"{TEST_DIR}/{test_name}/data.parquet"
|
||||
|
||||
# Write
|
||||
write_ok, write_msg = write_func(table, path, fs)
|
||||
if not write_ok:
|
||||
return False, f"WRITE_FAIL: {write_msg}"
|
||||
|
||||
# Read
|
||||
read_ok, read_msg, rows_read = read_func(path, fs)
|
||||
if not read_ok:
|
||||
return False, f"READ_FAIL: {read_msg}"
|
||||
|
||||
# Verify
|
||||
if rows_read != num_rows:
|
||||
return False, f"DATA_MISMATCH: expected {num_rows}, got {rows_read}"
|
||||
|
||||
return True, f"{write_msg} + {read_msg}"
|
||||
|
||||
|
||||
def cleanup_test_files(fs: s3fs.S3FileSystem) -> None:
|
||||
"""Clean up test files from S3."""
|
||||
try:
|
||||
if fs.exists(TEST_DIR):
|
||||
logging.info(f"Cleaning up test directory: {TEST_DIR}")
|
||||
fs.rm(TEST_DIR, recursive=True)
|
||||
logging.info("✓ Test directory cleaned up")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to cleanup test directory: {e}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all write/read method combinations."""
|
||||
print("=" * 80)
|
||||
print("Write/Read Method Combination Tests for S3-Compatible Storage")
|
||||
print("Testing PyArrow Parquet Files with Multiple Row Groups")
|
||||
if TEST_QUICK:
|
||||
print("*** QUICK TEST MODE - Small files only ***")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
print("Configuration:")
|
||||
print(f" S3 Endpoint: {S3_ENDPOINT_URL}")
|
||||
print(f" Bucket: {BUCKET_NAME}")
|
||||
print(f" Test Directory: {TEST_DIR}")
|
||||
print(f" Quick Mode: {'Yes (small files only)' if TEST_QUICK else 'No (all file sizes)'}")
|
||||
print()
|
||||
|
||||
try:
|
||||
fs = init_s3fs()
|
||||
ensure_bucket_exists(fs)
|
||||
except Exception as e:
|
||||
print(f"Cannot proceed without S3 connection: {e}")
|
||||
return 1
|
||||
|
||||
# Define all write methods
|
||||
write_methods = [
|
||||
("pads", write_with_pads),
|
||||
("buffer+s3fs", write_with_buffer_and_s3fs),
|
||||
]
|
||||
|
||||
# Define all read methods
|
||||
read_methods = [
|
||||
("pads.dataset", read_with_pads_dataset),
|
||||
("pq.ParquetDataset", read_with_parquet_dataset),
|
||||
("pq.read_table", read_with_pq_read_table),
|
||||
("s3fs+direct", read_direct_s3fs),
|
||||
("s3fs+buffered", read_buffered_s3fs),
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
# Test all combinations for each file size
|
||||
for size_name, num_rows in TEST_SIZES.items():
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Testing with {size_name} files ({num_rows:,} rows)")
|
||||
print(f"{'='*80}\n")
|
||||
print(f"{'Write Method':<20} | {'Read Method':<20} | {'Result':<40}")
|
||||
print("-" * 85)
|
||||
|
||||
for write_name, write_func in write_methods:
|
||||
for read_name, read_func in read_methods:
|
||||
test_name = f"{size_name}_{write_name}_{read_name}"
|
||||
success, message = test_combination(
|
||||
fs, test_name, write_func, read_func, num_rows
|
||||
)
|
||||
results.append((test_name, success, message))
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
print(f"{write_name:<20} | {read_name:<20} | {status}: {message[:35]}")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
passed = sum(1 for _, success, _ in results if success)
|
||||
total = len(results)
|
||||
print(f"\nTotal: {passed}/{total} passed\n")
|
||||
|
||||
# Group results by file size
|
||||
for size_name in TEST_SIZES.keys():
|
||||
size_results = [r for r in results if size_name in r[0]]
|
||||
size_passed = sum(1 for _, success, _ in size_results if success)
|
||||
print(f"{size_name.upper()}: {size_passed}/{len(size_results)} passed")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
if passed == total:
|
||||
print("✓ ALL TESTS PASSED!")
|
||||
else:
|
||||
print(f"✗ {total - passed} test(s) failed")
|
||||
print("\nFailing combinations:")
|
||||
for name, success, message in results:
|
||||
if not success:
|
||||
parts = name.split("_")
|
||||
size = parts[0]
|
||||
write = parts[1]
|
||||
read = "_".join(parts[2:])
|
||||
print(f" - {size:6} | {write:15} | {read:20} -> {message[:50]}")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
print(f"Error details logged to: {ERROR_LOG_FILE}")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Cleanup
|
||||
cleanup_test_files(fs)
|
||||
|
||||
return 0 if passed == total else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
453
test/s3/parquet/test_cross_filesystem_compatibility.py
Normal file
453
test/s3/parquet/test_cross_filesystem_compatibility.py
Normal file
@@ -0,0 +1,453 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cross-filesystem compatibility tests for PyArrow Parquet files.
|
||||
|
||||
This test verifies that Parquet files written using one filesystem implementation
|
||||
(s3fs or PyArrow native S3) can be correctly read using the other implementation.
|
||||
|
||||
Test Matrix:
|
||||
- Write with s3fs → Read with PyArrow native S3
|
||||
- Write with PyArrow native S3 → Read with s3fs
|
||||
|
||||
Requirements:
|
||||
- pyarrow>=22.0.0
|
||||
- s3fs>=2024.12.0
|
||||
- boto3>=1.40.0
|
||||
|
||||
Environment Variables:
|
||||
S3_ENDPOINT_URL: S3 endpoint (default: http://localhost:8333)
|
||||
S3_ACCESS_KEY: S3 access key (default: some_access_key1)
|
||||
S3_SECRET_KEY: S3 secret key (default: some_secret_key1)
|
||||
BUCKET_NAME: S3 bucket name (default: test-parquet-bucket)
|
||||
TEST_QUICK: Run only small/quick tests (default: 0, set to 1 for quick mode)
|
||||
|
||||
Usage:
|
||||
# Run with default environment variables
|
||||
python3 test_cross_filesystem_compatibility.py
|
||||
|
||||
# Run with custom environment variables
|
||||
S3_ENDPOINT_URL=http://localhost:8333 \
|
||||
S3_ACCESS_KEY=mykey \
|
||||
S3_SECRET_KEY=mysecret \
|
||||
BUCKET_NAME=mybucket \
|
||||
python3 test_cross_filesystem_compatibility.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.fs as pafs
|
||||
import pyarrow.parquet as pq
|
||||
import s3fs
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO3 = True
|
||||
except ImportError:
|
||||
HAS_BOTO3 = False
|
||||
|
||||
from parquet_test_utils import create_sample_table
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
# Configuration from environment variables with defaults
|
||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", "http://localhost:8333")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "some_secret_key1")
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-parquet-bucket")
|
||||
TEST_QUICK = os.getenv("TEST_QUICK", "0") == "1"
|
||||
|
||||
# Create randomized test directory
|
||||
TEST_RUN_ID = secrets.token_hex(8)
|
||||
TEST_DIR = f"parquet-cross-fs-tests/{TEST_RUN_ID}"
|
||||
|
||||
# Test file sizes
|
||||
TEST_SIZES = {
|
||||
"small": 5,
|
||||
"large": 200_000, # This will create multiple row groups
|
||||
}
|
||||
|
||||
# Filter to only small tests if quick mode is enabled
|
||||
if TEST_QUICK:
|
||||
TEST_SIZES = {"small": TEST_SIZES["small"]}
|
||||
logging.info("Quick test mode enabled - running only small tests")
|
||||
|
||||
|
||||
def init_s3fs() -> Optional[s3fs.S3FileSystem]:
|
||||
"""Initialize s3fs filesystem."""
|
||||
try:
|
||||
logging.info("Initializing s3fs...")
|
||||
fs = s3fs.S3FileSystem(
|
||||
client_kwargs={"endpoint_url": S3_ENDPOINT_URL},
|
||||
key=S3_ACCESS_KEY,
|
||||
secret=S3_SECRET_KEY,
|
||||
use_listings_cache=False,
|
||||
)
|
||||
logging.info("✓ s3fs initialized successfully")
|
||||
return fs
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to initialize s3fs")
|
||||
return None
|
||||
|
||||
|
||||
def init_pyarrow_s3() -> Tuple[Optional[pafs.S3FileSystem], str, str]:
|
||||
"""Initialize PyArrow's native S3 filesystem.
|
||||
|
||||
Returns:
|
||||
tuple: (S3FileSystem instance, scheme, endpoint)
|
||||
"""
|
||||
try:
|
||||
logging.info("Initializing PyArrow S3FileSystem...")
|
||||
|
||||
# Determine scheme from endpoint
|
||||
if S3_ENDPOINT_URL.startswith("http://"):
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL[7:] # Remove http://
|
||||
elif S3_ENDPOINT_URL.startswith("https://"):
|
||||
scheme = "https"
|
||||
endpoint = S3_ENDPOINT_URL[8:] # Remove https://
|
||||
else:
|
||||
# Default to http for localhost
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL
|
||||
|
||||
# Enable bucket creation and deletion for testing
|
||||
s3 = pafs.S3FileSystem(
|
||||
access_key=S3_ACCESS_KEY,
|
||||
secret_key=S3_SECRET_KEY,
|
||||
endpoint_override=endpoint,
|
||||
scheme=scheme,
|
||||
allow_bucket_creation=True,
|
||||
allow_bucket_deletion=True,
|
||||
)
|
||||
|
||||
logging.info("✓ PyArrow S3FileSystem initialized successfully")
|
||||
return s3, scheme, endpoint
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to initialize PyArrow S3FileSystem")
|
||||
return None, "", ""
|
||||
|
||||
|
||||
def ensure_bucket_exists(s3fs_fs: s3fs.S3FileSystem, pyarrow_s3: pafs.S3FileSystem) -> bool:
|
||||
"""Ensure the test bucket exists using s3fs."""
|
||||
try:
|
||||
if not s3fs_fs.exists(BUCKET_NAME):
|
||||
logging.info(f"Creating bucket: {BUCKET_NAME}")
|
||||
try:
|
||||
s3fs_fs.mkdir(BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
except FileExistsError:
|
||||
# Bucket was created between the check and mkdir call
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
else:
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to create/check bucket")
|
||||
return False
|
||||
|
||||
|
||||
def write_with_s3fs(table: pa.Table, path: str, s3fs_fs: s3fs.S3FileSystem) -> bool:
|
||||
"""Write Parquet file using s3fs filesystem."""
|
||||
try:
|
||||
pads.write_dataset(table, path, format="parquet", filesystem=s3fs_fs)
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to write with s3fs")
|
||||
return False
|
||||
|
||||
|
||||
def write_with_pyarrow_s3(table: pa.Table, path: str, pyarrow_s3: pafs.S3FileSystem) -> bool:
|
||||
"""Write Parquet file using PyArrow native S3 filesystem."""
|
||||
try:
|
||||
pads.write_dataset(table, path, format="parquet", filesystem=pyarrow_s3)
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to write with PyArrow S3")
|
||||
return False
|
||||
|
||||
|
||||
def read_with_s3fs(path: str, s3fs_fs: s3fs.S3FileSystem) -> Tuple[bool, Optional[pa.Table], str]:
|
||||
"""Read Parquet file using s3fs filesystem with multiple methods."""
|
||||
errors = []
|
||||
|
||||
# Try pq.read_table
|
||||
try:
|
||||
table = pq.read_table(path, filesystem=s3fs_fs)
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pq.read_table: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pq.read_table"
|
||||
|
||||
# Try pq.ParquetDataset
|
||||
try:
|
||||
dataset = pq.ParquetDataset(path, filesystem=s3fs_fs)
|
||||
table = dataset.read()
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pq.ParquetDataset: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pq.ParquetDataset"
|
||||
|
||||
# Try pads.dataset
|
||||
try:
|
||||
dataset = pads.dataset(path, format="parquet", filesystem=s3fs_fs)
|
||||
table = dataset.to_table()
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pads.dataset: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pads.dataset"
|
||||
|
||||
return False, None, " | ".join(errors)
|
||||
|
||||
|
||||
def read_with_pyarrow_s3(path: str, pyarrow_s3: pafs.S3FileSystem) -> Tuple[bool, Optional[pa.Table], str]:
|
||||
"""Read Parquet file using PyArrow native S3 filesystem with multiple methods."""
|
||||
errors = []
|
||||
|
||||
# Try pq.read_table
|
||||
try:
|
||||
table = pq.read_table(path, filesystem=pyarrow_s3)
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pq.read_table: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pq.read_table"
|
||||
|
||||
# Try pq.ParquetDataset
|
||||
try:
|
||||
dataset = pq.ParquetDataset(path, filesystem=pyarrow_s3)
|
||||
table = dataset.read()
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pq.ParquetDataset: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pq.ParquetDataset"
|
||||
|
||||
# Try pads.dataset
|
||||
try:
|
||||
dataset = pads.dataset(path, filesystem=pyarrow_s3)
|
||||
table = dataset.to_table()
|
||||
except Exception as e: # noqa: BLE001 - Intentionally broad for compatibility testing
|
||||
errors.append(f"pads.dataset: {type(e).__name__}: {e}")
|
||||
else:
|
||||
return True, table, "pads.dataset"
|
||||
|
||||
return False, None, " | ".join(errors)
|
||||
|
||||
|
||||
def verify_table_integrity(original: pa.Table, read: pa.Table) -> Tuple[bool, str]:
|
||||
"""Verify that read table matches the original table."""
|
||||
# Check row count
|
||||
if read.num_rows != original.num_rows:
|
||||
return False, f"Row count mismatch: expected {original.num_rows}, got {read.num_rows}"
|
||||
|
||||
# Check schema
|
||||
if not read.schema.equals(original.schema):
|
||||
return False, f"Schema mismatch: expected {original.schema}, got {read.schema}"
|
||||
|
||||
# Sort both tables by 'id' column before comparison to handle potential row order differences
|
||||
original_sorted = original.sort_by([('id', 'ascending')])
|
||||
read_sorted = read.sort_by([('id', 'ascending')])
|
||||
|
||||
# Check data equality
|
||||
if not read_sorted.equals(original_sorted):
|
||||
# Provide detailed error information
|
||||
error_details = []
|
||||
for col_name in original.column_names:
|
||||
col_original = original_sorted.column(col_name)
|
||||
col_read = read_sorted.column(col_name)
|
||||
if not col_original.equals(col_read):
|
||||
error_details.append(f"column '{col_name}' differs")
|
||||
return False, f"Data mismatch: {', '.join(error_details)}"
|
||||
|
||||
return True, "Data verified successfully"
|
||||
|
||||
|
||||
def test_write_s3fs_read_pyarrow(
|
||||
test_name: str,
|
||||
num_rows: int,
|
||||
s3fs_fs: s3fs.S3FileSystem,
|
||||
pyarrow_s3: pafs.S3FileSystem
|
||||
) -> Tuple[bool, str]:
|
||||
"""Test: Write with s3fs, read with PyArrow native S3."""
|
||||
try:
|
||||
table = create_sample_table(num_rows)
|
||||
path = f"{BUCKET_NAME}/{TEST_DIR}/{test_name}/data.parquet"
|
||||
|
||||
# Write with s3fs
|
||||
logging.info(f" Writing {num_rows:,} rows with s3fs to {path}...")
|
||||
if not write_with_s3fs(table, path, s3fs_fs):
|
||||
return False, "Write with s3fs failed"
|
||||
logging.info(" ✓ Write completed")
|
||||
|
||||
# Read with PyArrow native S3
|
||||
logging.info(" Reading with PyArrow native S3...")
|
||||
success, read_table, method = read_with_pyarrow_s3(path, pyarrow_s3)
|
||||
if not success:
|
||||
return False, f"Read with PyArrow S3 failed: {method}"
|
||||
logging.info(f" ✓ Read {read_table.num_rows:,} rows using {method}")
|
||||
|
||||
# Verify data integrity
|
||||
verify_success, verify_msg = verify_table_integrity(table, read_table)
|
||||
if not verify_success:
|
||||
return False, f"Verification failed: {verify_msg}"
|
||||
logging.info(f" ✓ {verify_msg}")
|
||||
|
||||
return True, f"s3fs→PyArrow: {method}"
|
||||
|
||||
except Exception as e: # noqa: BLE001 - Top-level exception handler for test orchestration
|
||||
logging.exception(" ✗ Test failed")
|
||||
return False, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def test_write_pyarrow_read_s3fs(
|
||||
test_name: str,
|
||||
num_rows: int,
|
||||
s3fs_fs: s3fs.S3FileSystem,
|
||||
pyarrow_s3: pafs.S3FileSystem
|
||||
) -> Tuple[bool, str]:
|
||||
"""Test: Write with PyArrow native S3, read with s3fs."""
|
||||
try:
|
||||
table = create_sample_table(num_rows)
|
||||
path = f"{BUCKET_NAME}/{TEST_DIR}/{test_name}/data.parquet"
|
||||
|
||||
# Write with PyArrow native S3
|
||||
logging.info(f" Writing {num_rows:,} rows with PyArrow native S3 to {path}...")
|
||||
if not write_with_pyarrow_s3(table, path, pyarrow_s3):
|
||||
return False, "Write with PyArrow S3 failed"
|
||||
logging.info(" ✓ Write completed")
|
||||
|
||||
# Read with s3fs
|
||||
logging.info(" Reading with s3fs...")
|
||||
success, read_table, method = read_with_s3fs(path, s3fs_fs)
|
||||
if not success:
|
||||
return False, f"Read with s3fs failed: {method}"
|
||||
logging.info(f" ✓ Read {read_table.num_rows:,} rows using {method}")
|
||||
|
||||
# Verify data integrity
|
||||
verify_success, verify_msg = verify_table_integrity(table, read_table)
|
||||
if not verify_success:
|
||||
return False, f"Verification failed: {verify_msg}"
|
||||
logging.info(f" ✓ {verify_msg}")
|
||||
|
||||
return True, f"PyArrow→s3fs: {method}"
|
||||
|
||||
except Exception as e: # noqa: BLE001 - Top-level exception handler for test orchestration
|
||||
logging.exception(" ✗ Test failed")
|
||||
return False, f"{type(e).__name__}: {e}"
|
||||
|
||||
|
||||
def cleanup_test_files(s3fs_fs: s3fs.S3FileSystem) -> None:
|
||||
"""Clean up test files from S3."""
|
||||
try:
|
||||
test_path = f"{BUCKET_NAME}/{TEST_DIR}"
|
||||
if s3fs_fs.exists(test_path):
|
||||
logging.info(f"Cleaning up test directory: {test_path}")
|
||||
s3fs_fs.rm(test_path, recursive=True)
|
||||
logging.info("✓ Test directory cleaned up")
|
||||
except Exception:
|
||||
logging.exception("Failed to cleanup test directory")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run cross-filesystem compatibility tests."""
|
||||
print("=" * 80)
|
||||
print("Cross-Filesystem Compatibility Tests for PyArrow Parquet")
|
||||
print("Testing: s3fs ↔ PyArrow Native S3 Filesystem")
|
||||
if TEST_QUICK:
|
||||
print("*** QUICK TEST MODE - Small files only ***")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
print("Configuration:")
|
||||
print(f" S3 Endpoint: {S3_ENDPOINT_URL}")
|
||||
print(f" Access Key: {S3_ACCESS_KEY}")
|
||||
print(f" Bucket: {BUCKET_NAME}")
|
||||
print(f" Test Directory: {TEST_DIR}")
|
||||
print(f" Quick Mode: {'Yes (small files only)' if TEST_QUICK else 'No (all file sizes)'}")
|
||||
print(f" PyArrow Version: {pa.__version__}")
|
||||
print()
|
||||
|
||||
# Initialize both filesystems
|
||||
s3fs_fs = init_s3fs()
|
||||
if s3fs_fs is None:
|
||||
print("Cannot proceed without s3fs connection")
|
||||
return 1
|
||||
|
||||
pyarrow_s3, _scheme, _endpoint = init_pyarrow_s3()
|
||||
if pyarrow_s3 is None:
|
||||
print("Cannot proceed without PyArrow S3 connection")
|
||||
return 1
|
||||
|
||||
print()
|
||||
|
||||
# Ensure bucket exists
|
||||
if not ensure_bucket_exists(s3fs_fs, pyarrow_s3):
|
||||
print("Cannot proceed without bucket")
|
||||
return 1
|
||||
|
||||
print()
|
||||
|
||||
results = []
|
||||
|
||||
# Test all file sizes
|
||||
for size_name, num_rows in TEST_SIZES.items():
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Testing with {size_name} files ({num_rows:,} rows)")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
# Test 1: Write with s3fs, read with PyArrow native S3
|
||||
test_name = f"{size_name}_s3fs_to_pyarrow"
|
||||
print(f"Test: Write with s3fs → Read with PyArrow native S3")
|
||||
success, message = test_write_s3fs_read_pyarrow(
|
||||
test_name, num_rows, s3fs_fs, pyarrow_s3
|
||||
)
|
||||
results.append((test_name, success, message))
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
print(f"{status}: {message}\n")
|
||||
|
||||
# Test 2: Write with PyArrow native S3, read with s3fs
|
||||
test_name = f"{size_name}_pyarrow_to_s3fs"
|
||||
print(f"Test: Write with PyArrow native S3 → Read with s3fs")
|
||||
success, message = test_write_pyarrow_read_s3fs(
|
||||
test_name, num_rows, s3fs_fs, pyarrow_s3
|
||||
)
|
||||
results.append((test_name, success, message))
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
print(f"{status}: {message}\n")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
passed = sum(1 for _, success, _ in results if success)
|
||||
total = len(results)
|
||||
print(f"\nTotal: {passed}/{total} passed\n")
|
||||
|
||||
for test_name, success, message in results:
|
||||
status = "✓" if success else "✗"
|
||||
print(f" {status} {test_name}: {message}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
if passed == total:
|
||||
print("✓ ALL CROSS-FILESYSTEM TESTS PASSED!")
|
||||
print()
|
||||
print("Conclusion: Files written with s3fs and PyArrow native S3 are")
|
||||
print("fully compatible and can be read by either filesystem implementation.")
|
||||
else:
|
||||
print(f"✗ {total - passed} test(s) failed")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Cleanup
|
||||
cleanup_test_files(s3fs_fs)
|
||||
|
||||
return 0 if passed == total else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
308
test/s3/parquet/test_implicit_directory_fix.py
Executable file
308
test/s3/parquet/test_implicit_directory_fix.py
Executable file
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify the implicit directory fix for s3fs compatibility.
|
||||
|
||||
This test verifies that:
|
||||
1. Implicit directory markers (0-byte objects with children) return 404 on HEAD
|
||||
2. s3fs correctly identifies them as directories via LIST fallback
|
||||
3. PyArrow can read datasets created with write_dataset()
|
||||
|
||||
The fix makes SeaweedFS behave like AWS S3 and improves s3fs compatibility.
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.parquet as pq
|
||||
import s3fs
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Configuration
|
||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", "http://localhost:8333")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "some_secret_key1")
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-implicit-dir")
|
||||
|
||||
def create_sample_table(num_rows: int = 1000) -> pa.Table:
|
||||
"""Create a sample PyArrow table."""
|
||||
return pa.table({
|
||||
'id': pa.array(range(num_rows), type=pa.int64()),
|
||||
'value': pa.array([f'value_{i}' for i in range(num_rows)], type=pa.string()),
|
||||
'score': pa.array([float(i) * 1.5 for i in range(num_rows)], type=pa.float64()),
|
||||
})
|
||||
|
||||
def setup_s3():
|
||||
"""Set up S3 clients."""
|
||||
# s3fs client
|
||||
fs = s3fs.S3FileSystem(
|
||||
key=S3_ACCESS_KEY,
|
||||
secret=S3_SECRET_KEY,
|
||||
client_kwargs={'endpoint_url': S3_ENDPOINT_URL},
|
||||
use_ssl=False
|
||||
)
|
||||
|
||||
# boto3 client for raw S3 operations
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=S3_ENDPOINT_URL,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name='us-east-1',
|
||||
use_ssl=False
|
||||
)
|
||||
|
||||
return fs, s3_client
|
||||
|
||||
def test_implicit_directory_head_behavior(fs, s3_client):
|
||||
"""Test that HEAD on implicit directory markers returns 404."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 1: Implicit Directory HEAD Behavior")
|
||||
logger.info("="*80)
|
||||
|
||||
test_path = f"{BUCKET_NAME}/test_implicit_dir"
|
||||
|
||||
# Clean up any existing data
|
||||
try:
|
||||
fs.rm(test_path, recursive=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Create a dataset using PyArrow (creates implicit directory)
|
||||
logger.info(f"Creating dataset at: {test_path}")
|
||||
table = create_sample_table(1000)
|
||||
pads.write_dataset(table, test_path, filesystem=fs, format='parquet')
|
||||
|
||||
# List what was created
|
||||
logger.info("\nFiles created:")
|
||||
files = fs.ls(test_path, detail=True)
|
||||
for f in files:
|
||||
logger.info(f" {f['name']} - size: {f['size']} bytes, type: {f['type']}")
|
||||
|
||||
# Test HEAD request on the directory marker (without trailing slash)
|
||||
logger.info(f"\nTesting HEAD on: {test_path}")
|
||||
try:
|
||||
response = s3_client.head_object(Bucket=BUCKET_NAME, Key='test_implicit_dir')
|
||||
logger.info(f" HEAD response: {response['ResponseMetadata']['HTTPStatusCode']}")
|
||||
logger.info(f" Content-Length: {response.get('ContentLength', 'N/A')}")
|
||||
logger.info(f" Content-Type: {response.get('ContentType', 'N/A')}")
|
||||
logger.warning(" ⚠️ Expected 404, but got 200 - fix may not be working")
|
||||
return False
|
||||
except ClientError as e:
|
||||
if e.response['Error']['Code'] == '404':
|
||||
logger.info(" ✓ HEAD returned 404 (expected - implicit directory)")
|
||||
return True
|
||||
else:
|
||||
logger.error(f" ✗ Unexpected error: {e}")
|
||||
return False
|
||||
|
||||
def test_s3fs_directory_detection(fs):
|
||||
"""Test that s3fs correctly detects the directory."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 2: s3fs Directory Detection")
|
||||
logger.info("="*80)
|
||||
|
||||
test_path = f"{BUCKET_NAME}/test_implicit_dir"
|
||||
|
||||
# Test s3fs.info()
|
||||
logger.info(f"\nTesting s3fs.info('{test_path}'):")
|
||||
try:
|
||||
info = fs.info(test_path)
|
||||
logger.info(f" Type: {info.get('type', 'N/A')}")
|
||||
logger.info(f" Size: {info.get('size', 'N/A')}")
|
||||
|
||||
if info.get('type') == 'directory':
|
||||
logger.info(" ✓ s3fs correctly identified as directory")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f" ⚠️ s3fs identified as: {info.get('type')}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
def test_s3fs_isdir(fs):
|
||||
"""Test that s3fs.isdir() works correctly."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 3: s3fs.isdir() Method")
|
||||
logger.info("="*80)
|
||||
|
||||
test_path = f"{BUCKET_NAME}/test_implicit_dir"
|
||||
|
||||
logger.info(f"\nTesting s3fs.isdir('{test_path}'):")
|
||||
try:
|
||||
is_dir = fs.isdir(test_path)
|
||||
logger.info(f" Result: {is_dir}")
|
||||
|
||||
if is_dir:
|
||||
logger.info(" ✓ s3fs.isdir() correctly returned True")
|
||||
return True
|
||||
else:
|
||||
logger.warning(" ⚠️ s3fs.isdir() returned False")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
def test_pyarrow_dataset_read(fs):
|
||||
"""Test that PyArrow can read the dataset."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 4: PyArrow Dataset Read")
|
||||
logger.info("="*80)
|
||||
|
||||
test_path = f"{BUCKET_NAME}/test_implicit_dir"
|
||||
|
||||
logger.info(f"\nReading dataset from: {test_path}")
|
||||
try:
|
||||
ds = pads.dataset(test_path, filesystem=fs, format='parquet')
|
||||
table = ds.to_table()
|
||||
logger.info(f" ✓ Successfully read {len(table)} rows")
|
||||
logger.info(f" Columns: {table.column_names}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to read dataset: {e}")
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
def test_explicit_directory_marker(fs, s3_client):
|
||||
"""Test that explicit directory markers (with trailing slash) still work."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 5: Explicit Directory Marker (with trailing slash)")
|
||||
logger.info("="*80)
|
||||
|
||||
# Create an explicit directory marker
|
||||
logger.info(f"Creating explicit directory: {BUCKET_NAME}/explicit_dir/")
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=BUCKET_NAME,
|
||||
Key='explicit_dir/',
|
||||
Body=b'',
|
||||
ContentType='httpd/unix-directory'
|
||||
)
|
||||
logger.info(" ✓ Created explicit directory marker")
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to create: {e}")
|
||||
return False
|
||||
|
||||
# Test HEAD with trailing slash
|
||||
logger.info(f"\nTesting HEAD on: {BUCKET_NAME}/explicit_dir/")
|
||||
try:
|
||||
response = s3_client.head_object(Bucket=BUCKET_NAME, Key='explicit_dir/')
|
||||
logger.info(f" ✓ HEAD returned 200 (expected for explicit directory)")
|
||||
logger.info(f" Content-Type: {response.get('ContentType', 'N/A')}")
|
||||
return True
|
||||
except ClientError as e:
|
||||
logger.error(f" ✗ HEAD failed: {e}")
|
||||
return False
|
||||
|
||||
def test_empty_file_not_directory(fs, s3_client):
|
||||
"""Test that legitimate empty files are not treated as directories."""
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST 6: Empty File (not a directory)")
|
||||
logger.info("="*80)
|
||||
|
||||
# Create an empty file with text/plain mime type
|
||||
logger.info(f"\nCreating empty file: {BUCKET_NAME}/empty.txt")
|
||||
try:
|
||||
s3_client.put_object(
|
||||
Bucket=BUCKET_NAME,
|
||||
Key='empty.txt',
|
||||
Body=b'',
|
||||
ContentType='text/plain'
|
||||
)
|
||||
logger.info(" ✓ Created empty file")
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Failed to create: {e}")
|
||||
return False
|
||||
|
||||
# Test HEAD
|
||||
logger.info(f"\nTesting HEAD on: {BUCKET_NAME}/empty.txt")
|
||||
try:
|
||||
response = s3_client.head_object(Bucket=BUCKET_NAME, Key='empty.txt')
|
||||
logger.info(f" ✓ HEAD returned 200 (expected for empty file)")
|
||||
logger.info(f" Content-Type: {response.get('ContentType', 'N/A')}")
|
||||
|
||||
# Verify s3fs doesn't think it's a directory
|
||||
info = fs.info(f"{BUCKET_NAME}/empty.txt")
|
||||
if info.get('type') == 'file':
|
||||
logger.info(" ✓ s3fs correctly identified as file")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f" ⚠️ s3fs identified as: {info.get('type')}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f" ✗ Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""Run all tests."""
|
||||
logger.info("="*80)
|
||||
logger.info("Implicit Directory Fix Test Suite")
|
||||
logger.info("="*80)
|
||||
logger.info(f"Endpoint: {S3_ENDPOINT_URL}")
|
||||
logger.info(f"Bucket: {BUCKET_NAME}")
|
||||
logger.info("="*80)
|
||||
|
||||
# Set up S3 clients
|
||||
fs, s3_client = setup_s3()
|
||||
|
||||
# Create bucket if it doesn't exist
|
||||
try:
|
||||
s3_client.create_bucket(Bucket=BUCKET_NAME)
|
||||
logger.info(f"\n✓ Created bucket: {BUCKET_NAME}")
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code in ['BucketAlreadyOwnedByYou', 'BucketAlreadyExists']:
|
||||
logger.info(f"\n✓ Bucket already exists: {BUCKET_NAME}")
|
||||
else:
|
||||
logger.error(f"\n✗ Failed to create bucket: {e}")
|
||||
return 1
|
||||
|
||||
# Run tests
|
||||
results = []
|
||||
|
||||
results.append(("Implicit Directory HEAD", test_implicit_directory_head_behavior(fs, s3_client)))
|
||||
results.append(("s3fs Directory Detection", test_s3fs_directory_detection(fs)))
|
||||
results.append(("s3fs.isdir() Method", test_s3fs_isdir(fs)))
|
||||
results.append(("PyArrow Dataset Read", test_pyarrow_dataset_read(fs)))
|
||||
results.append(("Explicit Directory Marker", test_explicit_directory_marker(fs, s3_client)))
|
||||
results.append(("Empty File Not Directory", test_empty_file_not_directory(fs, s3_client)))
|
||||
|
||||
# Print summary
|
||||
logger.info("\n" + "="*80)
|
||||
logger.info("TEST SUMMARY")
|
||||
logger.info("="*80)
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for name, result in results:
|
||||
status = "✓ PASS" if result else "✗ FAIL"
|
||||
logger.info(f"{status}: {name}")
|
||||
|
||||
logger.info("="*80)
|
||||
logger.info(f"Results: {passed}/{total} tests passed")
|
||||
logger.info("="*80)
|
||||
|
||||
if passed == total:
|
||||
logger.info("\n🎉 All tests passed! The implicit directory fix is working correctly.")
|
||||
return 0
|
||||
else:
|
||||
logger.warning(f"\n⚠️ {total - passed} test(s) failed. The fix may not be fully working.")
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
383
test/s3/parquet/test_pyarrow_native_s3.py
Executable file
383
test/s3/parquet/test_pyarrow_native_s3.py
Executable file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for PyArrow's NATIVE S3 filesystem with SeaweedFS.
|
||||
|
||||
This test uses PyArrow's built-in S3FileSystem (pyarrow.fs.S3FileSystem)
|
||||
instead of s3fs, providing a pure PyArrow solution for reading and writing
|
||||
Parquet files to S3-compatible storage.
|
||||
|
||||
Requirements:
|
||||
- pyarrow>=10.0.0
|
||||
|
||||
Environment Variables:
|
||||
S3_ENDPOINT_URL: S3 endpoint (default: localhost:8333)
|
||||
S3_ACCESS_KEY: S3 access key (default: some_access_key1)
|
||||
S3_SECRET_KEY: S3 secret key (default: some_secret_key1)
|
||||
BUCKET_NAME: S3 bucket name (default: test-parquet-bucket)
|
||||
TEST_QUICK: Run only small/quick tests (default: 0, set to 1 for quick mode)
|
||||
|
||||
Usage:
|
||||
# Run with default environment variables
|
||||
python3 test_pyarrow_native_s3.py
|
||||
|
||||
# Run with custom environment variables
|
||||
S3_ENDPOINT_URL=localhost:8333 \
|
||||
S3_ACCESS_KEY=mykey \
|
||||
S3_SECRET_KEY=mysecret \
|
||||
BUCKET_NAME=mybucket \
|
||||
python3 test_pyarrow_native_s3.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.fs as pafs
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO3 = True
|
||||
except ImportError:
|
||||
HAS_BOTO3 = False
|
||||
|
||||
from parquet_test_utils import create_sample_table
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
# Configuration from environment variables with defaults
|
||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", "localhost:8333")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "some_secret_key1")
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-parquet-bucket")
|
||||
TEST_QUICK = os.getenv("TEST_QUICK", "0") == "1"
|
||||
|
||||
# Create randomized test directory
|
||||
TEST_RUN_ID = secrets.token_hex(8)
|
||||
TEST_DIR = f"parquet-native-tests/{TEST_RUN_ID}"
|
||||
|
||||
# Test file sizes
|
||||
TEST_SIZES = {
|
||||
"small": 5,
|
||||
"large": 200_000, # This will create multiple row groups
|
||||
}
|
||||
|
||||
# Filter to only small tests if quick mode is enabled
|
||||
if TEST_QUICK:
|
||||
TEST_SIZES = {"small": TEST_SIZES["small"]}
|
||||
logging.info("Quick test mode enabled - running only small tests")
|
||||
|
||||
|
||||
def init_s3_filesystem() -> tuple[Optional[pafs.S3FileSystem], str, str]:
|
||||
"""Initialize PyArrow's native S3 filesystem.
|
||||
|
||||
Returns:
|
||||
tuple: (S3FileSystem instance, scheme, endpoint)
|
||||
"""
|
||||
try:
|
||||
logging.info("Initializing PyArrow S3FileSystem...")
|
||||
logging.info(f" Endpoint: {S3_ENDPOINT_URL}")
|
||||
logging.info(f" Bucket: {BUCKET_NAME}")
|
||||
|
||||
# Determine scheme from endpoint
|
||||
if S3_ENDPOINT_URL.startswith("http://"):
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL[7:] # Remove http://
|
||||
elif S3_ENDPOINT_URL.startswith("https://"):
|
||||
scheme = "https"
|
||||
endpoint = S3_ENDPOINT_URL[8:] # Remove https://
|
||||
else:
|
||||
# Default to http for localhost
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL
|
||||
|
||||
# Enable bucket creation and deletion for testing
|
||||
s3 = pafs.S3FileSystem(
|
||||
access_key=S3_ACCESS_KEY,
|
||||
secret_key=S3_SECRET_KEY,
|
||||
endpoint_override=endpoint,
|
||||
scheme=scheme,
|
||||
allow_bucket_creation=True,
|
||||
allow_bucket_deletion=True,
|
||||
)
|
||||
|
||||
logging.info("✓ PyArrow S3FileSystem initialized successfully\n")
|
||||
return s3, scheme, endpoint
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to initialize PyArrow S3FileSystem")
|
||||
return None, "", ""
|
||||
|
||||
|
||||
def ensure_bucket_exists_boto3(scheme: str, endpoint: str) -> bool:
|
||||
"""Ensure the test bucket exists using boto3."""
|
||||
if not HAS_BOTO3:
|
||||
logging.error("boto3 is required for bucket creation")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Create boto3 client
|
||||
endpoint_url = f"{scheme}://{endpoint}"
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name='us-east-1',
|
||||
)
|
||||
|
||||
# Check if bucket exists
|
||||
try:
|
||||
s3_client.head_bucket(Bucket=BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
return True
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == '404':
|
||||
# Bucket doesn't exist, create it
|
||||
logging.info(f"Creating bucket: {BUCKET_NAME}")
|
||||
s3_client.create_bucket(Bucket=BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
return True
|
||||
else:
|
||||
raise
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to create/check bucket")
|
||||
return False
|
||||
|
||||
|
||||
def ensure_bucket_exists(s3: pafs.S3FileSystem) -> bool:
|
||||
"""Ensure the test bucket exists using PyArrow's native S3FileSystem."""
|
||||
try:
|
||||
# Check if bucket exists by trying to list it
|
||||
try:
|
||||
file_info = s3.get_file_info(BUCKET_NAME)
|
||||
if file_info.type == pafs.FileType.Directory:
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
return True
|
||||
except OSError as e:
|
||||
# OSError typically means bucket not found or network/permission issues
|
||||
error_msg = str(e).lower()
|
||||
if "not found" in error_msg or "does not exist" in error_msg or "nosuchbucket" in error_msg:
|
||||
logging.debug(f"Bucket '{BUCKET_NAME}' not found, will attempt creation: {e}")
|
||||
else:
|
||||
# Log other OSErrors (network, auth, etc.) for debugging
|
||||
logging.debug(f"Error checking bucket '{BUCKET_NAME}', will attempt creation anyway: {type(e).__name__}: {e}")
|
||||
except Exception as e:
|
||||
# Catch any other unexpected exceptions and log them
|
||||
logging.debug(f"Unexpected error checking bucket '{BUCKET_NAME}', will attempt creation: {type(e).__name__}: {e}")
|
||||
|
||||
# Try to create the bucket
|
||||
logging.info(f"Creating bucket: {BUCKET_NAME}")
|
||||
s3.create_dir(BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
return True
|
||||
except Exception:
|
||||
logging.exception(f"✗ Failed to create/check bucket '{BUCKET_NAME}' with PyArrow")
|
||||
return False
|
||||
|
||||
|
||||
def test_write_and_read(s3: pafs.S3FileSystem, test_name: str, num_rows: int) -> tuple[bool, str]:
|
||||
"""Test writing and reading a Parquet dataset using PyArrow's native S3 filesystem."""
|
||||
try:
|
||||
table = create_sample_table(num_rows)
|
||||
|
||||
# Write using pads.write_dataset
|
||||
filename = f"{BUCKET_NAME}/{TEST_DIR}/{test_name}/data.parquet"
|
||||
logging.info(f" Writing {num_rows:,} rows to {filename}...")
|
||||
|
||||
pads.write_dataset(
|
||||
table,
|
||||
filename,
|
||||
filesystem=s3,
|
||||
format="parquet",
|
||||
)
|
||||
logging.info(" ✓ Write completed")
|
||||
|
||||
# Test Method 1: Read with pq.read_table
|
||||
logging.info(" Reading with pq.read_table...")
|
||||
table_read = pq.read_table(filename, filesystem=s3)
|
||||
if table_read.num_rows != num_rows:
|
||||
return False, f"pq.read_table: Row count mismatch (expected {num_rows}, got {table_read.num_rows})"
|
||||
|
||||
# Check schema first
|
||||
if not table_read.schema.equals(table.schema):
|
||||
return False, f"pq.read_table: Schema mismatch (expected {table.schema}, got {table_read.schema})"
|
||||
|
||||
# Sort both tables by 'id' column before comparison to handle potential row order differences
|
||||
table_sorted = table.sort_by([('id', 'ascending')])
|
||||
table_read_sorted = table_read.sort_by([('id', 'ascending')])
|
||||
|
||||
if not table_read_sorted.equals(table_sorted):
|
||||
# Provide more detailed error information
|
||||
error_details = []
|
||||
for col_name in table.column_names:
|
||||
col_original = table_sorted.column(col_name)
|
||||
col_read = table_read_sorted.column(col_name)
|
||||
if not col_original.equals(col_read):
|
||||
error_details.append(f"column '{col_name}' differs")
|
||||
return False, f"pq.read_table: Table contents mismatch ({', '.join(error_details)})"
|
||||
logging.info(f" ✓ pq.read_table: {table_read.num_rows:,} rows")
|
||||
|
||||
# Test Method 2: Read with pq.ParquetDataset
|
||||
logging.info(" Reading with pq.ParquetDataset...")
|
||||
dataset = pq.ParquetDataset(filename, filesystem=s3)
|
||||
table_dataset = dataset.read()
|
||||
if table_dataset.num_rows != num_rows:
|
||||
return False, f"pq.ParquetDataset: Row count mismatch (expected {num_rows}, got {table_dataset.num_rows})"
|
||||
|
||||
# Sort before comparison
|
||||
table_dataset_sorted = table_dataset.sort_by([('id', 'ascending')])
|
||||
if not table_dataset_sorted.equals(table_sorted):
|
||||
error_details = []
|
||||
for col_name in table.column_names:
|
||||
col_original = table_sorted.column(col_name)
|
||||
col_read = table_dataset_sorted.column(col_name)
|
||||
if not col_original.equals(col_read):
|
||||
error_details.append(f"column '{col_name}' differs")
|
||||
return False, f"pq.ParquetDataset: Table contents mismatch ({', '.join(error_details)})"
|
||||
logging.info(f" ✓ pq.ParquetDataset: {table_dataset.num_rows:,} rows")
|
||||
|
||||
# Test Method 3: Read with pads.dataset
|
||||
logging.info(" Reading with pads.dataset...")
|
||||
dataset_pads = pads.dataset(filename, filesystem=s3)
|
||||
table_pads = dataset_pads.to_table()
|
||||
if table_pads.num_rows != num_rows:
|
||||
return False, f"pads.dataset: Row count mismatch (expected {num_rows}, got {table_pads.num_rows})"
|
||||
|
||||
# Sort before comparison
|
||||
table_pads_sorted = table_pads.sort_by([('id', 'ascending')])
|
||||
if not table_pads_sorted.equals(table_sorted):
|
||||
error_details = []
|
||||
for col_name in table.column_names:
|
||||
col_original = table_sorted.column(col_name)
|
||||
col_read = table_pads_sorted.column(col_name)
|
||||
if not col_original.equals(col_read):
|
||||
error_details.append(f"column '{col_name}' differs")
|
||||
return False, f"pads.dataset: Table contents mismatch ({', '.join(error_details)})"
|
||||
logging.info(f" ✓ pads.dataset: {table_pads.num_rows:,} rows")
|
||||
|
||||
return True, "All read methods passed"
|
||||
|
||||
except Exception as exc:
|
||||
logging.exception(" ✗ Test failed")
|
||||
return False, f"{type(exc).__name__}: {exc}"
|
||||
|
||||
|
||||
def cleanup_test_files(s3: pafs.S3FileSystem) -> None:
|
||||
"""Clean up test files from S3.
|
||||
|
||||
Note: We cannot use s3.delete_dir() directly because SeaweedFS uses implicit
|
||||
directories (path prefixes without physical directory objects). PyArrow's
|
||||
delete_dir() attempts to delete the directory marker itself, which fails with
|
||||
"INTERNAL_FAILURE" on SeaweedFS. Instead, we list and delete files individually,
|
||||
letting implicit directories disappear automatically.
|
||||
"""
|
||||
try:
|
||||
test_path = f"{BUCKET_NAME}/{TEST_DIR}"
|
||||
logging.info(f"Cleaning up test directory: {test_path}")
|
||||
|
||||
# List and delete files individually to handle implicit directories
|
||||
try:
|
||||
file_selector = pafs.FileSelector(test_path, recursive=True)
|
||||
files = s3.get_file_info(file_selector)
|
||||
|
||||
# Delete files first (not directories)
|
||||
for file_info in files:
|
||||
if file_info.type == pafs.FileType.File:
|
||||
s3.delete_file(file_info.path)
|
||||
logging.debug(f" Deleted file: {file_info.path}")
|
||||
|
||||
logging.info("✓ Test directory cleaned up")
|
||||
except OSError as e:
|
||||
# Handle the case where the path doesn't exist or is inaccessible
|
||||
if "does not exist" in str(e).lower() or "not found" in str(e).lower():
|
||||
logging.info("✓ Test directory already clean or doesn't exist")
|
||||
else:
|
||||
raise
|
||||
except Exception:
|
||||
logging.exception("Failed to cleanup test directory")
|
||||
|
||||
|
||||
def main():
|
||||
"""Run all tests with PyArrow's native S3 filesystem."""
|
||||
print("=" * 80)
|
||||
print("PyArrow Native S3 Filesystem Tests for SeaweedFS")
|
||||
print("Testing Parquet Files with Multiple Row Groups")
|
||||
if TEST_QUICK:
|
||||
print("*** QUICK TEST MODE - Small files only ***")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
print("Configuration:")
|
||||
print(f" S3 Endpoint: {S3_ENDPOINT_URL}")
|
||||
print(f" Access Key: {S3_ACCESS_KEY}")
|
||||
print(f" Bucket: {BUCKET_NAME}")
|
||||
print(f" Test Directory: {TEST_DIR}")
|
||||
print(f" Quick Mode: {'Yes (small files only)' if TEST_QUICK else 'No (all file sizes)'}")
|
||||
print(f" PyArrow Version: {pa.__version__}")
|
||||
print()
|
||||
|
||||
# Initialize S3 filesystem
|
||||
s3, scheme, endpoint = init_s3_filesystem()
|
||||
if s3 is None:
|
||||
print("Cannot proceed without S3 connection")
|
||||
return 1
|
||||
|
||||
# Ensure bucket exists - try PyArrow first, fall back to boto3
|
||||
bucket_created = ensure_bucket_exists(s3)
|
||||
if not bucket_created:
|
||||
logging.info("Trying to create bucket with boto3...")
|
||||
bucket_created = ensure_bucket_exists_boto3(scheme, endpoint)
|
||||
|
||||
if not bucket_created:
|
||||
print("Cannot proceed without bucket")
|
||||
return 1
|
||||
|
||||
results = []
|
||||
|
||||
# Test all file sizes
|
||||
for size_name, num_rows in TEST_SIZES.items():
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Testing with {size_name} files ({num_rows:,} rows)")
|
||||
print(f"{'='*80}\n")
|
||||
|
||||
test_name = f"{size_name}_test"
|
||||
success, message = test_write_and_read(s3, test_name, num_rows)
|
||||
results.append((test_name, success, message))
|
||||
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
print(f"\n{status}: {message}\n")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
passed = sum(1 for _, success, _ in results if success)
|
||||
total = len(results)
|
||||
print(f"\nTotal: {passed}/{total} passed\n")
|
||||
|
||||
for test_name, success, message in results:
|
||||
status = "✓" if success else "✗"
|
||||
print(f" {status} {test_name}: {message}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
if passed == total:
|
||||
print("✓ ALL TESTS PASSED!")
|
||||
else:
|
||||
print(f"✗ {total - passed} test(s) failed")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
# Cleanup
|
||||
cleanup_test_files(s3)
|
||||
|
||||
return 0 if passed == total else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
254
test/s3/parquet/test_sse_s3_compatibility.py
Executable file
254
test/s3/parquet/test_sse_s3_compatibility.py
Executable file
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for SSE-S3 compatibility with PyArrow native S3 filesystem.
|
||||
|
||||
This test specifically targets the SSE-S3 multipart upload bug where
|
||||
SeaweedFS panics with "bad IV length" when reading multipart uploads
|
||||
that were encrypted with bucket-default SSE-S3.
|
||||
|
||||
Requirements:
|
||||
- pyarrow>=10.0.0
|
||||
- boto3>=1.28.0
|
||||
|
||||
Environment Variables:
|
||||
S3_ENDPOINT_URL: S3 endpoint (default: localhost:8333)
|
||||
S3_ACCESS_KEY: S3 access key (default: some_access_key1)
|
||||
S3_SECRET_KEY: S3 secret key (default: some_secret_key1)
|
||||
BUCKET_NAME: S3 bucket name (default: test-parquet-bucket)
|
||||
|
||||
Usage:
|
||||
# Start SeaweedFS with SSE-S3 enabled
|
||||
make start-seaweedfs-ci ENABLE_SSE_S3=true
|
||||
|
||||
# Run the test
|
||||
python3 test_sse_s3_compatibility.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import secrets
|
||||
import sys
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset as pads
|
||||
import pyarrow.fs as pafs
|
||||
import pyarrow.parquet as pq
|
||||
|
||||
try:
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
HAS_BOTO3 = True
|
||||
except ImportError:
|
||||
HAS_BOTO3 = False
|
||||
logging.exception("boto3 is required for this test")
|
||||
sys.exit(1)
|
||||
|
||||
from parquet_test_utils import create_sample_table
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
||||
# Configuration
|
||||
S3_ENDPOINT_URL = os.environ.get("S3_ENDPOINT_URL", "localhost:8333")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY", "some_access_key1")
|
||||
S3_SECRET_KEY = os.environ.get("S3_SECRET_KEY", "some_secret_key1")
|
||||
BUCKET_NAME = os.getenv("BUCKET_NAME", "test-parquet-bucket")
|
||||
|
||||
TEST_RUN_ID = secrets.token_hex(8)
|
||||
TEST_DIR = f"sse-s3-tests/{TEST_RUN_ID}"
|
||||
|
||||
# Test sizes designed to trigger multipart uploads
|
||||
# PyArrow typically uses 5MB chunks, so these sizes should trigger multipart
|
||||
TEST_SIZES = {
|
||||
"tiny": 10, # Single part
|
||||
"small": 1_000, # Single part
|
||||
"medium": 50_000, # Single part (~1.5MB)
|
||||
"large": 200_000, # Multiple parts (~6MB)
|
||||
"very_large": 500_000, # Multiple parts (~15MB)
|
||||
}
|
||||
|
||||
|
||||
def init_s3_filesystem() -> tuple[Optional[pafs.S3FileSystem], str, str]:
|
||||
"""Initialize PyArrow's native S3 filesystem."""
|
||||
try:
|
||||
logging.info("Initializing PyArrow S3FileSystem...")
|
||||
|
||||
# Determine scheme from endpoint
|
||||
if S3_ENDPOINT_URL.startswith("http://"):
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL[7:]
|
||||
elif S3_ENDPOINT_URL.startswith("https://"):
|
||||
scheme = "https"
|
||||
endpoint = S3_ENDPOINT_URL[8:]
|
||||
else:
|
||||
scheme = "http"
|
||||
endpoint = S3_ENDPOINT_URL
|
||||
|
||||
s3 = pafs.S3FileSystem(
|
||||
access_key=S3_ACCESS_KEY,
|
||||
secret_key=S3_SECRET_KEY,
|
||||
endpoint_override=endpoint,
|
||||
scheme=scheme,
|
||||
allow_bucket_creation=True,
|
||||
allow_bucket_deletion=True,
|
||||
)
|
||||
|
||||
logging.info("✓ PyArrow S3FileSystem initialized\n")
|
||||
return s3, scheme, endpoint
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to initialize PyArrow S3FileSystem")
|
||||
return None, "", ""
|
||||
|
||||
|
||||
def ensure_bucket_exists(scheme: str, endpoint: str) -> bool:
|
||||
"""Ensure the test bucket exists using boto3."""
|
||||
try:
|
||||
endpoint_url = f"{scheme}://{endpoint}"
|
||||
s3_client = boto3.client(
|
||||
's3',
|
||||
endpoint_url=endpoint_url,
|
||||
aws_access_key_id=S3_ACCESS_KEY,
|
||||
aws_secret_access_key=S3_SECRET_KEY,
|
||||
region_name='us-east-1',
|
||||
)
|
||||
|
||||
try:
|
||||
s3_client.head_bucket(Bucket=BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket exists: {BUCKET_NAME}")
|
||||
except ClientError as e:
|
||||
error_code = e.response['Error']['Code']
|
||||
if error_code == '404':
|
||||
logging.info(f"Creating bucket: {BUCKET_NAME}")
|
||||
s3_client.create_bucket(Bucket=BUCKET_NAME)
|
||||
logging.info(f"✓ Bucket created: {BUCKET_NAME}")
|
||||
else:
|
||||
logging.exception("✗ Failed to access bucket")
|
||||
return False
|
||||
|
||||
# Note: SeaweedFS doesn't support GetBucketEncryption API
|
||||
# so we can't verify if SSE-S3 is enabled via API
|
||||
# We assume it's configured correctly in the s3.json config file
|
||||
logging.info("✓ Assuming SSE-S3 is configured in s3.json")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
logging.exception("✗ Failed to check bucket")
|
||||
return False
|
||||
|
||||
|
||||
def test_write_read_with_sse(
|
||||
s3: pafs.S3FileSystem,
|
||||
test_name: str,
|
||||
num_rows: int
|
||||
) -> tuple[bool, str, int]:
|
||||
"""Test writing and reading with SSE-S3 encryption."""
|
||||
try:
|
||||
table = create_sample_table(num_rows)
|
||||
filename = f"{BUCKET_NAME}/{TEST_DIR}/{test_name}/data.parquet"
|
||||
|
||||
logging.info(f" Writing {num_rows:,} rows...")
|
||||
pads.write_dataset(
|
||||
table,
|
||||
filename,
|
||||
filesystem=s3,
|
||||
format="parquet",
|
||||
)
|
||||
|
||||
logging.info(" Reading back...")
|
||||
table_read = pq.read_table(filename, filesystem=s3)
|
||||
|
||||
if table_read.num_rows != num_rows:
|
||||
return False, f"Row count mismatch: {table_read.num_rows} != {num_rows}", 0
|
||||
|
||||
return True, "Success", table_read.num_rows
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"{type(e).__name__}: {e!s}"
|
||||
logging.exception(" ✗ Failed")
|
||||
return False, error_msg, 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Run SSE-S3 compatibility tests."""
|
||||
print("=" * 80)
|
||||
print("SSE-S3 Compatibility Tests for PyArrow Native S3")
|
||||
print("Testing Multipart Upload Encryption")
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
print("Configuration:")
|
||||
print(f" S3 Endpoint: {S3_ENDPOINT_URL}")
|
||||
print(f" Bucket: {BUCKET_NAME}")
|
||||
print(f" Test Directory: {TEST_DIR}")
|
||||
print(f" PyArrow Version: {pa.__version__}")
|
||||
print()
|
||||
|
||||
# Initialize
|
||||
s3, scheme, endpoint = init_s3_filesystem()
|
||||
if s3 is None:
|
||||
print("Cannot proceed without S3 connection")
|
||||
return 1
|
||||
|
||||
# Check bucket and SSE-S3
|
||||
if not ensure_bucket_exists(scheme, endpoint):
|
||||
print("\n⚠ WARNING: Failed to access or create the test bucket!")
|
||||
print("This test requires a reachable bucket with SSE-S3 enabled.")
|
||||
print("Please ensure SeaweedFS is running with: make start-seaweedfs-ci ENABLE_SSE_S3=true")
|
||||
return 1
|
||||
|
||||
print()
|
||||
results = []
|
||||
|
||||
# Test all sizes
|
||||
for size_name, num_rows in TEST_SIZES.items():
|
||||
print(f"\n{'='*80}")
|
||||
print(f"Testing {size_name} dataset ({num_rows:,} rows)")
|
||||
print(f"{'='*80}")
|
||||
|
||||
success, message, rows_read = test_write_read_with_sse(
|
||||
s3, size_name, num_rows
|
||||
)
|
||||
results.append((size_name, num_rows, success, message, rows_read))
|
||||
|
||||
if success:
|
||||
print(f" ✓ SUCCESS: Read {rows_read:,} rows")
|
||||
else:
|
||||
print(f" ✗ FAILED: {message}")
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 80)
|
||||
print("SUMMARY")
|
||||
print("=" * 80)
|
||||
|
||||
passed = sum(1 for _, _, success, _, _ in results if success)
|
||||
total = len(results)
|
||||
print(f"\nTotal: {passed}/{total} tests passed\n")
|
||||
|
||||
print(f"{'Size':<15} {'Rows':>10} {'Status':<10} {'Rows Read':>10} {'Message':<40}")
|
||||
print("-" * 90)
|
||||
for size_name, num_rows, success, message, rows_read in results:
|
||||
status = "✓ PASS" if success else "✗ FAIL"
|
||||
rows_str = f"{rows_read:,}" if success else "N/A"
|
||||
print(f"{size_name:<15} {num_rows:>10,} {status:<10} {rows_str:>10} {message[:40]}")
|
||||
|
||||
print("\n" + "=" * 80)
|
||||
if passed == total:
|
||||
print("✓ ALL TESTS PASSED WITH SSE-S3!")
|
||||
print("\nThis means:")
|
||||
print(" - SSE-S3 encryption is working correctly")
|
||||
print(" - PyArrow native S3 filesystem is compatible")
|
||||
print(" - Multipart uploads are handled properly")
|
||||
else:
|
||||
print(f"✗ {total - passed} test(s) failed")
|
||||
print("\nPossible issues:")
|
||||
print(" - SSE-S3 multipart upload bug with empty IV")
|
||||
print(" - Encryption/decryption mismatch")
|
||||
print(" - File corruption during upload")
|
||||
|
||||
print("=" * 80 + "\n")
|
||||
|
||||
return 0 if passed == total else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
||||
Reference in New Issue
Block a user