561 lines
14 KiB
Bash
Executable File
561 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# RuVector-Postgres Integration Test Runner
|
|
# Builds Docker environment, runs comprehensive integration tests, and reports results
|
|
|
|
set -e # Exit on error
|
|
set -u # Exit on undefined variable
|
|
set -o pipefail # Exit on pipe failure
|
|
|
|
# Colors for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Configuration
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
|
|
COMPOSE_FILE="${SCRIPT_DIR}/docker-compose.integration.yml"
|
|
TEST_RESULTS_DIR="${PROJECT_ROOT}/test-results/integration"
|
|
POSTGRES_CONTAINER="ruvector-postgres-integration"
|
|
TEST_RUNNER_CONTAINER="ruvector-integration-runner"
|
|
|
|
# Default settings
|
|
PG_VERSION="${PG_VERSION:-17}"
|
|
RUST_LOG="${RUST_LOG:-info}"
|
|
TEST_TIMEOUT="${TEST_TIMEOUT:-600}"
|
|
KEEP_RUNNING="${KEEP_RUNNING:-false}"
|
|
|
|
# Test categories
|
|
declare -a TEST_CATEGORIES=(
|
|
"pgvector_compat"
|
|
"integrity_tests"
|
|
"hybrid_search_tests"
|
|
"tenancy_tests"
|
|
"healing_tests"
|
|
"perf_tests"
|
|
)
|
|
|
|
# Functions
|
|
log_info() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[ERROR]${NC} $1"
|
|
}
|
|
|
|
log_section() {
|
|
echo -e "\n${CYAN}=== $1 ===${NC}\n"
|
|
}
|
|
|
|
cleanup() {
|
|
if [ "${KEEP_RUNNING}" != "true" ]; then
|
|
log_info "Cleaning up Docker containers..."
|
|
docker-compose -f "${COMPOSE_FILE}" down -v 2>/dev/null || true
|
|
docker rm -f "${POSTGRES_CONTAINER}" 2>/dev/null || true
|
|
docker rm -f "${TEST_RUNNER_CONTAINER}" 2>/dev/null || true
|
|
else
|
|
log_info "Keeping containers running for debugging"
|
|
fi
|
|
}
|
|
|
|
wait_for_postgres() {
|
|
log_info "Waiting for PostgreSQL to be ready..."
|
|
local max_attempts=60
|
|
local attempt=1
|
|
|
|
while [ ${attempt} -le ${max_attempts} ]; do
|
|
if docker exec "${POSTGRES_CONTAINER}" pg_isready -U ruvector -d ruvector_test &>/dev/null; then
|
|
log_success "PostgreSQL is ready!"
|
|
return 0
|
|
fi
|
|
|
|
echo -n "."
|
|
sleep 1
|
|
attempt=$((attempt + 1))
|
|
done
|
|
|
|
log_error "PostgreSQL failed to start after ${max_attempts} seconds"
|
|
docker logs "${POSTGRES_CONTAINER}" 2>&1 | tail -50
|
|
return 1
|
|
}
|
|
|
|
verify_extension() {
|
|
log_info "Verifying RuVector extension..."
|
|
|
|
docker exec "${POSTGRES_CONTAINER}" psql -U ruvector -d ruvector_test -c "
|
|
SELECT ruvector_version();
|
|
SELECT ruvector_simd_info();
|
|
" || {
|
|
log_error "Failed to verify RuVector extension"
|
|
return 1
|
|
}
|
|
|
|
log_success "RuVector extension verified"
|
|
}
|
|
|
|
build_extension() {
|
|
log_section "Building RuVector Extension"
|
|
|
|
cd "${PROJECT_ROOT}"
|
|
|
|
DOCKER_BUILDKIT=1 docker build \
|
|
-f crates/ruvector-postgres/docker/Dockerfile \
|
|
-t "ruvector-postgres:pg${PG_VERSION}-test" \
|
|
--build-arg PG_VERSION="${PG_VERSION}" \
|
|
--progress=plain \
|
|
. || {
|
|
log_error "Failed to build extension"
|
|
return 1
|
|
}
|
|
|
|
log_success "Extension built successfully"
|
|
}
|
|
|
|
start_postgres() {
|
|
log_section "Starting PostgreSQL Container"
|
|
|
|
docker run -d \
|
|
--name "${POSTGRES_CONTAINER}" \
|
|
-e POSTGRES_USER=ruvector \
|
|
-e POSTGRES_PASSWORD=ruvector \
|
|
-e POSTGRES_DB=ruvector_test \
|
|
-p 5433:5432 \
|
|
--health-cmd="pg_isready -U ruvector -d ruvector_test" \
|
|
--health-interval=5s \
|
|
--health-timeout=5s \
|
|
--health-retries=10 \
|
|
"ruvector-postgres:pg${PG_VERSION}-test"
|
|
|
|
wait_for_postgres
|
|
verify_extension
|
|
}
|
|
|
|
setup_test_schema() {
|
|
log_info "Setting up test schema..."
|
|
|
|
docker exec "${POSTGRES_CONTAINER}" psql -U ruvector -d ruvector_test << 'EOF'
|
|
-- Create test schemas for each category
|
|
CREATE SCHEMA IF NOT EXISTS test_pgvector;
|
|
CREATE SCHEMA IF NOT EXISTS test_integrity;
|
|
CREATE SCHEMA IF NOT EXISTS test_hybrid;
|
|
CREATE SCHEMA IF NOT EXISTS test_tenancy;
|
|
CREATE SCHEMA IF NOT EXISTS test_healing;
|
|
CREATE SCHEMA IF NOT EXISTS test_perf;
|
|
|
|
-- Grant permissions
|
|
GRANT ALL ON ALL SCHEMAS IN DATABASE ruvector_test TO ruvector;
|
|
|
|
-- Create test tables
|
|
CREATE TABLE IF NOT EXISTS test_pgvector.vectors (
|
|
id SERIAL PRIMARY KEY,
|
|
embedding vector(128),
|
|
metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS test_perf.benchmark_vectors (
|
|
id SERIAL PRIMARY KEY,
|
|
embedding vector(128),
|
|
metadata JSONB,
|
|
created_at TIMESTAMP DEFAULT NOW()
|
|
);
|
|
|
|
-- Create indexes
|
|
CREATE INDEX IF NOT EXISTS test_pgvector_hnsw ON test_pgvector.vectors
|
|
USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
|
|
|
|
CREATE INDEX IF NOT EXISTS test_perf_hnsw ON test_perf.benchmark_vectors
|
|
USING hnsw (embedding vector_l2_ops) WITH (m = 16, ef_construction = 64);
|
|
|
|
-- Insert test data
|
|
INSERT INTO test_pgvector.vectors (embedding, metadata)
|
|
SELECT
|
|
(SELECT array_agg(random()::real) FROM generate_series(1, 128))::vector,
|
|
jsonb_build_object('idx', i)
|
|
FROM generate_series(1, 1000) i;
|
|
|
|
ANALYZE test_pgvector.vectors;
|
|
|
|
\echo 'Test schema setup complete'
|
|
EOF
|
|
|
|
log_success "Test schema created"
|
|
}
|
|
|
|
run_unit_tests() {
|
|
log_section "Running Unit Tests"
|
|
|
|
cd "${PROJECT_ROOT}/crates/ruvector-postgres"
|
|
|
|
# Run tests in release mode for performance
|
|
cargo test \
|
|
--release \
|
|
--features "pg${PG_VERSION},graph-complete" \
|
|
--lib \
|
|
-- \
|
|
--test-threads=4 \
|
|
2>&1 | tee "${TEST_RESULTS_DIR}/unit_tests.log"
|
|
|
|
local exit_code=$?
|
|
|
|
if [ ${exit_code} -eq 0 ]; then
|
|
log_success "Unit tests passed"
|
|
else
|
|
log_error "Unit tests failed"
|
|
fi
|
|
|
|
return ${exit_code}
|
|
}
|
|
|
|
run_integration_tests() {
|
|
log_section "Running Integration Tests"
|
|
|
|
cd "${PROJECT_ROOT}/crates/ruvector-postgres"
|
|
|
|
export DATABASE_URL="postgresql://ruvector:ruvector@localhost:5433/ruvector_test"
|
|
export RUST_LOG="${RUST_LOG}"
|
|
export RUST_BACKTRACE=1
|
|
|
|
local failed_categories=()
|
|
|
|
for category in "${TEST_CATEGORIES[@]}"; do
|
|
log_info "Running ${category} tests..."
|
|
|
|
cargo test \
|
|
--release \
|
|
--features "pg${PG_VERSION},graph-complete" \
|
|
--test integration \
|
|
"${category}" \
|
|
-- \
|
|
--test-threads=1 \
|
|
2>&1 | tee "${TEST_RESULTS_DIR}/${category}.log"
|
|
|
|
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
|
log_error "${category} tests failed"
|
|
failed_categories+=("${category}")
|
|
else
|
|
log_success "${category} tests passed"
|
|
fi
|
|
done
|
|
|
|
if [ ${#failed_categories[@]} -gt 0 ]; then
|
|
log_error "Failed test categories: ${failed_categories[*]}"
|
|
return 1
|
|
fi
|
|
|
|
log_success "All integration tests passed"
|
|
return 0
|
|
}
|
|
|
|
run_sql_tests() {
|
|
log_section "Running SQL Integration Tests"
|
|
|
|
local test_sql_dir="${SCRIPT_DIR}/test_sql"
|
|
mkdir -p "${test_sql_dir}"
|
|
|
|
# Generate and run SQL tests
|
|
cat > "${test_sql_dir}/pgvector_compat.sql" << 'EOF'
|
|
-- pgvector compatibility tests
|
|
\echo 'Testing pgvector compatibility...'
|
|
|
|
-- Test vector type
|
|
SELECT '[1,2,3]'::vector AS test_vector;
|
|
|
|
-- Test operators
|
|
SELECT '[1,2,3]'::vector <-> '[4,5,6]'::vector AS l2_distance;
|
|
SELECT '[1,2,3]'::vector <=> '[4,5,6]'::vector AS cosine_distance;
|
|
SELECT '[1,2,3]'::vector <#> '[4,5,6]'::vector AS inner_product;
|
|
|
|
-- Test nearest neighbor search
|
|
SELECT id, embedding <-> '[0.5, 0.5, 0.5]'::vector(3) AS distance
|
|
FROM (VALUES (1, '[1,2,3]'::vector), (2, '[2,3,4]'::vector)) AS t(id, embedding)
|
|
ORDER BY embedding <-> '[0.5, 0.5, 0.5]'::vector(3)
|
|
LIMIT 2;
|
|
|
|
\echo 'pgvector compatibility tests passed!'
|
|
EOF
|
|
|
|
docker exec "${POSTGRES_CONTAINER}" psql -U ruvector -d ruvector_test \
|
|
-f /dev/stdin < "${test_sql_dir}/pgvector_compat.sql" \
|
|
2>&1 | tee "${TEST_RESULTS_DIR}/sql_tests.log"
|
|
|
|
log_success "SQL integration tests completed"
|
|
}
|
|
|
|
run_performance_benchmark() {
|
|
log_section "Running Performance Benchmark"
|
|
|
|
docker exec "${POSTGRES_CONTAINER}" psql -U ruvector -d ruvector_test << 'EOF'
|
|
\timing on
|
|
|
|
-- Insert benchmark
|
|
\echo 'Insert benchmark (1000 vectors)...'
|
|
INSERT INTO test_perf.benchmark_vectors (embedding, metadata)
|
|
SELECT
|
|
(SELECT array_agg(random()::real) FROM generate_series(1, 128))::vector,
|
|
jsonb_build_object('idx', i)
|
|
FROM generate_series(1, 1000) i;
|
|
|
|
-- Query benchmark
|
|
\echo 'Query benchmark (100 queries)...'
|
|
DO $$
|
|
DECLARE
|
|
query_vec vector;
|
|
start_time timestamp;
|
|
total_time interval := '0'::interval;
|
|
i integer;
|
|
BEGIN
|
|
FOR i IN 1..100 LOOP
|
|
query_vec := (SELECT array_agg(random()::real) FROM generate_series(1, 128))::vector;
|
|
start_time := clock_timestamp();
|
|
|
|
PERFORM id FROM test_perf.benchmark_vectors
|
|
ORDER BY embedding <-> query_vec
|
|
LIMIT 10;
|
|
|
|
total_time := total_time + (clock_timestamp() - start_time);
|
|
END LOOP;
|
|
|
|
RAISE NOTICE 'Total time for 100 queries: %', total_time;
|
|
RAISE NOTICE 'Average query time: %', total_time / 100;
|
|
END;
|
|
$$;
|
|
|
|
\echo 'Performance benchmark complete!'
|
|
EOF
|
|
|
|
log_success "Performance benchmark completed"
|
|
}
|
|
|
|
generate_report() {
|
|
log_section "Generating Test Report"
|
|
|
|
local report_file="${TEST_RESULTS_DIR}/report.md"
|
|
|
|
cat > "${report_file}" << EOF
|
|
# RuVector Postgres Integration Test Report
|
|
|
|
Generated: $(date -Iseconds)
|
|
PostgreSQL Version: ${PG_VERSION}
|
|
|
|
## Test Results Summary
|
|
|
|
| Category | Status |
|
|
|----------|--------|
|
|
EOF
|
|
|
|
for category in "${TEST_CATEGORIES[@]}"; do
|
|
local status="PASS"
|
|
if grep -q "FAILED" "${TEST_RESULTS_DIR}/${category}.log" 2>/dev/null; then
|
|
status="FAIL"
|
|
fi
|
|
echo "| ${category} | ${status} |" >> "${report_file}"
|
|
done
|
|
|
|
cat >> "${report_file}" << EOF
|
|
|
|
## Test Categories
|
|
|
|
### pgvector Compatibility
|
|
- Vector type creation and operators
|
|
- HNSW and IVFFlat index creation
|
|
- Basic CRUD operations
|
|
|
|
### Integrity System
|
|
- Contracted graph construction
|
|
- Mincut computation
|
|
- State transitions
|
|
|
|
### Hybrid Search
|
|
- BM25 scoring accuracy
|
|
- RRF fusion
|
|
- Linear fusion
|
|
|
|
### Multi-Tenancy
|
|
- Schema isolation
|
|
- RLS policies
|
|
- Quota enforcement
|
|
|
|
### Self-Healing
|
|
- Problem detection
|
|
- Remediation strategies
|
|
- Recovery from failures
|
|
|
|
### Performance
|
|
- Insert throughput
|
|
- Query latency (p50, p95, p99)
|
|
- SIMD acceleration
|
|
- Concurrent scaling
|
|
|
|
## Logs
|
|
|
|
Test logs are available in: ${TEST_RESULTS_DIR}/
|
|
|
|
## Environment
|
|
|
|
- Docker: $(docker --version)
|
|
- Rust: $(rustc --version)
|
|
- PostgreSQL: ${PG_VERSION}
|
|
EOF
|
|
|
|
log_success "Report generated: ${report_file}"
|
|
}
|
|
|
|
show_usage() {
|
|
cat << EOF
|
|
RuVector-Postgres Integration Test Runner
|
|
|
|
Usage: $0 [OPTIONS]
|
|
|
|
Options:
|
|
-b, --build-only Build Docker image only
|
|
-t, --tests-only Run tests only (skip build)
|
|
-c, --category CAT Run specific test category
|
|
-s, --sql-only Run SQL tests only
|
|
-p, --perf Run performance benchmarks
|
|
-k, --keep-running Keep containers after tests
|
|
--pg-version VER PostgreSQL version (default: 17)
|
|
-h, --help Show this help
|
|
|
|
Test Categories:
|
|
pgvector_compat pgvector SQL compatibility
|
|
integrity_tests Integrity system tests
|
|
hybrid_search_tests Hybrid search tests
|
|
tenancy_tests Multi-tenancy tests
|
|
healing_tests Self-healing tests
|
|
perf_tests Performance tests
|
|
|
|
Examples:
|
|
# Run all tests
|
|
$0
|
|
|
|
# Run specific category
|
|
$0 -c pgvector_compat
|
|
|
|
# Run performance benchmark only
|
|
$0 -p
|
|
|
|
# Keep containers for debugging
|
|
$0 -k
|
|
EOF
|
|
}
|
|
|
|
main() {
|
|
local build_only=false
|
|
local tests_only=false
|
|
local sql_only=false
|
|
local perf_only=false
|
|
local specific_category=""
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-b|--build-only)
|
|
build_only=true
|
|
shift
|
|
;;
|
|
-t|--tests-only)
|
|
tests_only=true
|
|
shift
|
|
;;
|
|
-c|--category)
|
|
specific_category="$2"
|
|
shift 2
|
|
;;
|
|
-s|--sql-only)
|
|
sql_only=true
|
|
shift
|
|
;;
|
|
-p|--perf)
|
|
perf_only=true
|
|
shift
|
|
;;
|
|
-k|--keep-running)
|
|
KEEP_RUNNING=true
|
|
shift
|
|
;;
|
|
--pg-version)
|
|
PG_VERSION="$2"
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
show_usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
log_error "Unknown option: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Setup trap for cleanup
|
|
trap cleanup EXIT
|
|
|
|
# Create results directory
|
|
mkdir -p "${TEST_RESULTS_DIR}"
|
|
|
|
log_section "RuVector Integration Test Suite"
|
|
log_info "PostgreSQL Version: ${PG_VERSION}"
|
|
log_info "Results Directory: ${TEST_RESULTS_DIR}"
|
|
|
|
# Build phase
|
|
if [ "${tests_only}" != "true" ]; then
|
|
build_extension
|
|
fi
|
|
|
|
if [ "${build_only}" == "true" ]; then
|
|
log_success "Build complete!"
|
|
exit 0
|
|
fi
|
|
|
|
# Start PostgreSQL
|
|
start_postgres
|
|
setup_test_schema
|
|
|
|
# Run tests
|
|
local test_result=0
|
|
|
|
if [ "${sql_only}" == "true" ]; then
|
|
run_sql_tests || test_result=$?
|
|
elif [ "${perf_only}" == "true" ]; then
|
|
run_performance_benchmark || test_result=$?
|
|
elif [ -n "${specific_category}" ]; then
|
|
TEST_CATEGORIES=("${specific_category}")
|
|
run_integration_tests || test_result=$?
|
|
else
|
|
# Run all tests
|
|
run_unit_tests || test_result=$?
|
|
run_integration_tests || test_result=$?
|
|
run_sql_tests || test_result=$?
|
|
run_performance_benchmark || test_result=$?
|
|
fi
|
|
|
|
# Generate report
|
|
generate_report
|
|
|
|
if [ ${test_result} -eq 0 ]; then
|
|
log_success "All tests completed successfully!"
|
|
else
|
|
log_error "Some tests failed. Check logs in ${TEST_RESULTS_DIR}/"
|
|
fi
|
|
|
|
exit ${test_result}
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|