diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index 75fccda0..a4cfd86c 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -90,6 +90,11 @@ jobs: # show running containers docker ps -a + - name: Install MongoDB core test dependencies + if: matrix.backend == 'mongodb' + run: | + python -m pip install -e . -r tests/mongodb_requirements.txt + - name: Unit tests (DB) if: matrix.backend == 'mongodb' run: pytest -m "mongo" --cov=cachier --cov-report=term --cov-report=xml:cov.xml diff --git a/CLAUDE.md b/CLAUDE.md index 8e24c167..6874a178 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -161,8 +161,9 @@ ______________________________________________________________________ pip install -e . pip install -r tests/requirements.txt # For specific backends: - pip install -r tests/sql_requirements.txt + pip install -r tests/mongodb_requirements.txt pip install -r tests/redis_requirements.txt + pip install -r tests/sql_requirements.txt ``` - **Run all tests:** `pytest` - **Run backend-specific tests:** `pytest -m ` (mongo, redis, sql, memory, pickle, maxage) @@ -177,41 +178,54 @@ ______________________________________________________________________ - **Run example:** `python examples/redis_example.py` - **Update requirements:** Edit `tests/*_requirements.txt` as needed (sql_requirements.txt, redis_requirements.txt). -### MongoDB Local Testing +### Local Testing with Docker -**Quick Start for MongoDB Testing:** +**Quick Start - Test Any Backend Locally:** ```bash -# Option 1: Using the shell script (recommended) -./scripts/test-mongo-local.sh # MongoDB tests only -./scripts/test-mongo-local.sh --mode also-local # MongoDB + memory, pickle, maxage tests - -# Option 2: Using make -make test-mongo-local - -# Option 3: Using docker-compose -docker-compose -f scripts/docker-compose.mongodb.yml up -d -CACHIER_TEST_HOST=localhost CACHIER_TEST_PORT=27017 CACHIER_TEST_VS_DOCKERIZED_MONGO=true pytest -m mongo -docker-compose -f scripts/docker-compose.mongodb.yml down +# Test single backend +./scripts/test-local.sh mongo +./scripts/test-local.sh redis +./scripts/test-local.sh sql + +# Test multiple backends +./scripts/test-local.sh mongo redis +./scripts/test-local.sh external # All external (mongo, redis, sql) +./scripts/test-local.sh all # All backends + +# Test with options +./scripts/test-local.sh mongo redis -v -k # Verbose, keep containers running ``` -**Available Options:** - -- `./scripts/test-mongo-local.sh` - Run MongoDB tests only (default) -- `./scripts/test-mongo-local.sh --mode also-local` - Include memory, pickle, and maxage tests -- `./scripts/test-mongo-local.sh --keep-running` - Keep MongoDB running after tests -- `./scripts/test-mongo-local.sh --verbose` - Show verbose output -- `./scripts/test-mongo-local.sh --coverage-html` - Generate HTML coverage report - **Make Targets:** -- `make test-mongo-local` - Run MongoDB tests with Docker -- `make test-mongo-inmemory` - Run with in-memory MongoDB (default) -- `make mongo-start` - Start MongoDB container -- `make mongo-stop` - Stop MongoDB container -- `make mongo-logs` - View MongoDB logs - -**Note:** By default, MongoDB tests use `pymongo_inmemory` which doesn't require Docker. The above commands let you test against a real MongoDB instance matching the CI environment. +- `make test-local CORES="mongo redis"` - Test specified cores +- `make test-all-local` - Test all backends with Docker +- `make test-external` - Test all external backends +- `make test-mongo-local` - Test MongoDB only +- `make test-redis-local` - Test Redis only +- `make test-sql-local` - Test SQL only +- `make services-start` - Start all Docker containers +- `make services-stop` - Stop all Docker containers + +**Available Cores:** + +- `mongo` - MongoDB backend +- `redis` - Redis backend +- `sql` - PostgreSQL backend +- `memory` - Memory backend (no Docker) +- `pickle` - Pickle backend (no Docker) +- `all` - All backends +- `external` - All external backends (mongo, redis, sql) +- `local` - All local backends (memory, pickle) + +**Options:** + +- `-v, --verbose` - Verbose pytest output +- `-k, --keep-running` - Keep containers running after tests +- `-h, --html-coverage` - Generate HTML coverage report + +**Note:** External backends (MongoDB, Redis, SQL) require Docker. Memory and pickle backends work without Docker. ______________________________________________________________________ diff --git a/Makefile b/Makefile index 28afd175..22918c71 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,11 @@ # Cachier Makefile # Development tasks and shortcuts -.PHONY: help test test-all test-mongo-local test-mongo-docker test-mongo-inmemory \ - test-mongo-also-local mongo-start mongo-stop mongo-logs lint type-check format clean \ +.PHONY: help test test-all test-local test-all-local test-external \ + test-mongo-local test-mongo-docker test-mongo-inmemory test-mongo-also-local \ + test-redis-local test-sql-local \ + services-start services-stop services-logs \ + mongo-start mongo-stop mongo-logs lint type-check format clean \ install install-dev install-all # Default target @@ -11,9 +14,16 @@ help: @echo "" @echo "Testing:" @echo " make test - Run all tests" + @echo " make test-local CORES='...' - Run tests for specified cores with Docker" + @echo " make test-all-local - Run all backend tests with Docker" + @echo " make test-external - Run all external backend tests (mongo, redis, sql)" @echo " make test-mongo-local - Run MongoDB tests with Docker" + @echo " make test-redis-local - Run Redis tests with Docker" + @echo " make test-sql-local - Run SQL tests with Docker" @echo " make test-mongo-also-local - Run MongoDB + memory, pickle, maxage tests with Docker" @echo " make test-mongo-inmemory - Run MongoDB tests with in-memory backend" + @echo " make services-start - Start all test containers" + @echo " make services-stop - Stop all test containers" @echo " make mongo-start - Start MongoDB container" @echo " make mongo-stop - Stop MongoDB container" @echo " make mongo-logs - View MongoDB container logs" @@ -42,8 +52,9 @@ install-dev: install-all: pip install -e .[all] pip install -r tests/requirements.txt - pip install -r tests/sql_requirements.txt + pip install -r tests/mongodb_requirements.txt pip install -r tests/redis_requirements.txt + pip install -r tests/sql_requirements.txt # Testing targets test: @@ -66,6 +77,39 @@ test-mongo-also-local: @echo "Running MongoDB tests with local core tests..." ./scripts/test-mongo-local.sh --mode also-local +# New unified testing targets +test-local: + @echo "Running tests for cores: $(CORES)" + ./scripts/test-local.sh $(CORES) + +test-all-local: + @echo "Running all backend tests with Docker..." + ./scripts/test-local.sh all + +test-external: + @echo "Running all external backend tests..." + ./scripts/test-local.sh external + +test-redis-local: + @echo "Running Redis tests with Docker..." + ./scripts/test-local.sh redis + +test-sql-local: + @echo "Running SQL tests with Docker..." + ./scripts/test-local.sh sql + +# Service management +services-start: + @echo "Starting all test services..." + docker-compose -f scripts/docker-compose.all-cores.yml up -d + +services-stop: + @echo "Stopping all test services..." + docker-compose -f scripts/docker-compose.all-cores.yml down + +services-logs: + docker-compose -f scripts/docker-compose.all-cores.yml logs -f + # MongoDB container management mongo-start: @echo "Starting MongoDB container..." diff --git a/README.rst b/README.rst index 5283444f..025659a8 100644 --- a/README.rst +++ b/README.rst @@ -541,22 +541,25 @@ Running MongoDB tests against a live MongoDB instance .. code-block:: bash - ./scripts/test-mongo-local.sh # MongoDB tests only (default) - ./scripts/test-mongo-local.sh --mode also-local # MongoDB + memory, pickle, maxage tests + # Test MongoDB only + ./scripts/test-local.sh mongo + + # Test MongoDB with local backends + ./scripts/test-local.sh mongo memory pickle This script automatically handles Docker container lifecycle, environment variables, and cleanup. Additional options: -- ``--mode also-local``: Include memory, pickle, and maxage tests alongside MongoDB tests -- ``--keep-running``: Keep MongoDB container running after tests -- ``--verbose``: Show verbose output -- ``--coverage-html``: Generate HTML coverage report +- ``-v, --verbose``: Show verbose output +- ``-k, --keep-running``: Keep containers running after tests +- ``-h, --html-coverage``: Generate HTML coverage report **Option 2: Using Make** .. code-block:: bash - make test-mongo-local # Run tests with Docker MongoDB - make test-mongo-inmemory # Run tests with in-memory MongoDB (default) + make test-mongo-local # Run MongoDB tests with Docker + make test-all-local # Run all backends with Docker + make test-mongo-inmemory # Run with in-memory MongoDB (default) **Option 3: Manual setup** @@ -578,6 +581,28 @@ Contributors are encouraged to test against a real MongoDB instance before submi **HOWEVER, the tests run against a live MongoDB instance when you submit a PR are the determining tests for deciding whether your code functions correctly against MongoDB.** +Testing all backends locally +----------------------------- + +To test all cachier backends (MongoDB, Redis, SQL, Memory, Pickle) locally with Docker: + +.. code-block:: bash + + # Test all backends at once + ./scripts/test-local.sh all + + # Test only external backends (MongoDB, Redis, SQL) + ./scripts/test-local.sh external + + # Test specific combinations + ./scripts/test-local.sh mongo redis + + # Keep containers running for debugging + ./scripts/test-local.sh all -k + +The unified test script automatically manages Docker containers, installs required dependencies, and runs the appropriate test suites. See ``scripts/README-local-testing.md`` for detailed documentation. + + Adding documentation -------------------- diff --git a/pyproject.toml b/pyproject.toml index 5e4e5370..85bb1135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ line-length = 79 # --- ruff --- [tool.ruff] -target-version = "py38" +target-version = "py39" line-length = 79 # Exclude a variety of commonly ignored directories. exclude = [ diff --git a/scripts/README-local-testing.md b/scripts/README-local-testing.md new file mode 100644 index 00000000..7939e31f --- /dev/null +++ b/scripts/README-local-testing.md @@ -0,0 +1,192 @@ +# Local Testing Guide for Cachier + +This guide explains how to run cachier tests locally with Docker containers for external backends. + +## Quick Start + +```bash +# Test a single backend +./scripts/test-local.sh mongo + +# Test multiple backends +./scripts/test-local.sh redis sql + +# Test all backends +./scripts/test-local.sh all + +# Test all external backends (mongo, redis, sql) +./scripts/test-local.sh external + +# Test with options +./scripts/test-local.sh mongo redis -v -k +``` + +## Available Cores + +- **mongo** - MongoDB backend tests +- **redis** - Redis backend tests +- **sql** - SQL (PostgreSQL) backend tests +- **memory** - Memory backend tests (no Docker needed) +- **pickle** - Pickle backend tests (no Docker needed) + +### Core Groups + +- **all** - All backends (mongo, redis, sql, memory, pickle) +- **external** - All external backends requiring Docker (mongo, redis, sql) +- **local** - All local backends (memory, pickle) + +## Command Line Options + +- `-v, --verbose` - Show verbose pytest output +- `-k, --keep-running` - Keep Docker containers running after tests +- `-h, --html-coverage` - Generate HTML coverage report +- `--help` - Show help message + +## Examples + +### Basic Usage + +```bash +# Run MongoDB tests +./scripts/test-local.sh mongo + +# Run Redis and SQL tests +./scripts/test-local.sh redis sql + +# Run all tests +./scripts/test-local.sh all +``` + +### Using Make + +```bash +# Run specific backends +make test-local CORES="mongo redis" + +# Run all tests +make test-all-local + +# Run external backends only +make test-external + +# Run individual backends +make test-mongo-local +make test-redis-local +make test-sql-local +``` + +### Advanced Usage + +```bash +# Keep containers running for debugging +./scripts/test-local.sh mongo redis -k + +# Verbose output with HTML coverage +./scripts/test-local.sh all -v -h + +# Using environment variable +CACHIER_TEST_CORES="mongo redis" ./scripts/test-local.sh +``` + +### Docker Compose + +```bash +# Start all services +make services-start + +# Run tests manually +CACHIER_TEST_HOST=localhost CACHIER_TEST_PORT=27017 CACHIER_TEST_VS_DOCKERIZED_MONGO=true \ +CACHIER_TEST_REDIS_HOST=localhost CACHIER_TEST_REDIS_PORT=6379 CACHIER_TEST_VS_DOCKERIZED_REDIS=true \ +SQLALCHEMY_DATABASE_URL="postgresql://testuser:testpass@localhost:5432/testdb" \ +pytest -m "mongo or redis or sql" + +# Stop all services +make services-stop + +# View logs +make services-logs +``` + +## Docker Containers + +The script manages the following containers: + +| Backend | Container Name | Port | Image | +| ---------- | --------------------- | ----- | -------------- | +| MongoDB | cachier-test-mongo | 27017 | mongo:latest | +| Redis | cachier-test-redis | 6379 | redis:7-alpine | +| PostgreSQL | cachier-test-postgres | 5432 | postgres:15 | + +## Environment Variables + +The script automatically sets the required environment variables: + +### MongoDB + +- `CACHIER_TEST_HOST=localhost` +- `CACHIER_TEST_PORT=27017` +- `CACHIER_TEST_VS_DOCKERIZED_MONGO=true` + +### Redis + +- `CACHIER_TEST_REDIS_HOST=localhost` +- `CACHIER_TEST_REDIS_PORT=6379` +- `CACHIER_TEST_REDIS_DB=0` +- `CACHIER_TEST_VS_DOCKERIZED_REDIS=true` + +### SQL/PostgreSQL + +- `SQLALCHEMY_DATABASE_URL=postgresql://testuser:testpass@localhost:5432/testdb` + +## Prerequisites + +1. **Docker** - Required for external backends (mongo, redis, sql) +2. **Python dependencies** - Install test requirements: + ```bash + pip install -r tests/requirements.txt + pip install -r tests/mongodb_requirements.txt # For MongoDB tests + pip install -r tests/redis_requirements.txt # For Redis tests + pip install -r tests/sql_requirements.txt # For SQL tests + ``` + +## Troubleshooting + +### Docker not found + +- Install Docker Desktop from https://www.docker.com/products/docker-desktop +- Ensure Docker daemon is running + +### Port conflicts + +- The script will fail if required ports are already in use +- Stop conflicting services or use `docker ps` to check running containers + +### Tests failing + +- Check container logs: `docker logs cachier-test-` +- Ensure all dependencies are installed +- Try running with `-v` for verbose output + +### Cleanup issues + +- If containers aren't cleaned up properly: + ```bash + make services-stop + # or manually + docker stop cachier-test-mongo cachier-test-redis cachier-test-postgres + docker rm cachier-test-mongo cachier-test-redis cachier-test-postgres + ``` + +## Best Practices + +1. **Before committing**: Run `./scripts/test-local.sh external` to test all external backends +2. **For quick iteration**: Use memory and pickle tests (no Docker required) +3. **For debugging**: Use `-k` to keep containers running and inspect them +4. **For CI parity**: Test with the same backends that CI uses + +## Future Enhancements + +- Add MySQL/MariaDB support +- Add Elasticsearch support +- Add performance benchmarking mode +- Add parallel test execution for multiple backends diff --git a/scripts/docker-compose.all-cores.yml b/scripts/docker-compose.all-cores.yml new file mode 100644 index 00000000..90de27e5 --- /dev/null +++ b/scripts/docker-compose.all-cores.yml @@ -0,0 +1,81 @@ +# Docker Compose configuration for local testing of all cachier cores +# This file provides all external services needed for comprehensive testing + +version: "3.8" + +services: + mongodb: + image: mongo:latest + container_name: cachier-test-mongo + ports: + - "27017:27017" + environment: + MONGO_INITDB_DATABASE: cachier_test + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - cachier-test + + redis: + image: redis:7-alpine + container_name: cachier-test-redis + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 + start_period: 5s + networks: + - cachier-test + + postgres: + image: postgres:15 + container_name: cachier-test-postgres + ports: + - "5432:5432" + environment: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpass + POSTGRES_DB: testdb + healthcheck: + test: ["CMD-SHELL", "pg_isready -U testuser"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - cachier-test + +networks: + cachier-test: + driver: bridge +# Usage examples: +# +# Start all services: +# docker-compose -f scripts/docker-compose.all-cores.yml up -d +# +# Start specific services: +# docker-compose -f scripts/docker-compose.all-cores.yml up -d mongodb redis +# +# Check service health: +# docker-compose -f scripts/docker-compose.all-cores.yml ps +# +# View logs: +# docker-compose -f scripts/docker-compose.all-cores.yml logs -f +# +# Stop all services: +# docker-compose -f scripts/docker-compose.all-cores.yml down +# +# Run tests with all services: +# docker-compose -f scripts/docker-compose.all-cores.yml up -d +# CACHIER_TEST_HOST=localhost CACHIER_TEST_PORT=27017 CACHIER_TEST_VS_DOCKERIZED_MONGO=true \ +# CACHIER_TEST_REDIS_HOST=localhost CACHIER_TEST_REDIS_PORT=6379 CACHIER_TEST_VS_DOCKERIZED_REDIS=true \ +# SQLALCHEMY_DATABASE_URL="postgresql://testuser:testpass@localhost:5432/testdb" \ +# pytest -m "mongo or redis or sql" +# docker-compose -f scripts/docker-compose.all-cores.yml down diff --git a/scripts/docker-compose.mongodb.yml b/scripts/docker-compose.mongodb.yml deleted file mode 100644 index a34b11e8..00000000 --- a/scripts/docker-compose.mongodb.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Docker Compose configuration for local MongoDB testing -# This file provides an alternative to the shell script approach - -version: "3.8" - -services: - mongodb: - image: mongo:latest - container_name: cachier-test-mongo - ports: - - "27017:27017" - environment: - MONGO_INITDB_DATABASE: cachier_test - healthcheck: - test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet - interval: 5s - timeout: 5s - retries: 5 - start_period: 10s - volumes: - # Optional: persist data between test runs - # - cachier-mongo-data:/data/db - # Optional: custom initialization scripts - # - ./scripts/mongo-init:/docker-entrypoint-initdb.d - networks: - - cachier-test - -networks: - cachier-test: - driver: bridge -# Optional: volume for data persistence -# volumes: -# cachier-mongo-data: - -# Usage: -# 1. Start MongoDB: docker-compose -f scripts/docker-compose.mongodb.yml up -d -# 2. Run tests with environment variables: -# CACHIER_TEST_HOST=localhost CACHIER_TEST_PORT=27017 CACHIER_TEST_VS_DOCKERIZED_MONGO=true pytest -m "mongo" -# 3. Stop MongoDB: docker-compose -f scripts/docker-compose.mongodb.yml down - -# Alternative one-liner: -# docker-compose -f scripts/docker-compose.mongodb.yml run --rm -e CACHIER_TEST_HOST=localhost -e CACHIER_TEST_PORT=27017 -e CACHIER_TEST_VS_DOCKERIZED_MONGO=true test diff --git a/scripts/test-local.sh b/scripts/test-local.sh new file mode 100755 index 00000000..9c9b9b8e --- /dev/null +++ b/scripts/test-local.sh @@ -0,0 +1,486 @@ +#!/bin/bash +# test-local.sh - Run cachier tests locally with Docker for any combination of cores +# This script provides a unified interface for testing all cachier backends locally + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Configuration +MONGO_CONTAINER="cachier-test-mongo" +REDIS_CONTAINER="cachier-test-redis" +POSTGRES_CONTAINER="cachier-test-postgres" + +# Default settings +VERBOSE=false +COVERAGE_REPORT="term" +KEEP_RUNNING=false +SELECTED_CORES="" +INCLUDE_LOCAL_CORES=false + +# Function to print colored messages +print_message() { + local color=$1 + local message=$2 + echo -e "${color}${message}${NC}" +} + +# Function to print usage +usage() { + cat << EOF +Usage: $0 [OPTIONS] [CORES...] + +Run cachier tests locally with Docker containers for external backends. + +CORES: + mongo MongoDB backend tests + redis Redis backend tests + sql SQL (PostgreSQL) backend tests + memory Memory backend tests (no Docker needed) + pickle Pickle backend tests (no Docker needed) + all All backends (equivalent to: mongo redis sql memory pickle) + external All external backends (mongo redis sql) + local All local backends (memory pickle) + +OPTIONS: + -v, --verbose Show verbose output + -k, --keep-running Keep containers running after tests + -h, --html-coverage Generate HTML coverage report + --help Show this help message + +EXAMPLES: + $0 mongo # Run only MongoDB tests + $0 redis sql # Run Redis and SQL tests + $0 all # Run all backend tests + $0 external -k # Run external backends, keep containers + $0 mongo memory -v # Run MongoDB and memory tests verbosely + +ENVIRONMENT: + You can also set cores via CACHIER_TEST_CORES environment variable: + CACHIER_TEST_CORES="mongo redis" $0 + +EOF +} + +# Parse command line arguments +while [[ $# -gt 0 ]]; do + case $1 in + -v|--verbose) + VERBOSE=true + shift + ;; + -k|--keep-running) + KEEP_RUNNING=true + shift + ;; + -h|--html-coverage) + COVERAGE_REPORT="html" + shift + ;; + --help) + usage + exit 0 + ;; + -*) + print_message $RED "Unknown option: $1" + usage + exit 1 + ;; + *) + # This is a core name + SELECTED_CORES="$SELECTED_CORES $1" + shift + ;; + esac +done + +# If no cores specified, check environment variable +if [ -z "$SELECTED_CORES" ] && [ -n "$CACHIER_TEST_CORES" ]; then + SELECTED_CORES=$CACHIER_TEST_CORES +fi + +# If still no cores specified, show usage +if [ -z "$SELECTED_CORES" ]; then + print_message $RED "Error: No cores specified" + usage + exit 1 +fi + +# Expand core groups +expand_cores() { + local cores="" + for core in $1; do + case $core in + all) + cores="$cores mongo redis sql memory pickle" + ;; + external) + cores="$cores mongo redis sql" + ;; + local) + cores="$cores memory pickle" + ;; + *) + cores="$cores $core" + ;; + esac + done + # Remove duplicates + echo "$cores" | tr ' ' '\n' | sort -u | tr '\n' ' ' +} + +SELECTED_CORES=$(expand_cores "$SELECTED_CORES") + +# Define core to marker mappings using a function +get_markers_for_core() { + case $1 in + mongo) echo "mongo" ;; + redis) echo "redis" ;; + sql) echo "sql" ;; + memory) echo "memory" ;; + pickle) echo "pickle or maxage" ;; + *) echo "$1" ;; # Default to core name + esac +} + +# Validate cores +validate_cores() { + local valid_cores="mongo redis sql memory pickle" + for core in $1; do + if ! echo "$valid_cores" | grep -qw "$core"; then + print_message $RED "Error: Invalid core '$core'" + print_message $YELLOW "Valid cores: mongo, redis, sql, memory, pickle" + exit 1 + fi + done +} + +validate_cores "$SELECTED_CORES" + +# Function to check if Docker is available +check_docker() { + if ! command -v docker &> /dev/null; then + print_message $RED "Error: Docker is required but not installed." + echo "Please install Docker from: https://www.docker.com/products/docker-desktop" + exit 1 + fi + + if ! docker ps &> /dev/null; then + print_message $RED "Error: Docker daemon is not running." + echo "Please start Docker and try again." + exit 1 + fi +} + +# Function to check and install dependencies +check_dependencies() { + local missing_deps=false + + print_message $YELLOW "Checking test dependencies..." + + # Check base test requirements + if ! python -c "import pytest" 2>/dev/null; then + print_message $YELLOW "Installing base test requirements..." + pip install -r tests/requirements.txt || { + print_message $RED "Failed to install base test requirements" + exit 1 + } + fi + + # Check MongoDB dependencies if testing MongoDB + if echo "$SELECTED_CORES" | grep -qw "mongo"; then + if ! python -c "import pymongo" 2>/dev/null; then + print_message $YELLOW "Installing MongoDB test requirements..." + pip install -r tests/mongodb_requirements.txt || { + print_message $RED "Failed to install MongoDB requirements" + exit 1 + } + fi + fi + + # Check Redis dependencies if testing Redis + if echo "$SELECTED_CORES" | grep -qw "redis"; then + if ! python -c "import redis" 2>/dev/null; then + print_message $YELLOW "Installing Redis test requirements..." + pip install -r tests/redis_requirements.txt || { + print_message $RED "Failed to install Redis requirements" + exit 1 + } + fi + fi + + # Check SQL dependencies if testing SQL + if echo "$SELECTED_CORES" | grep -qw "sql"; then + if ! python -c "import sqlalchemy" 2>/dev/null; then + print_message $YELLOW "Installing SQL test requirements..." + pip install -r tests/sql_requirements.txt || { + print_message $RED "Failed to install SQL requirements" + exit 1 + } + fi + fi + + print_message $GREEN "All required dependencies are installed!" +} + +# MongoDB functions +start_mongodb() { + print_message $YELLOW "Starting MongoDB container..." + + # Remove existing container if any + docker rm -f $MONGO_CONTAINER > /dev/null 2>&1 || true + + # Start MongoDB + if [ "$VERBOSE" = true ]; then + docker run -d -p 27017:27017 --name $MONGO_CONTAINER mongo:latest + else + docker run -d -p 27017:27017 --name $MONGO_CONTAINER mongo:latest > /dev/null 2>&1 + fi + + # Wait for MongoDB to be ready + print_message $YELLOW "Waiting for MongoDB to be ready..." + sleep 5 + + if docker exec $MONGO_CONTAINER mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; then + print_message $GREEN "MongoDB is ready!" + else + print_message $YELLOW "MongoDB might need more time to start. Proceeding anyway..." + fi +} + +stop_mongodb() { + if [ "$KEEP_RUNNING" = false ]; then + print_message $YELLOW "Stopping MongoDB container..." + docker stop $MONGO_CONTAINER > /dev/null 2>&1 || true + docker rm $MONGO_CONTAINER > /dev/null 2>&1 || true + else + print_message $BLUE "MongoDB container kept running at localhost:27017" + fi +} + +test_mongodb() { + export CACHIER_TEST_HOST=localhost + export CACHIER_TEST_PORT=27017 + export CACHIER_TEST_VS_DOCKERIZED_MONGO=true +} + +# Redis functions +start_redis() { + print_message $YELLOW "Starting Redis container..." + + # Remove existing container if any + docker rm -f $REDIS_CONTAINER > /dev/null 2>&1 || true + + # Start Redis + if [ "$VERBOSE" = true ]; then + docker run -d -p 6379:6379 --name $REDIS_CONTAINER redis:7-alpine + else + docker run -d -p 6379:6379 --name $REDIS_CONTAINER redis:7-alpine > /dev/null 2>&1 + fi + + # Wait for Redis to be ready + print_message $YELLOW "Waiting for Redis to be ready..." + sleep 2 + + if docker exec $REDIS_CONTAINER redis-cli ping > /dev/null 2>&1; then + print_message $GREEN "Redis is ready!" + else + print_message $YELLOW "Redis might need more time to start. Proceeding anyway..." + fi +} + +stop_redis() { + if [ "$KEEP_RUNNING" = false ]; then + print_message $YELLOW "Stopping Redis container..." + docker stop $REDIS_CONTAINER > /dev/null 2>&1 || true + docker rm $REDIS_CONTAINER > /dev/null 2>&1 || true + else + print_message $BLUE "Redis container kept running at localhost:6379" + fi +} + +test_redis() { + export CACHIER_TEST_REDIS_HOST=localhost + export CACHIER_TEST_REDIS_PORT=6379 + export CACHIER_TEST_REDIS_DB=0 + export CACHIER_TEST_VS_DOCKERIZED_REDIS=true +} + +# SQL/PostgreSQL functions +start_postgres() { + print_message $YELLOW "Starting PostgreSQL container..." + + # Remove existing container if any + docker rm -f $POSTGRES_CONTAINER > /dev/null 2>&1 || true + + # Start PostgreSQL + if [ "$VERBOSE" = true ]; then + docker run -d \ + -e POSTGRES_USER=testuser \ + -e POSTGRES_PASSWORD=testpass \ + -e POSTGRES_DB=testdb \ + -p 5432:5432 \ + --name $POSTGRES_CONTAINER \ + postgres:15 + else + docker run -d \ + -e POSTGRES_USER=testuser \ + -e POSTGRES_PASSWORD=testpass \ + -e POSTGRES_DB=testdb \ + -p 5432:5432 \ + --name $POSTGRES_CONTAINER \ + postgres:15 > /dev/null 2>&1 + fi + + # Wait for PostgreSQL to be ready + print_message $YELLOW "Waiting for PostgreSQL to be ready..." + sleep 5 + + if docker exec $POSTGRES_CONTAINER pg_isready -U testuser > /dev/null 2>&1; then + print_message $GREEN "PostgreSQL is ready!" + else + print_message $YELLOW "PostgreSQL might need more time to start. Proceeding anyway..." + fi +} + +stop_postgres() { + if [ "$KEEP_RUNNING" = false ]; then + print_message $YELLOW "Stopping PostgreSQL container..." + docker stop $POSTGRES_CONTAINER > /dev/null 2>&1 || true + docker rm $POSTGRES_CONTAINER > /dev/null 2>&1 || true + else + print_message $BLUE "PostgreSQL container kept running at localhost:5432" + fi +} + +test_sql() { + export SQLALCHEMY_DATABASE_URL="postgresql://testuser:testpass@localhost:5432/testdb" +} + +# Main execution +main() { + print_message $GREEN "=== Cachier Local Testing ===" + print_message $BLUE "Selected cores: $SELECTED_CORES" + + # Check and install dependencies + check_dependencies + + # Check if we need Docker + needs_docker=false + for core in $SELECTED_CORES; do + case $core in + mongo|redis|sql) + needs_docker=true + ;; + esac + done + + if [ "$needs_docker" = true ]; then + check_docker + fi + + # Track which containers we started + STARTED_CONTAINERS="" + + # Start required services + for core in $SELECTED_CORES; do + case $core in + mongo) + start_mongodb + STARTED_CONTAINERS="$STARTED_CONTAINERS mongo" + ;; + redis) + start_redis + STARTED_CONTAINERS="$STARTED_CONTAINERS redis" + ;; + sql) + start_postgres + STARTED_CONTAINERS="$STARTED_CONTAINERS sql" + ;; + esac + done + + # Set up cleanup trap + cleanup() { + for container in $STARTED_CONTAINERS; do + case $container in + mongo) stop_mongodb ;; + redis) stop_redis ;; + sql) stop_postgres ;; + esac + done + } + trap cleanup EXIT + + # Run tests + print_message $YELLOW "Running tests..." + + # Build pytest marker expression + pytest_markers="" + for core in $SELECTED_CORES; do + # Get the markers for this core + core_markers=$(get_markers_for_core "$core") + + if [ -z "$pytest_markers" ]; then + pytest_markers="$core_markers" + else + # Add parentheses around multi-part markers for proper precedence + if [[ "$core_markers" == *" or "* ]]; then + pytest_markers="$pytest_markers or ($core_markers)" + else + pytest_markers="$pytest_markers or $core_markers" + fi + fi + + # Set environment variables for each core + case $core in + mongo) test_mongodb ;; + redis) test_redis ;; + sql) test_sql ;; + esac + done + + # Run pytest + # Check if we selected all cores - if so, run all tests without marker filtering + all_cores="memory mongo pickle redis sql" + selected_sorted=$(echo "$SELECTED_CORES" | tr ' ' '\n' | sort | tr '\n' ' ' | xargs) + all_sorted=$(echo "$all_cores" | tr ' ' '\n' | sort | tr '\n' ' ' | xargs) + + if [ "$selected_sorted" = "$all_sorted" ]; then + print_message $BLUE "Running: pytest (all tests, including unmarked)" + if [ "$VERBOSE" = true ]; then + pytest -v --cov=cachier --cov-report=$COVERAGE_REPORT + else + pytest --cov=cachier --cov-report=$COVERAGE_REPORT + fi + else + print_message $BLUE "Running: pytest -m \"$pytest_markers\"" + if [ "$VERBOSE" = true ]; then + pytest -v -m "$pytest_markers" --cov=cachier --cov-report=$COVERAGE_REPORT + else + pytest -m "$pytest_markers" --cov=cachier --cov-report=$COVERAGE_REPORT + fi + fi + + TEST_EXIT_CODE=$? + + if [ $TEST_EXIT_CODE -eq 0 ]; then + print_message $GREEN "All tests passed!" + else + print_message $RED "Some tests failed. Exit code: $TEST_EXIT_CODE" + fi + + # Exit with test status + exit $TEST_EXIT_CODE +} + +# Run main function +main diff --git a/scripts/test-mongo-local.sh b/scripts/test-mongo-local.sh deleted file mode 100755 index ecebc3c2..00000000 --- a/scripts/test-mongo-local.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -# test-mongo-local.sh - Run MongoDB tests locally with Docker -# This script replicates the CI MongoDB testing environment locally - -set -e # Exit on error - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Configuration -CONTAINER_NAME="cachier-test-mongo" -MONGODB_PORT=27017 -MONGODB_IMAGE="mongo:latest" -WAIT_TIME=5 - -# Parse command line arguments -KEEP_RUNNING=false -VERBOSE=false -COVERAGE_REPORT="term" -TEST_MODE="mongo" # Default to mongo only - -while [[ "$#" -gt 0 ]]; do - case $1 in - --keep-running) KEEP_RUNNING=true ;; - --verbose|-v) VERBOSE=true ;; - --coverage-html) COVERAGE_REPORT="html" ;; - --mode) - if [[ "$2" == "also-local" ]]; then - TEST_MODE="also-local" - else - echo "Unknown mode: $2. Use 'also-local' to include memory, pickle, and maxage tests." - exit 1 - fi - shift - ;; - --help|-h) - echo "Usage: $0 [options]" - echo "Options:" - echo " --keep-running Keep MongoDB container running after tests" - echo " --verbose, -v Show verbose output" - echo " --coverage-html Generate HTML coverage report" - echo " --mode also-local Include memory, pickle, and maxage tests" - echo " --help, -h Show this help message" - exit 0 - ;; - *) echo "Unknown parameter: $1"; exit 1 ;; - esac - shift -done - -# Function to print colored messages -print_message() { - local color=$1 - local message=$2 - echo -e "${color}${message}${NC}" -} - -# Function to check if Docker is available -check_docker() { - if ! command -v docker &> /dev/null; then - print_message $RED "Error: Docker is required but not installed." - echo "Please install Docker from: https://www.docker.com/products/docker-desktop" - exit 1 - fi - - if ! docker ps &> /dev/null; then - print_message $RED "Error: Docker daemon is not running." - echo "Please start Docker and try again." - exit 1 - fi -} - -# Function to check if port is available -check_port() { - if lsof -Pi :$MONGODB_PORT -sTCP:LISTEN -t >/dev/null 2>&1; then - print_message $RED "Error: Port $MONGODB_PORT is already in use." - echo "Please stop the service using this port or use a different port." - exit 1 - fi -} - -# Function to start MongoDB container -start_mongodb() { - print_message $YELLOW "Starting MongoDB container..." - - # Check if container already exists - if docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then - print_message $YELLOW "Removing existing container..." - docker rm -f $CONTAINER_NAME > /dev/null 2>&1 - fi - - # Start new container - if [ "$VERBOSE" = true ]; then - docker run -d -p $MONGODB_PORT:27017 --name $CONTAINER_NAME $MONGODB_IMAGE - else - docker run -d -p $MONGODB_PORT:27017 --name $CONTAINER_NAME $MONGODB_IMAGE > /dev/null 2>&1 - fi - - if [ $? -eq 0 ]; then - print_message $GREEN "MongoDB container started successfully." - else - print_message $RED "Failed to start MongoDB container." - exit 1 - fi - - # Wait for MongoDB to be ready - print_message $YELLOW "Waiting for MongoDB to be ready..." - sleep $WAIT_TIME - - # Verify MongoDB is responding - if docker exec $CONTAINER_NAME mongosh --eval "db.adminCommand('ping')" > /dev/null 2>&1; then - print_message $GREEN "MongoDB is ready!" - else - print_message $YELLOW "MongoDB might need more time to start. Proceeding anyway..." - fi -} - -# Function to run tests -run_tests() { - print_message $YELLOW "Running MongoDB tests..." - echo $PWD - - # Set environment variables - export CACHIER_TEST_HOST=localhost - export CACHIER_TEST_PORT=$MONGODB_PORT - export CACHIER_TEST_VS_DOCKERIZED_MONGO=true - - # Run pytest with coverage - if [ "$TEST_MODE" = "also-local" ]; then - print_message $YELLOW "Running MongoDB tests with local core tests (memory, pickle, maxage)..." - if [ "$VERBOSE" = true ]; then - pytest -v -m "mongo or memory or pickle or maxage" \ - --cov=cachier --cov-report=$COVERAGE_REPORT - else - pytest -m "mongo or memory or pickle or maxage" \ - --cov=cachier --cov-report=$COVERAGE_REPORT - fi - else - print_message $YELLOW "Running MongoDB tests only..." - if [ "$VERBOSE" = true ]; then - pytest -v -m "mongo" \ - --cov=cachier --cov-report=$COVERAGE_REPORT - else - pytest -m "mongo" \ - --cov=cachier --cov-report=$COVERAGE_REPORT - fi - fi - - # Capture test exit code - TEST_EXIT_CODE=$? - - if [ $TEST_EXIT_CODE -eq 0 ]; then - print_message $GREEN "All tests passed!" - else - print_message $RED "Some tests failed. Exit code: $TEST_EXIT_CODE" - fi - - return $TEST_EXIT_CODE -} - -# Function to cleanup -cleanup() { - if [ "$KEEP_RUNNING" = true ]; then - print_message $YELLOW "MongoDB container kept running at localhost:$MONGODB_PORT" - echo "To stop it manually, run: docker stop $CONTAINER_NAME && docker rm $CONTAINER_NAME" - else - print_message $YELLOW "Cleaning up..." - docker stop $CONTAINER_NAME > /dev/null 2>&1 - docker rm $CONTAINER_NAME > /dev/null 2>&1 - print_message $GREEN "Cleanup complete." - fi -} - -# Main execution -main() { - print_message $GREEN "=== Cachier MongoDB Local Testing ===" - - # Check prerequisites - check_docker - check_port - - # Set trap for cleanup on exit - trap cleanup EXIT - - # Start MongoDB - start_mongodb - - # Run tests - run_tests - TEST_RESULT=$? - - # Exit with test status - exit $TEST_RESULT -} - -# Run main function -main diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..3e3717f3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,28 @@ +"""Pytest configuration and shared fixtures for cachier tests.""" + +import pytest + + +@pytest.fixture(scope="session", autouse=True) +def cleanup_mongo_clients(): + """Clean up any MongoDB clients created during tests. + + This fixture runs automatically after all tests complete. + + """ + # Let tests run + yield + + # Cleanup after all tests + try: + from tests.test_mongo_core import _test_mongetter + + if hasattr(_test_mongetter, "client"): + # Close the MongoDB client to avoid ResourceWarning + _test_mongetter.client.close() + # Remove the client attribute so future test runs start fresh + delattr(_test_mongetter, "client") + except (ImportError, AttributeError): + # If the module wasn't imported or client wasn't created, + # then there's nothing to clean up + pass diff --git a/tests/mongodb_requirements.txt b/tests/mongodb_requirements.txt new file mode 100644 index 00000000..4348500a --- /dev/null +++ b/tests/mongodb_requirements.txt @@ -0,0 +1,7 @@ + +# to connect to the test mongodb server +pymongo +dnspython +pymongo-inmemory +# to test pandas dataframe as-param hashing with mongodb core +pandas diff --git a/tests/requirements.txt b/tests/requirements.txt index 195b393e..23c73edc 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,12 +4,8 @@ pytest coverage pytest-cov birch -# to connect to the test mongodb server -pymongo -dnspython -pymongo-inmemory -# to test pandas dataframe as-param hashing with mongodb core -pandas # to be able to run `python setup.py checkdocs` collective.checkdocs pygments +# the memory core tests dataframe caching +pandas diff --git a/tests/test_core_lookup.py b/tests/test_core_lookup.py index 442f2e5c..877b683f 100644 --- a/tests/test_core_lookup.py +++ b/tests/test_core_lookup.py @@ -3,7 +3,6 @@ import pytest from cachier import cachier, get_global_params -from cachier.cores.mongo import MissingMongetter def test_get_default_params(): @@ -33,13 +32,3 @@ def test_bad_name(): @cachier(backend=invalid_core) def dummy_func(): pass - - -def test_missing_mongetter(): - # Test that the appropriate exception is thrown - # when forgetting to specify the mongetter. - with pytest.raises(MissingMongetter): - - @cachier(backend="mongo", mongetter=None) - def dummy_func(): - pass diff --git a/tests/test_mongo_core.py b/tests/test_mongo_core.py index 92371f79..86f88fc9 100644 --- a/tests/test_mongo_core.py +++ b/tests/test_mongo_core.py @@ -1,5 +1,6 @@ """Testing the MongoDB core of cachier.""" +# standard library imports import datetime import hashlib import platform @@ -10,14 +11,58 @@ from time import sleep from urllib.parse import quote_plus -import pandas as pd -import pymongo +# third-party imports import pytest from birch import Birch # type: ignore[import-not-found] -from pymongo.errors import OperationFailure -from pymongo.mongo_client import MongoClient -from pymongo_inmemory import MongoClient as InMemoryMongoClient +try: + import pandas as pd +except (ImportError, ModuleNotFoundError): + pd = None + print("pandas is not installed; tests requiring pandas will fail!") + +try: + import pymongo + from pymongo.errors import OperationFailure + from pymongo.mongo_client import MongoClient + + from cachier.cores.mongo import MissingMongetter +except (ImportError, ModuleNotFoundError): + print("pymongo is not installed; tests requiring pymongo will fail!") + pymongo = None + OperationFailure = None + MissingMongetter = None + + # define a mock MongoClient class that will raise an exception + # on init, warning that pymongo is not installed + class MongoClient: + """Mock MongoClient class raising ImportError on missing pymongo.""" + + def __init__(self, *args, **kwargs): + """Initialize the mock MongoClient.""" + raise ImportError("pymongo is not installed!") + + +try: + from pymongo_inmemory import MongoClient as InMemoryMongoClient +except (ImportError, ModuleNotFoundError): + + class InMemoryMongoClient: + """Mock InMemoryMongoClient class. + + Raises an ImportError on missing pymongo_inmemory. + + """ + + def __init__(self, *args, **kwargs): + """Initialize the mock InMemoryMongoClient.""" + raise ImportError("pymongo_inmemory is not installed!") + + print( + "pymongo_inmemory is not installed; in-memory MongoDB tests will fail!" + ) + +# local imports from cachier import cachier from cachier.config import CacheEntry from cachier.cores.base import RecalculationNeeded @@ -78,6 +123,17 @@ def _test_mongetter(): # === Mongo core tests === +@pytest.mark.mongo +def test_missing_mongetter(): + # Test that the appropriate exception is thrown + # when forgetting to specify the mongetter. + with pytest.raises(MissingMongetter): + + @cachier(backend="mongo", mongetter=None) + def dummy_func(): + pass + + @pytest.mark.mongo def test_information(): print("\npymongo version: ", end="") diff --git a/tests/test_sql_core.py b/tests/test_sql_core.py index fdcaa04a..a1f1867c 100644 --- a/tests/test_sql_core.py +++ b/tests/test_sql_core.py @@ -191,24 +191,42 @@ def test_import_cachier_without_sqlalchemy(monkeypatch): sys.modules.update(modules_backup) -@pytest.mark.pickle +@pytest.mark.sql def test_sqlcore_importerror_without_sqlalchemy(monkeypatch): """Test that using SQL core without SQLAlchemy raises an ImportError.""" - # Simulate SQLAlchemy not installed - modules_backup = sys.modules.copy() - sys.modules["sqlalchemy"] = None - sys.modules["sqlalchemy.orm"] = None - sys.modules["sqlalchemy.engine"] = None + # Remove sql module from sys.modules to force reimport + if "cachier.cores.sql" in sys.modules: + del sys.modules["cachier.cores.sql"] + + # Mock the sqlalchemy import to raise ImportError + import builtins + + original_import = builtins.__import__ + + def mock_import(name, *args, **kwargs): + if name.startswith("sqlalchemy"): + raise ImportError(f"No module named '{name}'") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", mock_import) + try: + # Now import sql - it should set SQLALCHEMY_AVAILABLE = False import importlib sql_mod = importlib.import_module("cachier.cores.sql") + + # Verify that SQLALCHEMY_AVAILABLE is False + assert not sql_mod.SQLALCHEMY_AVAILABLE + + # Now trying to create _SQLCore should raise ImportError with pytest.raises(ImportError) as excinfo: sql_mod._SQLCore(hash_func=None, sql_engine="sqlite:///:memory:") assert "SQLAlchemy is required" in str(excinfo.value) finally: - sys.modules.clear() - sys.modules.update(modules_backup) + # Clean up - remove the module so next tests reimport it fresh + if "cachier.cores.sql" in sys.modules: + del sys.modules["cachier.cores.sql"] @pytest.mark.sql