diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3e8296a..e2f4a04 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,15 +48,15 @@ jobs: MYSQL_PASSWORD: userpassword ports: - "3306:3306" - # volumes: - # - mysql_data:/var/lib/mysql + # Required for MySQL 8.0+ compatibility with Go MySQL drivers + command: --default-authentication-plugin=mysql_native_password steps: - name: Install Go if: success() uses: actions/setup-go@v4 with: go-version: ${{ matrix.go-version }} - - name: Install Dependenies + - name: Install Dependencies run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates postgresql-client - name: Checkout code @@ -64,21 +64,23 @@ jobs: - name: Add postgres tables run: PGPASSWORD=testPass psql -U testUser -h localhost -p 5432 -d testDB -c 'CREATE TABLE entries (id BIGSERIAL PRIMARY KEY, amount REAL, user_id VARCHAR(6), entry_date DATE, timestamp TIMESTAMP)'; - + - name: Add mysql tables run: mysql -h localhost -P 3306 -u root -p mydb --protocol=TCP --password=rootpassword -e 'CREATE TABLE entries (id BIGINT PRIMARY KEY, amount REAL, user_id VARCHAR(6), entry_date DATE, timestamp TIMESTAMP)' - name: Build binary run: CGO_ENABLED=0 go build -o server.bin -ldflags="-s -w -X 'main.buildString=${BUILDSTR}'" ./cmd/*.go - - - name: Run binary server - run: ./server.bin --config config.test_pg.toml --sql-directory=sql/pg & - - - name: Run tests - run: sleep 5 && go test ./client -v -covermode=count - - - name: Run binary server - run: ./server.bin --config config.test_mysql.toml --sql-directory=sql/mysql & - - - name: Run tests - run: sleep 5 && go test ./client -v -covermode=count + + - name: Run PostgreSQL tests + run: | + ./server.bin --config config.test_pg.toml --sql-directory=sql/pg & + sleep 5 + go test ./client -v -covermode=count + pkill -f server.bin || true + + - name: Run MySQL tests + run: | + ./server.bin --config config.test_mysql.toml --sql-directory=sql/mysql & + sleep 5 + go test ./client -v -covermode=count + pkill -f server.bin || true diff --git a/Makefile b/Makefile index f188742..f51034c 100644 --- a/Makefile +++ b/Makefile @@ -25,6 +25,31 @@ dist: build test: go test ./... -v -p 1 +# Local testing targets using unified test script +.PHONY: test-local +test-local: + ./scripts/test.sh all + +.PHONY: test-setup +test-setup: + ./scripts/test.sh setup + +.PHONY: test-run +test-run: + ./scripts/test.sh run + +.PHONY: test-cleanup +test-cleanup: + ./scripts/test.sh cleanup + +.PHONY: test-postgres +test-postgres: + ./scripts/test.sh run --postgres + +.PHONY: test-mysql +test-mysql: + ./scripts/test.sh run --mysql + # Use goreleaser to do a dry run producing local builds. .PHONY: release-dry release-dry: diff --git a/README.md b/README.md index 2aa9431..b5de281 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,38 @@ dungbeetle --config /path/to/config.toml --sql-directory /path/to/your/sql/queri Starting the server runs a set of workers listening on a default job queue. It also starts an HTTP service on `http://127.0.0.1:6060` which is the control interface. It's possible to run the server without the HTTP interface by passing the `--worker-only` flag. +## Local Testing + +Run tests locally using Docker and Docker Compose. + +### Quick Start + +```bash +# Run all tests +make test-local + +# Test specific databases +make test-postgres +make test-mysql +``` + +### Test Commands + +| Command | Description | +|---------|-------------| +| `make test-local` | Complete test cycle | +| `make test-setup` | Start test services | +| `make test-run` | Run tests | +| `make test-cleanup` | Stop services | +| `make test-postgres` | PostgreSQL tests only | +| `make test-mysql` | MySQL tests only | + +### Prerequisites + +- Docker +- Docker Compose +- Go 1.21+ + ### Usage | Method | URI | | diff --git a/docker-compose.test.yml b/docker-compose.test.yml new file mode 100644 index 0000000..a3b9040 --- /dev/null +++ b/docker-compose.test.yml @@ -0,0 +1,55 @@ +version: '3.8' + +services: + redis: + image: redis:7-alpine + container_name: dungbeetle-test-redis + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - dungbeetle-test + + postgres: + image: postgres:15-alpine + container_name: dungbeetle-test-postgres + environment: + POSTGRES_PASSWORD: testPass + POSTGRES_USER: testUser + POSTGRES_DB: testDB + ports: + - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U testUser -d testDB"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - dungbeetle-test + + mysql: + image: mysql:8.0 + container_name: dungbeetle-test-mysql + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: mydb + MYSQL_USER: user + MYSQL_PASSWORD: userpassword + ports: + - "3306:3306" + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "--password=rootpassword"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - dungbeetle-test + command: --default-authentication-plugin=mysql_native_password + +networks: + dungbeetle-test: + driver: bridge \ No newline at end of file diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..038f490 --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Default values +MODE="all" +DATABASE="all" +VERBOSE=false +CI_MODE=false + +# Project root directory +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DOCKER_COMPOSE_FILE="$PROJECT_ROOT/docker-compose.test.yml" +BINARY_NAME="dungbeetle.test.bin" + +# Determine docker compose command (prefer newer 'docker compose' over legacy 'docker-compose') +if docker compose version > /dev/null 2>&1; then + DOCKER_COMPOSE="docker compose" +elif command -v docker-compose > /dev/null 2>&1; then + DOCKER_COMPOSE="docker-compose" +else + log_error "Neither docker compose nor docker-compose is available" + exit 1 +fi + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + setup|run|cleanup|all) + MODE="$1" + shift + ;; + --postgres|--mysql|--all) + DATABASE="${1#--}" + shift + ;; + -v|--verbose) + VERBOSE=true + shift + ;; + --ci) + CI_MODE=true + shift + ;; + -h|--help) + echo "Usage: $0 [MODE] [OPTIONS]" + echo "" + echo "Modes:" + echo " setup Setup test environment (start services, create tables)" + echo " run Run tests (services must be running)" + echo " cleanup Stop services and cleanup" + echo " all Setup → Run tests → Cleanup (default)" + echo "" + echo "Database Options:" + echo " --postgres Test PostgreSQL only" + echo " --mysql Test MySQL only" + echo " --all Test both databases (default)" + echo "" + echo "Other Options:" + echo " -v, --verbose Verbose output" + echo " --ci CI mode (skip setup/cleanup)" + echo " -h, --help Show this help message" + exit 0 + ;; + *) + echo "Unknown option: $1" + exit 1 + ;; + esac +done + +# Helper functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +log_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +log_verbose() { + if [ "$VERBOSE" = true ]; then + echo -e "${BLUE}[VERBOSE]${NC} $1" + fi +} + +# Check if Docker is running +check_docker() { + if ! docker info > /dev/null 2>&1; then + log_error "Docker is not running. Please start Docker." + exit 1 + fi +} + +# Wait for service to be healthy +wait_for_service() { + local service_name="$1" + local health_check="$2" + local max_attempts=30 + local attempt=1 + + log_info "Waiting for $service_name to be healthy..." + + while [ $attempt -le $max_attempts ]; do + if eval "$health_check" > /dev/null 2>&1; then + log_success "$service_name is healthy" + return 0 + fi + + log_verbose "Attempt $attempt/$max_attempts: $service_name not ready yet" + sleep 2 + attempt=$((attempt + 1)) + done + + log_error "$service_name failed to become healthy" + return 1 +} + +# Setup test environment +setup_environment() { + log_info "Setting up test environment..." + + check_docker + + # Start services + log_info "Starting test services..." + $DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" up -d + + # Wait for services to be healthy + wait_for_service "Redis" "docker exec dungbeetle-test-redis redis-cli ping" + wait_for_service "PostgreSQL" "docker exec dungbeetle-test-postgres pg_isready -U testUser -d testDB" + wait_for_service "MySQL" "docker exec dungbeetle-test-mysql mysqladmin ping -h localhost -u root --password=rootpassword" + + # Create test tables + log_info "Creating test tables..." + + # PostgreSQL table + docker exec dungbeetle-test-postgres psql -U testUser -d testDB -c " + CREATE TABLE IF NOT EXISTS entries ( + id BIGSERIAL PRIMARY KEY, + amount REAL, + user_id VARCHAR(6), + entry_date DATE, + timestamp TIMESTAMP + );" > /dev/null 2>&1 + + # MySQL table + docker exec dungbeetle-test-mysql mysql -u root -prootpassword mydb -e " + CREATE TABLE IF NOT EXISTS entries ( + id BIGINT PRIMARY KEY, + amount REAL, + user_id VARCHAR(6), + entry_date DATE, + timestamp TIMESTAMP + );" > /dev/null 2>&1 + + # Build binary + log_info "Building dungbeetle binary..." + cd "$PROJECT_ROOT" + CGO_ENABLED=0 go build -o "$BINARY_NAME" -ldflags="-s -w" ./cmd/*.go + + log_success "Test environment setup complete" +} + +# Run tests +run_tests() { + log_info "Running tests..." + + cd "$PROJECT_ROOT" + local test_result=0 + + # Test PostgreSQL + if [ "$DATABASE" = "all" ] || [ "$DATABASE" = "postgres" ]; then + log_info "Running PostgreSQL tests..." + + # Start server with PostgreSQL config + ./"$BINARY_NAME" --config config.test_pg.toml --sql-directory=sql/pg & + local server_pid=$! + + # Wait for server to start + sleep 5 + + # Run tests + if go test ./client -v -covermode=count; then + log_success "PostgreSQL tests passed" + else + log_error "PostgreSQL tests failed" + test_result=1 + fi + + # Stop server + kill $server_pid 2>/dev/null || true + wait $server_pid 2>/dev/null || true + fi + + # Test MySQL + if [ "$DATABASE" = "all" ] || [ "$DATABASE" = "mysql" ]; then + log_info "Running MySQL tests..." + + # Start server with MySQL config + ./"$BINARY_NAME" --config config.test_mysql.toml --sql-directory=sql/mysql & + local server_pid=$! + + # Wait for server to start + sleep 5 + + # Run tests + if go test ./client -v -covermode=count; then + log_success "MySQL tests passed" + else + log_error "MySQL tests failed" + test_result=1 + fi + + # Stop server + kill $server_pid 2>/dev/null || true + wait $server_pid 2>/dev/null || true + fi + + if [ $test_result -eq 0 ]; then + log_success "All tests passed" + else + log_error "Some tests failed" + fi + + return $test_result +} + +# Cleanup test environment +cleanup_environment() { + log_info "Cleaning up test environment..." + + cd "$PROJECT_ROOT" + + # Stop and remove containers + if [ -f "$DOCKER_COMPOSE_FILE" ]; then + $DOCKER_COMPOSE -f "$DOCKER_COMPOSE_FILE" down -v + fi + + # Remove binary + rm -f "$BINARY_NAME" + + log_success "Cleanup complete" +} + +# Main execution +main() { + log_info "DungBeetle Test Script" + log_info "Mode: $MODE, Database: $DATABASE, CI Mode: $CI_MODE" + + # Skip setup/cleanup in CI mode + if [ "$CI_MODE" = true ]; then + log_info "CI mode detected, skipping setup/cleanup" + run_tests + exit $? + fi + + case $MODE in + setup) + setup_environment + ;; + run) + run_tests + ;; + cleanup) + cleanup_environment + ;; + all) + setup_environment + run_tests + cleanup_environment + ;; + *) + log_error "Unknown mode: $MODE" + exit 1 + ;; + esac +} + +# Run main function +main "$@"