#!/bin/bash set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_DIR" # Cleanup function to stop containers cleanup() { echo "" echo "🛑 Shutting down containers..." COMPOSE_FILES="-f docker-compose.test.yml" if [ -f "docker-compose.test.local.yml" ]; then COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.test.local.yml" fi docker compose $COMPOSE_FILES down 2>/dev/null || docker-compose $COMPOSE_FILES down 2>/dev/null || true rm -f docker-compose.test.local.yml exit 0 } # Trap SIGINT (Ctrl+C) and SIGTERM to run cleanup trap cleanup SIGINT SIGTERM # Port configuration (can be overridden via environment variables) SERVICE_HOST_PORT="${BOM_SERVICE_PORT:-8082}" HA_HOST_PORT="${HA_PORT:-8124}" # Parse command-line arguments SKIP_BUILD=0 FORCE_BUILD=0 USE_DOCKER_BUILD="" USE_NPM_BUILD="" SERVICE_PATH="" show_help() { cat < /dev/null; then USE_DOCKER_BUILD=1 elif command -v npm &> /dev/null; then USE_NPM_BUILD=1 else echo "❌ Error: Neither Docker nor npm found" echo " Please install Docker or Node.js/npm" exit 1 fi fi ensure_test_ha_config() { if [ ! -f "test-ha/configuration.yaml" ]; then echo "❌ Error: test-ha/configuration.yaml not found" echo " This test harness expects local Home Assistant seed data in test-ha/" exit 1 fi } # Write docker-compose override for local service build (single place; avoids create/rm/create on cold start) write_service_compose_override() { cat > docker-compose.test.local.yml </dev/null; then echo " ✅ Updated $file_name" return 0 fi echo " 🔐 Retrying $file_name update via Docker (root-owned target)..." docker run --rm \ -v "$(pwd)/test-ha:/workspace:rw" \ -u root \ alpine:latest \ sh -c "cp \"/workspace/$file_name\" \"/workspace/config/$file_name\"" >/dev/null echo " ✅ Updated $file_name" } # Function to build using npm (local) build_with_npm() { echo "🔨 Building card with npm (local)..." if ! command -v npm &> /dev/null; then echo "❌ Error: npm not found" echo " Install Node.js/npm or use Docker: ./scripts/test.sh --docker-build" exit 1 fi # Check if node_modules exists if [ ! -d "node_modules" ]; then echo "📦 Installing dependencies..." npm install fi npm run build } # Function to build using Docker build_with_docker() { echo "🐳 Building card using Docker (isolated environment)..." if ! command -v docker &> /dev/null; then echo "❌ Error: docker not found" echo " Install Docker or use npm: ./scripts/test.sh --npm-build" exit 1 fi if [ "$FORCE_BUILD" = "1" ]; then FORCE_REBUILD=1 bash scripts/build-docker.sh else bash scripts/build-docker.sh fi } # Build card CARD_BUILD_PATH="custom_components/bom_local/www/bom-local-card.js" if [ "$SKIP_BUILD" = "1" ]; then echo "⏭️ Skipping build (--skip-build)" if [ ! -f "$CARD_BUILD_PATH" ]; then echo "❌ Error: $CARD_BUILD_PATH not found and --skip-build was used" exit 1 fi elif [ "$FORCE_BUILD" = "1" ] || [ ! -f "$CARD_BUILD_PATH" ]; then if [ "$USE_DOCKER_BUILD" = "1" ]; then build_with_docker elif [ "$USE_NPM_BUILD" = "1" ]; then build_with_npm else # Auto-detect: prefer Docker, fallback to npm if command -v docker &> /dev/null; then build_with_docker else build_with_npm fi fi if [ ! -f "$CARD_BUILD_PATH" ]; then echo "❌ Error: $CARD_BUILD_PATH not found after build" exit 1 fi # Verify build quality FILE_SIZE=$(stat -f%z "$CARD_BUILD_PATH" 2>/dev/null || stat -c%s "$CARD_BUILD_PATH" 2>/dev/null || echo "0") if [ "$FILE_SIZE" -lt 50000 ]; then echo "⚠️ Warning: Built file is suspiciously small (${FILE_SIZE} bytes)" echo " This might indicate dependencies weren't bundled correctly" echo " Check rollup.config.js - dependencies should NOT be marked as external" fi # Check for external imports (should not exist if dependencies are bundled) if grep -q "from ['\"]lit['\"]" "$CARD_BUILD_PATH" 2>/dev/null || \ grep -q "from ['\"]custom-card-helpers['\"]" "$CARD_BUILD_PATH" 2>/dev/null; then echo "⚠️ Warning: Found external import statements in built file" echo " Dependencies may not be bundled. Check rollup.config.js" fi echo "✅ Build complete (${FILE_SIZE} bytes)" else echo "✅ Using existing $CARD_BUILD_PATH" echo " (Use --force-build to rebuild, --docker-build or --npm-build to specify method)" fi # Check if containers are already running CONTAINERS_RUNNING=0 COMPOSE_FILES_CHECK="-f docker-compose.test.yml" if [ -f "docker-compose.test.local.yml" ]; then COMPOSE_FILES_CHECK="$COMPOSE_FILES_CHECK -f docker-compose.test.local.yml" fi if docker compose $COMPOSE_FILES_CHECK ps 2>/dev/null | grep -q "Up" || \ docker-compose $COMPOSE_FILES_CHECK ps 2>/dev/null | grep -q "Up"; then CONTAINERS_RUNNING=1 fi # If containers are running, automatically update the card (no need for UPDATE_MODE) if [ "$CONTAINERS_RUNNING" = "1" ]; then echo "🔄 Updating card in running test environment (preserving HA state)..." # Copy card file - no longer needed as integration is mounted echo "📋 Integration is mounted directly, no file copy needed." # Copy configuration files to HA config directory (for dashboard changes) ensure_test_ha_config echo "📋 Updating HA configuration files..." for config_file in configuration.yaml ui-lovelace.yaml; do sync_test_ha_file "$config_file" done # Restart HA to pick up changes if docker compose version &> /dev/null; then DOCKER_COMPOSE_CMD="docker compose" elif command -v docker-compose &> /dev/null; then DOCKER_COMPOSE_CMD="docker-compose" else echo "❌ Error: docker compose not found" exit 1 fi # Determine compose files to use (check if override exists) COMPOSE_FILES="-f docker-compose.test.yml" if [ -f "docker-compose.test.local.yml" ]; then COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.test.local.yml" fi # If service path is specified, rebuild and restart the service if [ -n "$SERVICE_PATH" ]; then echo "🔨 Rebuilding service from local source: $SERVICE_PATH" if [ ! -f "docker-compose.test.local.yml" ]; then write_service_compose_override fi COMPOSE_FILES="$COMPOSE_FILES -f docker-compose.test.local.yml" # Rebuild service - Docker's cache will detect source file changes automatically # Only use --no-cache if explicitly needed (it's much slower) echo " Building service image (Docker will detect code changes automatically)..." if $DOCKER_COMPOSE_CMD $COMPOSE_FILES build bom-local-service; then echo " ✅ Service rebuilt successfully" echo "🔄 Restarting service container..." if $DOCKER_COMPOSE_CMD $COMPOSE_FILES up -d --no-deps bom-local-service; then echo " ✅ Service restarted with latest code" else echo " ❌ Error: Failed to restart service container" echo " Check logs: docker logs bom-card-test-service" exit 1 fi else echo " ❌ Error: Service rebuild failed" echo " Check the build output above for details" exit 1 fi fi echo "🔄 Restarting Home Assistant to load updated card..." $DOCKER_COMPOSE_CMD $COMPOSE_FILES restart homeassistant # Check and restart the service if it's not running (it may have crashed) SERVICE_STATUS=$($DOCKER_COMPOSE_CMD $COMPOSE_FILES ps bom-local-service 2>/dev/null | grep -c "Up" || echo "0") if [ "$SERVICE_STATUS" = "0" ]; then echo "🔄 Restarting BOM service (was not running)..." $DOCKER_COMPOSE_CMD $COMPOSE_FILES restart bom-local-service || \ $DOCKER_COMPOSE_CMD $COMPOSE_FILES up -d bom-local-service # Wait a moment and check if service started sleep 3 if ! curl -s -f "http://localhost:${SERVICE_HOST_PORT}/api/radar/Brisbane/QLD" > /dev/null 2>&1; then echo " ⚠️ Warning: BOM service may not be responding correctly" echo " Check logs: docker logs bom-card-test-service" else echo " ✅ Service is responding" fi fi echo "⏳ Waiting for Home Assistant to restart..." sleep 5 MAX_WAIT=60 WAIT_COUNT=0 while [ $WAIT_COUNT -lt $MAX_WAIT ]; do if curl -s -f "http://localhost:${HA_HOST_PORT}" > /dev/null 2>&1; then echo "✅ Home Assistant restarted with updated card!" break fi sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2)) done echo "" echo "✅ Card updated! Refresh your browser (Ctrl+F5) to see changes." exit 0 fi # Normal mode - full setup (containers not running) echo "🚀 Setting up fresh test environment..." echo "🐳 Stopping any existing containers..." # Remove stale override so cold start always matches SERVICE_PATH handling below rm -f docker-compose.test.local.yml # Base compose file is enough to stop this project's containers (override was just removed) COMPOSE_FILES_CLEAN="-f docker-compose.test.yml" docker compose $COMPOSE_FILES_CLEAN down 2>/dev/null || docker-compose $COMPOSE_FILES_CLEAN down 2>/dev/null || true # Preserve .storage directory to keep user accounts and HA state # Only create it if it doesn't exist echo "📦 Ensuring test directories exist..." mkdir -p test-ha/config/.storage mkdir -p test-ha/config/www mkdir -p test-ha/cache # Ensure directories exist (Docker will create files as current user due to user: setting in compose) # Integration is mounted directly, skipping redundant copy echo "📋 Integration is mounted directly." ensure_test_ha_config # Copy configuration files to HA config directory echo "📋 Copying HA configuration files..." for config_file in configuration.yaml ui-lovelace.yaml; do sync_test_ha_file "$config_file" done # Create onboarding bypass - skip everything EXCEPT user creation # This way HA will prompt for user creation but skip other setup steps # Only create if it doesn't exist (preserve existing state) if [ ! -f "test-ha/config/.storage/onboarding" ]; then echo "📝 Creating onboarding bypass (first time setup)..." mkdir -p test-ha/config/.storage cat > test-ha/config/.storage/onboarding < /dev/null; then DOCKER_COMPOSE_CMD="docker compose" elif command -v docker-compose &> /dev/null; then DOCKER_COMPOSE_CMD="docker-compose" else echo "❌ Error: docker compose or docker-compose not found" exit 1 fi # Handle service build - either from local source or pre-built image DOCKER_COMPOSE_FILES="-f docker-compose.test.yml" if [ -n "$SERVICE_PATH" ]; then echo "🔨 Building service from local source: $SERVICE_PATH" write_service_compose_override DOCKER_COMPOSE_FILES="$DOCKER_COMPOSE_FILES -f docker-compose.test.local.yml" # Build service - Docker's cache will detect source file changes automatically # The Dockerfile is structured to cache dependencies, so only changed source files trigger rebuilds # This is much faster (~10-20s) than --no-cache (~100s) when only code changes echo " Building service image (Docker will detect code changes automatically)..." if ! $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES build bom-local-service; then echo " ❌ Error: Service build failed" echo " Check the build output above for details" exit 1 fi echo " ✅ Service built successfully" else echo "🐳 Pulling latest service image..." if ! docker pull ghcr.io/alexhopeoconnor/bom-local-service:latest; then echo " ⚠️ Warning: Failed to pull service image, will use cached version if available" else echo " ✅ Service image pulled successfully" fi fi echo "🐳 Starting test environment..." echo " - BOM Local Service: http://localhost:${SERVICE_HOST_PORT}" echo " - Home Assistant: http://localhost:${HA_HOST_PORT}" echo "" echo " On first access to HA, you'll be prompted to create a user account." echo " This is a one-time setup - the account persists across restarts." echo "" echo " The service may take a minute to initialize on first run" echo "" # Start containers (use override file if building from local source) $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES up -d --build || { echo "❌ Error: Failed to start containers" cleanup exit 1 } # Wait for containers to be running echo "⏳ Waiting for containers to start..." sleep 3 # Check if containers are running if ! $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES ps | grep -q "Up"; then echo " ⚠️ Warning: Some containers may not be running. Check logs:" echo " $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES logs" fi # Wait for HA to be ready (check if port is responding) echo "⏳ Waiting for Home Assistant to be ready..." MAX_WAIT=120 WAIT_COUNT=0 while [ $WAIT_COUNT -lt $MAX_WAIT ]; do if curl -s -f "http://localhost:${HA_HOST_PORT}" > /dev/null 2>&1; then echo "✅ Home Assistant is ready!" break fi sleep 2 WAIT_COUNT=$((WAIT_COUNT + 2)) if [ $((WAIT_COUNT % 10)) -eq 0 ]; then echo " Still waiting... (${WAIT_COUNT}s / ${MAX_WAIT}s)" fi done if [ $WAIT_COUNT -ge $MAX_WAIT ]; then echo " ⚠️ Warning: Home Assistant may not be fully ready yet" echo " Check logs: $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES logs homeassistant" fi echo "" echo "✅ Test environment started!" echo "" echo "📋 Next steps:" echo " 1. Open http://localhost:${HA_HOST_PORT} in your browser" echo " 2. Create a user account (first time only)" echo " 3. The test dashboard should be available with your custom card" echo " 4. Check browser console (F12) for any errors" echo "" echo "🔧 Useful commands:" echo " View logs: $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES logs -f" echo " Stop: $DOCKER_COMPOSE_CMD $DOCKER_COMPOSE_FILES down" echo " Clean restart: ./run.sh clean && ./scripts/test.sh" echo " Force rebuild: ./scripts/test.sh --force-build" echo " Docker build: ./scripts/test.sh --docker-build" echo " NPM build: ./scripts/test.sh --npm-build" if [ -n "$SERVICE_PATH" ]; then echo " Local service: ./scripts/test.sh --service-path $SERVICE_PATH" fi echo " Help: ./scripts/test.sh --help" echo ""