diff --git a/scripts/setup-test-environment.sh b/scripts/setup-test-environment.sh new file mode 100755 index 0000000..423b074 --- /dev/null +++ b/scripts/setup-test-environment.sh @@ -0,0 +1,795 @@ +#!/bin/bash + +# Calejo Control Adapter - Test Environment Setup Script +# Sets up mock SCADA and optimizer services for testing + +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 + +# Function to print colored output +print_status() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +print_success() { + echo -e "${GREEN}[SUCCESS]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Function to display usage +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --scada-only Only setup mock SCADA services" + echo " --optimizer-only Only setup mock optimizer services" + echo " --with-dashboard Include test dashboard setup" + echo " --clean Clean up existing test services" + echo " -h, --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Setup complete test environment" + echo " $0 --scada-only # Setup only mock SCADA services" + echo " $0 --clean # Clean up test environment" +} + +# Parse command line arguments +SCADA_ONLY=false +OPTIMIZER_ONLY=false +WITH_DASHBOARD=false +CLEAN=false + +while [[ $# -gt 0 ]]; do + case $1 in + --scada-only) + SCADA_ONLY=true + shift + ;; + --optimizer-only) + OPTIMIZER_ONLY=true + shift + ;; + --with-dashboard) + WITH_DASHBOARD=true + shift + ;; + --clean) + CLEAN=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + print_error "Unknown option: $1" + usage + exit 1 + ;; + esac +done + +# Check if Docker is available +if ! command -v docker &> /dev/null; then + print_error "Docker is not installed or not in PATH" + exit 1 +fi + +if ! command -v docker-compose &> /dev/null; then + print_error "Docker Compose is not installed or not in PATH" + exit 1 +fi + +# Function to cleanup test services +cleanup_test_services() { + print_status "Cleaning up test services..." + + # Stop and remove test containers + docker-compose -f docker-compose.test.yml down --remove-orphans 2>/dev/null || true + + # Remove test network if exists + docker network rm calejo-test-network 2>/dev/null || true + + # Remove test volumes + docker volume rm calejo-scada-data 2>/dev/null || true + docker volume rm calejo-optimizer-data 2>/dev/null || true + + print_success "Test services cleaned up" +} + +# If clean mode, cleanup and exit +if [[ "$CLEAN" == "true" ]]; then + cleanup_test_services + exit 0 +fi + +# Create test docker-compose file +print_status "Creating test environment configuration..." + +cat > docker-compose.test.yml << 'EOF' +version: '3.8' + +services: + # Main Calejo Control Adapter + calejo-control-adapter: + build: + context: . + dockerfile: Dockerfile + container_name: calejo-control-adapter-test + ports: + - "8080:8080" # REST API + - "4840:4840" # OPC UA + - "502:502" # Modbus TCP + - "9090:9090" # Prometheus metrics + environment: + - DATABASE_URL=postgresql://calejo:password@postgres:5432/calejo + - JWT_SECRET_KEY=test-secret-key + - API_KEY=test-api-key + - MOCK_SCADA_ENABLED=true + - MOCK_OPTIMIZER_ENABLED=true + - SCADA_MOCK_URL=http://mock-scada:8081 + - OPTIMIZER_MOCK_URL=http://mock-optimizer:8082 + depends_on: + - postgres + - mock-scada + - mock-optimizer + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + volumes: + - ./logs:/app/logs + - ./config:/app/config + networks: + - calejo-test-network + + # PostgreSQL Database + postgres: + image: postgres:15 + container_name: calejo-postgres-test + environment: + - POSTGRES_DB=calejo + - POSTGRES_USER=calejo + - POSTGRES_PASSWORD=password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./database/init.sql:/docker-entrypoint-initdb.d/init.sql + restart: unless-stopped + networks: + - calejo-test-network + + # Mock SCADA System + mock-scada: + image: python:3.11-slim + container_name: calejo-mock-scada + ports: + - "8081:8081" + working_dir: /app + volumes: + - ./tests/mock_services:/app + command: > + sh -c "pip install flask requests && + python mock_scada_server.py" + environment: + - FLASK_ENV=development + - PORT=8081 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8081/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - calejo-test-network + + # Mock Optimizer Service + mock-optimizer: + image: python:3.11-slim + container_name: calejo-mock-optimizer + ports: + - "8082:8082" + working_dir: /app + volumes: + - ./tests/mock_services:/app + command: > + sh -c "pip install flask requests numpy && + python mock_optimizer_server.py" + environment: + - FLASK_ENV=development + - PORT=8082 + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8082/health"] + interval: 30s + timeout: 10s + retries: 3 + networks: + - calejo-test-network + + # Test Data Generator + test-data-generator: + image: python:3.11-slim + container_name: calejo-test-data-generator + working_dir: /app + volumes: + - ./tests/mock_services:/app + command: > + sh -c "pip install requests && + python test_data_generator.py" + depends_on: + - calejo-control-adapter + - mock-scada + restart: "no" + networks: + - calejo-test-network + +volumes: + postgres_data: + +networks: + calejo-test-network: + driver: bridge +EOF + +print_success "Test configuration created" + +# Create mock services directory +print_status "Creating mock services..." +mkdir -p tests/mock_services + +# Create mock SCADA server +cat > tests/mock_services/mock_scada_server.py << 'EOF' +#!/usr/bin/env python3 +""" +Mock SCADA Server for Testing +Simulates a real SCADA system with industrial process data +""" + +import json +import random +import time +from datetime import datetime +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Mock SCADA data storage +scada_data = { + "temperature": {"value": 75.0, "unit": "ยฐC", "min": 50.0, "max": 100.0}, + "pressure": {"value": 15.2, "unit": "bar", "min": 10.0, "max": 20.0}, + "flow_rate": {"value": 120.5, "unit": "mยณ/h", "min": 80.0, "max": 150.0}, + "level": {"value": 65.3, "unit": "%", "min": 0.0, "max": 100.0}, + "power": {"value": 450.7, "unit": "kW", "min": 300.0, "max": 600.0}, + "status": {"value": "RUNNING", "options": ["STOPPED", "RUNNING", "FAULT"]}, + "efficiency": {"value": 87.5, "unit": "%", "min": 0.0, "max": 100.0} +} + +# Equipment status +equipment_status = { + "pump_1": "RUNNING", + "pump_2": "STOPPED", + "valve_1": "OPEN", + "valve_2": "CLOSED", + "compressor": "RUNNING", + "heater": "ON" +} + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({"status": "healthy", "service": "mock-scada"}) + +@app.route('/api/v1/data', methods=['GET']) +def get_all_data(): + """Get all SCADA data""" + # Simulate data variation + for key in scada_data: + if key != "status": + current = scada_data[key] + variation = random.uniform(-2.0, 2.0) + new_value = current["value"] + variation + # Keep within bounds + new_value = max(current["min"], min(new_value, current["max"])) + scada_data[key]["value"] = round(new_value, 2) + + return jsonify({ + "timestamp": datetime.utcnow().isoformat(), + "data": scada_data, + "equipment": equipment_status + }) + +@app.route('/api/v1/data/', methods=['GET']) +def get_specific_data(tag): + """Get specific SCADA data tag""" + if tag in scada_data: + # Simulate variation for numeric values + if tag != "status": + current = scada_data[tag] + variation = random.uniform(-1.0, 1.0) + new_value = current["value"] + variation + new_value = max(current["min"], min(new_value, current["max"])) + scada_data[tag]["value"] = round(new_value, 2) + + return jsonify({ + "tag": tag, + "value": scada_data[tag]["value"], + "unit": scada_data[tag].get("unit", ""), + "timestamp": datetime.utcnow().isoformat() + }) + else: + return jsonify({"error": "Tag not found"}), 404 + +@app.route('/api/v1/control/', methods=['POST']) +def control_equipment(equipment): + """Control SCADA equipment""" + data = request.get_json() + + if not data or 'command' not in data: + return jsonify({"error": "Missing command"}), 400 + + command = data['command'] + + if equipment in equipment_status: + # Simulate control logic + if command in ["START", "STOP", "OPEN", "CLOSE", "ON", "OFF"]: + old_status = equipment_status[equipment] + equipment_status[equipment] = command + + return jsonify({ + "equipment": equipment, + "previous_status": old_status, + "current_status": command, + "timestamp": datetime.utcnow().isoformat(), + "message": f"Equipment {equipment} changed from {old_status} to {command}" + }) + else: + return jsonify({"error": "Invalid command"}), 400 + else: + return jsonify({"error": "Equipment not found"}), 404 + +@app.route('/api/v1/alarms', methods=['GET']) +def get_alarms(): + """Get current alarms""" + # Simulate occasional alarms + alarms = [] + + # Temperature alarm + if scada_data["temperature"]["value"] > 90: + alarms.append({ + "id": "TEMP_HIGH", + "message": "High temperature alarm", + "severity": "HIGH", + "timestamp": datetime.utcnow().isoformat() + }) + + # Pressure alarm + if scada_data["pressure"]["value"] > 18: + alarms.append({ + "id": "PRESS_HIGH", + "message": "High pressure alarm", + "severity": "MEDIUM", + "timestamp": datetime.utcnow().isoformat() + }) + + return jsonify({"alarms": alarms}) + +if __name__ == '__main__': + import os + port = int(os.getenv('PORT', 8081)) + app.run(host='0.0.0.0', port=port, debug=True) +EOF + +print_success "Mock SCADA server created" + +# Create mock optimizer server +cat > tests/mock_services/mock_optimizer_server.py << 'EOF' +#!/usr/bin/env python3 +""" +Mock Optimizer Server for Testing +Simulates an optimization service for industrial processes +""" + +import json +import random +import numpy as np +from datetime import datetime, timedelta +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Mock optimization models +optimization_models = { + "energy_optimization": { + "name": "Energy Consumption Optimizer", + "description": "Optimizes energy usage across processes", + "parameters": ["power_load", "time_of_day", "production_rate"] + }, + "production_optimization": { + "name": "Production Efficiency Optimizer", + "description": "Maximizes production efficiency", + "parameters": ["raw_material_quality", "machine_utilization", "operator_skill"] + }, + "cost_optimization": { + "name": "Cost Reduction Optimizer", + "description": "Minimizes operational costs", + "parameters": ["energy_cost", "labor_cost", "maintenance_cost"] + } +} + +# Optimization history +optimization_history = [] + +@app.route('/health', methods=['GET']) +def health(): + """Health check endpoint""" + return jsonify({"status": "healthy", "service": "mock-optimizer"}) + +@app.route('/api/v1/models', methods=['GET']) +def get_models(): + """Get available optimization models""" + return jsonify({"models": optimization_models}) + +@app.route('/api/v1/optimize/', methods=['POST']) +def optimize(model_name): + """Run optimization for a specific model""" + data = request.get_json() + + if not data: + return jsonify({"error": "No input data provided"}), 400 + + if model_name not in optimization_models: + return jsonify({"error": "Model not found"}), 404 + + # Simulate optimization processing + processing_time = random.uniform(0.5, 3.0) + + # Generate optimization results + if model_name == "energy_optimization": + result = { + "optimal_power_setpoint": random.uniform(400, 500), + "recommended_actions": [ + "Reduce compressor load during peak hours", + "Optimize pump sequencing", + "Adjust temperature setpoints" + ], + "estimated_savings": random.uniform(5, 15), + "confidence": random.uniform(0.7, 0.95) + } + elif model_name == "production_optimization": + result = { + "optimal_production_rate": random.uniform(80, 120), + "recommended_actions": [ + "Adjust raw material mix", + "Optimize machine speeds", + "Improve operator scheduling" + ], + "efficiency_gain": random.uniform(3, 12), + "confidence": random.uniform(0.75, 0.92) + } + elif model_name == "cost_optimization": + result = { + "optimal_cost_structure": { + "energy": random.uniform(40, 60), + "labor": random.uniform(25, 35), + "maintenance": random.uniform(10, 20) + }, + "recommended_actions": [ + "Shift energy consumption to off-peak", + "Optimize maintenance schedules", + "Improve labor allocation" + ], + "cost_reduction": random.uniform(8, 20), + "confidence": random.uniform(0.8, 0.98) + } + + # Record optimization + optimization_record = { + "model": model_name, + "timestamp": datetime.utcnow().isoformat(), + "input_data": data, + "result": result, + "processing_time": processing_time + } + optimization_history.append(optimization_record) + + return jsonify({ + "optimization_id": len(optimization_history), + "model": model_name, + "result": result, + "processing_time": processing_time, + "timestamp": datetime.utcnow().isoformat() + }) + +@app.route('/api/v1/history', methods=['GET']) +def get_history(): + """Get optimization history""" + limit = request.args.get('limit', 10, type=int) + return jsonify({ + "history": optimization_history[-limit:], + "total_optimizations": len(optimization_history) + }) + +@app.route('/api/v1/forecast', methods=['POST']) +def forecast(): + """Generate forecasts based on current data""" + data = request.get_json() + + if not data or 'hours' not in data: + return jsonify({"error": "Missing forecast hours"}), 400 + + hours = data['hours'] + + # Generate mock forecast + forecast_data = [] + current_time = datetime.utcnow() + + for i in range(hours): + forecast_time = current_time + timedelta(hours=i) + forecast_data.append({ + "timestamp": forecast_time.isoformat(), + "energy_consumption": random.uniform(400, 600), + "production_rate": random.uniform(85, 115), + "efficiency": random.uniform(80, 95), + "cost": random.uniform(45, 65) + }) + + return jsonify({ + "forecast": forecast_data, + "generated_at": current_time.isoformat(), + "horizon_hours": hours + }) + +if __name__ == '__main__': + import os + port = int(os.getenv('PORT', 8082)) + app.run(host='0.0.0.0', port=port, debug=True) +EOF + +print_success "Mock optimizer server created" + +# Create test data generator +cat > tests/mock_services/test_data_generator.py << 'EOF' +#!/usr/bin/env python3 +""" +Test Data Generator +Generates realistic test data for the Calejo Control Adapter +""" + +import requests +import time +import random +import json +from datetime import datetime + +# Configuration +CALEJO_API_URL = "http://calejo-control-adapter-test:8080" +SCADA_MOCK_URL = "http://mock-scada:8081" +OPTIMIZER_MOCK_URL = "http://mock-optimizer:8082" + +# Test scenarios +test_scenarios = [ + "normal_operation", + "high_load", + "low_efficiency", + "alarm_condition", + "optimization_test" +] + +def test_health_checks(): + """Test health of all services""" + print("๐Ÿ” Testing service health...") + + services = [ + ("Calejo Control Adapter", f"{CALEJO_API_URL}/health"), + ("Mock SCADA", f"{SCADA_MOCK_URL}/health"), + ("Mock Optimizer", f"{OPTIMIZER_MOCK_URL}/health") + ] + + for service_name, url in services: + try: + response = requests.get(url, timeout=5) + if response.status_code == 200: + print(f" โœ… {service_name}: Healthy") + else: + print(f" โŒ {service_name}: Unhealthy (Status: {response.status_code})") + except Exception as e: + print(f" โŒ {service_name}: Connection failed - {e}") + +def generate_scada_data(): + """Generate and send SCADA data""" + print("๐Ÿ“Š Generating SCADA test data...") + + try: + # Get current SCADA data + response = requests.get(f"{SCADA_MOCK_URL}/api/v1/data") + if response.status_code == 200: + data = response.json() + print(f" ๐Ÿ“ˆ Current SCADA data: {len(data.get('data', {}))} tags") + + # Send some control commands + equipment_to_control = ["pump_1", "valve_1", "compressor"] + for equipment in equipment_to_control: + command = random.choice(["START", "STOP", "OPEN", "CLOSE"]) + try: + control_response = requests.post( + f"{SCADA_MOCK_URL}/api/v1/control/{equipment}", + json={"command": command}, + timeout=5 + ) + if control_response.status_code == 200: + print(f" ๐ŸŽ›๏ธ Controlled {equipment}: {command}") + except: + pass + + except Exception as e: + print(f" โŒ SCADA data generation failed: {e}") + +def test_optimization(): + """Test optimization scenarios""" + print("๐Ÿง  Testing optimization...") + + try: + # Get available models + response = requests.get(f"{OPTIMIZER_MOCK_URL}/api/v1/models") + if response.status_code == 200: + models = response.json().get('models', {}) + + # Test each model + for model_name in models: + test_data = { + "power_load": random.uniform(400, 600), + "time_of_day": random.randint(0, 23), + "production_rate": random.uniform(80, 120) + } + + opt_response = requests.post( + f"{OPTIMIZER_MOCK_URL}/api/v1/optimize/{model_name}", + json=test_data, + timeout=10 + ) + + if opt_response.status_code == 200: + result = opt_response.json() + print(f" โœ… {model_name}: Optimization completed") + print(f" Processing time: {result.get('processing_time', 0):.2f}s") + else: + print(f" โŒ {model_name}: Optimization failed") + + except Exception as e: + print(f" โŒ Optimization test failed: {e}") + +def test_calejo_api(): + """Test Calejo Control Adapter API""" + print("๐ŸŒ Testing Calejo API...") + + endpoints = [ + "/health", + "/dashboard", + "/api/v1/status", + "/api/v1/metrics" + ] + + for endpoint in endpoints: + try: + response = requests.get(f"{CALEJO_API_URL}{endpoint}", timeout=5) + if response.status_code == 200: + print(f" โœ… {endpoint}: Accessible") + else: + print(f" โš ๏ธ {endpoint}: Status {response.status_code}") + except Exception as e: + print(f" โŒ {endpoint}: Failed - {e}") + +def run_comprehensive_test(): + """Run comprehensive test scenario""" + print("\n๐Ÿš€ Starting comprehensive test scenario...") + print("=" * 50) + + # Test all components + test_health_checks() + print() + + generate_scada_data() + print() + + test_optimization() + print() + + test_calejo_api() + print() + + print("โœ… Comprehensive test completed!") + print("\n๐Ÿ“‹ Test Summary:") + print(" โ€ข Service health checks") + print(" โ€ข SCADA data generation and control") + print(" โ€ข Optimization model testing") + print(" โ€ข Calejo API endpoint validation") + +if __name__ == "__main__": + # Wait a bit for services to start + print("โณ Waiting for services to initialize...") + time.sleep(10) + + run_comprehensive_test() +EOF + +print_success "Test data generator created" + +# Start test services +print_status "Starting test services..." + +docker-compose -f docker-compose.test.yml up -d + +# Wait for services to start +print_status "Waiting for services to initialize..." +sleep 30 + +# Run health checks +print_status "Running health checks..." + +# Check Calejo Control Adapter +if curl -f http://localhost:8080/health > /dev/null 2>&1; then + print_success "Calejo Control Adapter is healthy" +else + print_error "Calejo Control Adapter health check failed" +fi + +# Check Mock SCADA +if curl -f http://localhost:8081/health > /dev/null 2>&1; then + print_success "Mock SCADA is healthy" +else + print_error "Mock SCADA health check failed" +fi + +# Check Mock Optimizer +if curl -f http://localhost:8082/health > /dev/null 2>&1; then + print_success "Mock Optimizer is healthy" +else + print_error "Mock Optimizer health check failed" +fi + +# Run test data generator +print_status "Running test data generator..." +docker-compose -f docker-compose.test.yml run --rm test-data-generator + +print_success "Test environment setup completed!" + +# Display access information +print "" +echo "==================================================" +echo " TEST ENVIRONMENT READY" +echo "==================================================" +echo "" +echo "๐ŸŒ Access URLs:" +echo " Calejo Dashboard: http://localhost:8080/dashboard" +echo " Mock SCADA API: http://localhost:8081/api/v1/data" +echo " Mock Optimizer API: http://localhost:8082/api/v1/models" +echo " PostgreSQL: localhost:5432" +echo "" +echo "๐Ÿ”ง Management Commands:" +echo " View logs: docker-compose -f docker-compose.test.yml logs -f" +echo " Stop services: docker-compose -f docker-compose.test.yml down" +echo " Cleanup: ./scripts/setup-test-environment.sh --clean" +echo "" +echo "๐Ÿงช Test Commands:" +echo " Run tests: python -m pytest tests/" +echo " Generate data: docker-compose -f docker-compose.test.yml run --rm test-data-generator" +echo "" +echo "==================================================" \ No newline at end of file diff --git a/scripts/test-mock-services.sh b/scripts/test-mock-services.sh new file mode 100755 index 0000000..acd3b8e --- /dev/null +++ b/scripts/test-mock-services.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Quick test for mock SCADA and optimizer services + +set -e + +echo "๐Ÿงช Testing Mock Services..." +echo "" + +# Test Mock SCADA +echo "๐Ÿ“Š Testing Mock SCADA..." +if curl -s http://localhost:8081/health | grep -q "healthy"; then + echo "โœ… Mock SCADA is healthy" + + # Get SCADA data + echo " Fetching SCADA data..." + curl -s http://localhost:8081/api/v1/data | jq '.data | keys' 2>/dev/null || echo " SCADA data available" +else + echo "โŒ Mock SCADA is not responding" +fi + +echo "" + +# Test Mock Optimizer +echo "๐Ÿง  Testing Mock Optimizer..." +if curl -s http://localhost:8082/health | grep -q "healthy"; then + echo "โœ… Mock Optimizer is healthy" + + # Get available models + echo " Fetching optimization models..." + curl -s http://localhost:8082/api/v1/models | jq '.models | keys' 2>/dev/null || echo " Optimization models available" +else + echo "โŒ Mock Optimizer is not responding" +fi + +echo "" + +# Test Calejo Control Adapter +echo "๐ŸŒ Testing Calejo Control Adapter..." +if curl -s http://localhost:8080/health | grep -q "healthy"; then + echo "โœ… Calejo Control Adapter is healthy" + + # Test dashboard + echo " Testing dashboard access..." + curl -s -I http://localhost:8080/dashboard | head -1 | grep -q "200" && echo " Dashboard accessible" || echo " Dashboard status check" +else + echo "โŒ Calejo Control Adapter is not responding" +fi + +echo "" +echo "โœ… Mock services test completed!" \ No newline at end of file