diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c58a2a..c011f90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: YAMLLINT_CONFIG="{extends: relaxed, rules: {line-length: {max: 120}}}" find . \( -name "*.yaml" -o -name "*.yml" \) -type f | xargs yamllint -d "$YAMLLINT_CONFIG" - docker-build: + build-and-test-sensor: runs-on: ubuntu-24.04 steps: - name: Show triggering source @@ -54,20 +54,65 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Build and start sensor_diagnostics demo + working-directory: demos/sensor_diagnostics + run: docker compose --profile ci up -d --build sensor-demo-ci - - name: Build Sensor Diagnostics demo image - run: | - cd demos/sensor_diagnostics - docker build -t sensor-diagnostics-demo:test -f Dockerfile . + - name: Run smoke tests + run: ./tests/smoke_test.sh - - name: Build TurtleBot3 demo image - run: | - cd demos/turtlebot3_integration - docker build -t turtlebot3-medkit-demo:test -f Dockerfile . + - name: Show container logs on failure + if: failure() + working-directory: demos/sensor_diagnostics + run: docker compose --profile ci logs sensor-demo-ci --tail=200 - - name: Build MoveIt Pick-and-Place demo image - run: | - cd demos/moveit_pick_place - docker build -t moveit-pick-place-demo:test -f Dockerfile . + - name: Teardown + if: always() + working-directory: demos/sensor_diagnostics + run: docker compose --profile ci down + + build-and-test-turtlebot: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and start turtlebot3 demo + working-directory: demos/turtlebot3_integration + run: docker compose --profile ci up -d --build turtlebot3-demo-ci + + - name: Run smoke tests + run: ./tests/smoke_test_turtlebot3.sh + + - name: Show container logs on failure + if: failure() + working-directory: demos/turtlebot3_integration + run: docker compose --profile ci logs turtlebot3-demo-ci --tail=200 + + - name: Teardown + if: always() + working-directory: demos/turtlebot3_integration + run: docker compose --profile ci down + + build-and-test-moveit: + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and start moveit demo + working-directory: demos/moveit_pick_place + run: docker compose --profile ci up -d --build moveit-demo-ci + + - name: Run smoke tests + run: ./tests/smoke_test_moveit.sh + + - name: Show container logs on failure + if: failure() + working-directory: demos/moveit_pick_place + run: docker compose --profile ci logs moveit-demo-ci --tail=200 + + - name: Teardown + if: always() + working-directory: demos/moveit_pick_place + run: docker compose --profile ci down diff --git a/README.md b/README.md index 91e9bde..9813666 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,19 @@ curl http://localhost:8080/api/v1/faults | jq See individual demo READMEs for more examples. +## Testing + +Each demo has automated smoke tests that verify the gateway starts and the REST API works correctly: + +```bash +# Run smoke tests against a running demo (default: http://localhost:8080) +./tests/smoke_test.sh # Sensor diagnostics (21 tests, incl. fault injection) +./tests/smoke_test_turtlebot3.sh # TurtleBot3 (entity discovery) +./tests/smoke_test_moveit.sh # MoveIt pick-and-place (entity discovery) +``` + +CI runs all 3 demos in parallel - each job builds the Docker image, starts the container, and runs the smoke tests against it. See [CI workflow](.github/workflows/ci.yml). + ## Related Projects - [ros2_medkit](https://github.com/selfpatch/ros2_medkit) — Core diagnostics library with SOVD-compliant gateway diff --git a/demos/moveit_pick_place/config/medkit_params.yaml b/demos/moveit_pick_place/config/medkit_params.yaml index 5874a79..f72ef9f 100644 --- a/demos/moveit_pick_place/config/medkit_params.yaml +++ b/demos/moveit_pick_place/config/medkit_params.yaml @@ -18,14 +18,11 @@ diagnostics: allow_credentials: false max_age_seconds: 86400 - max_parallel_topic_samples: 10 - # Discovery configuration - discovery_mode: "hybrid" # runtime_only, manifest_only, or hybrid - manifest_path: "" # Will be set via launch argument - manifest_strict_validation: true - discovery: + mode: "hybrid" # runtime_only, manifest_only, or hybrid + manifest_path: "" # Will be set via launch argument + manifest_strict_validation: true runtime: create_synthetic_components: false # Manifest defines components diff --git a/demos/moveit_pick_place/docker-compose.yml b/demos/moveit_pick_place/docker-compose.yml index 69d56c2..482493b 100644 --- a/demos/moveit_pick_place/docker-compose.yml +++ b/demos/moveit_pick_place/docker-compose.yml @@ -62,6 +62,23 @@ services: source /root/demo_ws/install/setup.bash && ros2 launch moveit_medkit_demo $${LAUNCH_FILE} headless:=$${HEADLESS}" + # For CI testing - fake hardware (no Gazebo), tests run externally + moveit-demo-ci: + profiles: ["ci"] + build: + context: . + dockerfile: Dockerfile + container_name: moveit_medkit_demo_ci + environment: + - ROS_DOMAIN_ID=40 + ports: + - "8080:8080" + command: > + bash -c "mkdir -p /var/lib/ros2_medkit/rosbags && + source /opt/ros/jazzy/setup.bash && + source /root/demo_ws/install/setup.bash && + ros2 launch moveit_medkit_demo demo.launch.py headless:=true" + # SOVD Web UI — pre-built from GHCR sovd-web-ui: image: ghcr.io/selfpatch/sovd_web_ui:latest diff --git a/demos/moveit_pick_place/launch/demo.launch.py b/demos/moveit_pick_place/launch/demo.launch.py index 36a66c3..85d4d5c 100644 --- a/demos/moveit_pick_place/launch/demo.launch.py +++ b/demos/moveit_pick_place/launch/demo.launch.py @@ -126,7 +126,7 @@ def generate_launch_description(): medkit_params_file, { "use_sim_time": use_sim_time, - "manifest_path": manifest_file, + "discovery.manifest_path": manifest_file, }, ], ), diff --git a/demos/moveit_pick_place/launch/demo_gazebo.launch.py b/demos/moveit_pick_place/launch/demo_gazebo.launch.py index 3a700f6..5107d38 100644 --- a/demos/moveit_pick_place/launch/demo_gazebo.launch.py +++ b/demos/moveit_pick_place/launch/demo_gazebo.launch.py @@ -470,7 +470,7 @@ def generate_launch_description(): medkit_params_file, { "use_sim_time": True, - "manifest_path": manifest_file, + "discovery.manifest_path": manifest_file, }, ], ), diff --git a/demos/sensor_diagnostics/config/medkit_params.yaml b/demos/sensor_diagnostics/config/medkit_params.yaml index 17d9a78..9bd2eca 100644 --- a/demos/sensor_diagnostics/config/medkit_params.yaml +++ b/demos/sensor_diagnostics/config/medkit_params.yaml @@ -16,14 +16,11 @@ diagnostics: allow_credentials: false max_age_seconds: 86400 - max_parallel_topic_samples: 10 - # Discovery configuration - discovery_mode: "hybrid" # runtime_only, manifest_only, or hybrid - manifest_path: "" # Will be set via launch argument - manifest_strict_validation: true - discovery: + mode: "hybrid" # runtime_only, manifest_only, or hybrid + manifest_path: "" # Will be set via launch argument + manifest_strict_validation: true runtime: create_synthetic_components: false # Manifest defines components diff --git a/demos/sensor_diagnostics/docker-compose.yml b/demos/sensor_diagnostics/docker-compose.yml index 541d864..cc11a54 100644 --- a/demos/sensor_diagnostics/docker-compose.yml +++ b/demos/sensor_diagnostics/docker-compose.yml @@ -28,7 +28,7 @@ services: depends_on: - sensor-demo - # For CI testing - headless mode with quick exit + # For CI testing - headless mode, tests run externally sensor-demo-ci: profiles: ["ci"] build: @@ -43,8 +43,4 @@ services: bash -c "mkdir -p /var/lib/ros2_medkit/rosbags && source /opt/ros/jazzy/setup.bash && source /root/demo_ws/install/setup.bash && - ros2 launch sensor_diagnostics_demo demo.launch.py & - sleep 10 && - curl -sf http://localhost:8080/api/v1/health && - curl -sf http://localhost:8080/api/v1/apps | jq '.items[] | .id' && - echo 'CI validation passed!'" + ros2 launch sensor_diagnostics_demo demo.launch.py" diff --git a/demos/sensor_diagnostics/launch/demo.launch.py b/demos/sensor_diagnostics/launch/demo.launch.py index 4a35590..85a55a4 100644 --- a/demos/sensor_diagnostics/launch/demo.launch.py +++ b/demos/sensor_diagnostics/launch/demo.launch.py @@ -119,7 +119,7 @@ def generate_launch_description(): parameters=[ medkit_params_file, {"use_sim_time": use_sim_time}, - {"manifest_path": manifest_file}, + {"discovery.manifest_path": manifest_file}, ], ), # ===== Fault Manager (at root namespace) ===== diff --git a/demos/turtlebot3_integration/config/medkit_params.yaml b/demos/turtlebot3_integration/config/medkit_params.yaml index 5b7a1f5..fdc605f 100644 --- a/demos/turtlebot3_integration/config/medkit_params.yaml +++ b/demos/turtlebot3_integration/config/medkit_params.yaml @@ -17,14 +17,11 @@ diagnostics: allow_credentials: false max_age_seconds: 86400 - max_parallel_topic_samples: 10 - # Discovery configuration - discovery_mode: "hybrid" # runtime_only, manifest_only, or hybrid - manifest_path: "" # Will be set via launch argument - manifest_strict_validation: true - discovery: + mode: "hybrid" # runtime_only, manifest_only, or hybrid + manifest_path: "" # Will be set via launch argument + manifest_strict_validation: true runtime: create_synthetic_components: false # Manifest defines components diff --git a/demos/turtlebot3_integration/config/medkit_params_debounce.yaml b/demos/turtlebot3_integration/config/medkit_params_debounce.yaml index d931df3..d75eab6 100644 --- a/demos/turtlebot3_integration/config/medkit_params_debounce.yaml +++ b/demos/turtlebot3_integration/config/medkit_params_debounce.yaml @@ -22,14 +22,11 @@ diagnostics: allow_credentials: false max_age_seconds: 86400 - max_parallel_topic_samples: 10 - # Discovery configuration - discovery_mode: "hybrid" # runtime_only, manifest_only, or hybrid - manifest_path: "" # Will be set via launch argument - manifest_strict_validation: true - discovery: + mode: "hybrid" # runtime_only, manifest_only, or hybrid + manifest_path: "" # Will be set via launch argument + manifest_strict_validation: true runtime: create_synthetic_components: false # Manifest defines components diff --git a/demos/turtlebot3_integration/docker-compose.yml b/demos/turtlebot3_integration/docker-compose.yml index e5817f4..c64e6c2 100644 --- a/demos/turtlebot3_integration/docker-compose.yml +++ b/demos/turtlebot3_integration/docker-compose.yml @@ -63,6 +63,25 @@ services: export TURTLEBOT3_MODEL=burger && ros2 launch turtlebot3_medkit_demo demo.launch.py headless:=$${HEADLESS}" + # For CI testing - headless Gazebo, tests run externally + turtlebot3-demo-ci: + profiles: ["ci"] + build: + context: . + dockerfile: Dockerfile + container_name: turtlebot3_medkit_demo_ci + environment: + - TURTLEBOT3_MODEL=burger + - ROS_DOMAIN_ID=30 + ports: + - "8080:8080" + command: > + bash -c "mkdir -p /var/lib/ros2_medkit/rosbags && + source /opt/ros/jazzy/setup.bash && + source /root/demo_ws/install/setup.bash && + export TURTLEBOT3_MODEL=burger && + ros2 launch turtlebot3_medkit_demo demo.launch.py headless:=true" + sovd-web-ui: image: ghcr.io/selfpatch/sovd_web_ui:latest container_name: sovd_web_ui diff --git a/demos/turtlebot3_integration/launch/demo.launch.py b/demos/turtlebot3_integration/launch/demo.launch.py index 1355f4f..7d32d00 100644 --- a/demos/turtlebot3_integration/launch/demo.launch.py +++ b/demos/turtlebot3_integration/launch/demo.launch.py @@ -165,7 +165,7 @@ def generate_launch_description(): medkit_params_file, { "use_sim_time": use_sim_time, - "manifest_path": manifest_file, + "discovery.manifest_path": manifest_file, }, ], ), diff --git a/tests/smoke_lib.sh b/tests/smoke_lib.sh new file mode 100755 index 0000000..ed7270f --- /dev/null +++ b/tests/smoke_lib.sh @@ -0,0 +1,165 @@ +#!/bin/bash +# Shared test infrastructure for smoke tests +# Source this file from individual smoke test scripts +# +# Required: set GATEWAY_URL before sourcing, or pass as $1 to the test script +# Required: set API_BASE="${GATEWAY_URL}/api/v1" + +set -euo pipefail + +# --- Dependency preflight --- + +for cmd in curl jq; do + if ! command -v "$cmd" > /dev/null 2>&1; then + echo "Error: required command '$cmd' not found in PATH" >&2 + exit 1 + fi +done + +# --- Test infrastructure --- + +PASS_COUNT=0 +FAIL_COUNT=0 +FAILED_TESTS="" + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +NC='\033[0m' + +pass() { + PASS_COUNT=$((PASS_COUNT + 1)) + echo -e " ${GREEN}PASS${NC} $1" +} + +fail() { + FAIL_COUNT=$((FAIL_COUNT + 1)) + FAILED_TESTS="${FAILED_TESTS}\\n - $1" + echo -e " ${RED}FAIL${NC} $1" + if [ -n "${2:-}" ]; then + echo -e " ${RED}$2${NC}" + fi +} + +section() { + echo -e "\n${BLUE}--- $1 ---${NC}" +} + +# Helper: GET endpoint, store response in $RESPONSE, check HTTP status +# Usage: api_get "/health" 200 +api_get() { + local endpoint="$1" + local expected_status="${2:-200}" + local http_code + RESPONSE=$(curl -s -w "\n%{http_code}" "${API_BASE}${endpoint}" 2>/dev/null) || true + http_code=$(echo "$RESPONSE" | tail -1) + RESPONSE=$(echo "$RESPONSE" | sed '$d') + if [ "$http_code" != "$expected_status" ]; then + return 1 + fi + return 0 +} + +# Helper: check that JSON array at .items contains an element with .id == value +# Usage: echo "$JSON" | items_contain_id "lidar-sim" +items_contain_id() { + local id="$1" + jq -e --arg id "$id" '.items[] | select(.id == $id)' > /dev/null 2>&1 +} + +# Helper: poll endpoint until jq filter matches (max wait seconds) +# Usage: poll_until "/faults" '.items[] | select(.fault_code == "LIDAR_SIM")' 15 +poll_until() { + local endpoint="$1" + local jq_filter="$2" + local max_wait="${3:-15}" + local elapsed=0 + + while [ $elapsed -lt "$max_wait" ]; do + if api_get "$endpoint" && echo "$RESPONSE" | jq -e "$jq_filter" > /dev/null 2>&1; then + return 0 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + return 1 +} + +# Wait for gateway to become healthy +# Usage: wait_for_gateway [max_wait_seconds] +wait_for_gateway() { + local max_wait="${1:-90}" + section "Waiting for gateway" + echo -e " Polling ${API_BASE}/health (max ${max_wait}s)..." + local elapsed=0 + while [ $elapsed -lt "$max_wait" ]; do + if curl -sf "${API_BASE}/health" > /dev/null 2>&1; then + echo -e " ${GREEN}Gateway ready after ${elapsed}s${NC}" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + echo -e " ${RED}Gateway failed to start within ${max_wait}s${NC}" + exit 1 +} + +# Wait for runtime node linking (manifest entities need runtime refresh to get data) +# Usage: wait_for_runtime_linking "/apps/lidar-sim/data" [max_wait_seconds] +wait_for_runtime_linking() { + local data_endpoint="$1" + local max_wait="${2:-60}" + echo -e " Waiting for runtime node linking (max ${max_wait}s)..." + local elapsed=0 + while [ $elapsed -lt "$max_wait" ]; do + if api_get "$data_endpoint" && echo "$RESPONSE" | jq -e '.items | length > 0' > /dev/null 2>&1; then + echo -e " ${GREEN}Runtime linking complete after ${elapsed}s${NC}" + return 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) + done + echo -e " ${RED}Runtime node linking did not complete within ${max_wait}s${NC}" + exit 1 +} + +# Test entity discovery for a given entity type +# Usage: test_entity_discovery "areas" "sensors processing diagnostics" +test_entity_discovery() { + local entity_type="$1" + shift + local entity_ids=("$@") + local label + label="${entity_type^}" + + section "Entity Discovery - ${label}" + + if api_get "/${entity_type}"; then + for entity_id in "${entity_ids[@]}"; do + if echo "$RESPONSE" | items_contain_id "$entity_id"; then + pass "${entity_type} contains '${entity_id}'" + else + fail "${entity_type} contains '${entity_id}'" "not found in response" + fi + done + else + fail "GET /${entity_type} returns 200" "unexpected status code" + fi +} + +# Print test summary and exit with appropriate code +print_summary() { + echo "" + echo -e "${BLUE}================================${NC}" + local total=$((PASS_COUNT + FAIL_COUNT)) + echo -e " Results: ${GREEN}${PASS_COUNT} passed${NC}, ${RED}${FAIL_COUNT} failed${NC} (${total} total)" + + if [ "$FAIL_COUNT" -gt 0 ]; then + echo -e "\n ${RED}Failed tests:${FAILED_TESTS}${NC}" + echo -e "${BLUE}================================${NC}" + exit 1 + fi + + echo -e "${BLUE}================================${NC}" + echo -e "\n${GREEN}All smoke tests passed!${NC}" +} diff --git a/tests/smoke_test.sh b/tests/smoke_test.sh new file mode 100755 index 0000000..e943579 --- /dev/null +++ b/tests/smoke_test.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# Smoke tests for sensor_diagnostics demo +# Runs from the host against the containerized gateway on localhost:8080 +# +# Usage: ./tests/smoke_test.sh [GATEWAY_URL] +# Default GATEWAY_URL: http://localhost:8080 + +GATEWAY_URL="${1:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=tests/smoke_lib.sh +source "${SCRIPT_DIR}/smoke_lib.sh" + +# --- Wait for gateway startup --- + +wait_for_gateway 90 + +# Wait for entity discovery + runtime linking (nodes need to be linked to manifest apps) +# In hybrid mode, manifest entities appear instantly but data/configurations require +# the runtime refresh cycle to link ROS 2 nodes to manifest apps. +wait_for_runtime_linking "/apps/lidar-sim/data" 60 + +# --- Tests --- + +section "Health" + +if api_get "/health"; then + pass "GET /health returns 200" +else + fail "GET /health returns 200" "unexpected status code" +fi + +test_entity_discovery "areas" sensors processing diagnostics +test_entity_discovery "components" lidar-unit imu-unit gps-unit camera-unit +test_entity_discovery "apps" lidar-sim imu-sim gps-sim camera-sim anomaly-detector + +section "Data Access" + +if api_get "/apps/lidar-sim/data"; then + if echo "$RESPONSE" | jq -e '.items | length > 0' > /dev/null 2>&1; then + pass "GET /apps/lidar-sim/data returns non-empty items" + else + fail "GET /apps/lidar-sim/data returns non-empty items" "items is empty" + fi +else + fail "GET /apps/lidar-sim/data returns 200" "unexpected status code" +fi + +section "Configurations" + +if api_get "/apps/lidar-sim/configurations"; then + if echo "$RESPONSE" | jq -e '.items | length > 0' > /dev/null 2>&1; then + pass "GET /apps/lidar-sim/configurations returns non-empty items" + else + fail "GET /apps/lidar-sim/configurations returns non-empty items" "items is empty" + fi + if echo "$RESPONSE" | jq -e '.items[] | select(.name == "noise_stddev")' > /dev/null 2>&1; then + pass "configurations contains 'noise_stddev' parameter" + else + fail "configurations contains 'noise_stddev' parameter" "not found in response" + fi +else + fail "GET /apps/lidar-sim/configurations returns 200" "unexpected status code" +fi + +section "Fault Injection" + +# Inject noise fault via configuration API +echo " Injecting noise fault (noise_stddev=0.5)..." +INJECT_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ + -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ + -H "Content-Type: application/json" \ + -d '{"value": 0.5}') || true + +if [ -z "$INJECT_STATUS" ]; then + fail "PUT noise_stddev=0.5 returns 200" "request failed (no HTTP status received)" +elif [ "$INJECT_STATUS" = "200" ]; then + pass "PUT noise_stddev=0.5 returns 200" +else + fail "PUT noise_stddev=0.5 returns 200" "got status $INJECT_STATUS" +fi + +# Wait for fault to appear (pipeline: config change -> sensor detects -> /diagnostics -> bridge -> fault_manager) +echo " Waiting for LIDAR_SIM fault to appear (max 30s)..." +if poll_until "/faults" '.items[] | select(.fault_code == "LIDAR_SIM")' 30; then + pass "LIDAR_SIM fault appeared in /faults" +else + fail "LIDAR_SIM fault appeared in /faults" "fault not found after 30s" +fi + +# Check fault detail with environment data +if api_get "/apps/diagnostic-bridge/faults/LIDAR_SIM"; then + pass "GET fault detail returns 200" + if echo "$RESPONSE" | jq -e '.environment_data' > /dev/null 2>&1; then + pass "fault detail contains environment_data" + else + fail "fault detail contains environment_data" "field missing from response" + fi +else + fail "GET fault detail returns 200" "unexpected status code" +fi + +# Cleanup: restore config + delete fault +echo " Cleaning up: restoring config and clearing fault..." +curl -s -X PUT "${API_BASE}/apps/lidar-sim/configurations/noise_stddev" \ + -H "Content-Type: application/json" -d '{"value": 0.01}' > /dev/null || true + +curl -s -X DELETE "${API_BASE}/apps/diagnostic-bridge/faults/LIDAR_SIM" > /dev/null || true + +# Verify fault is cleared (poll to avoid race with fault manager processing the DELETE) +echo " Verifying LIDAR_SIM fault cleared (max 5s)..." +elapsed=0 +cleared=false +while [ $elapsed -lt 5 ]; do + if api_get "/faults" && ! echo "$RESPONSE" | jq -e '.items[] | select(.fault_code == "LIDAR_SIM")' > /dev/null 2>&1; then + pass "LIDAR_SIM fault cleared after cleanup" + cleared=true + break + fi + sleep 1 + elapsed=$((elapsed + 1)) +done +if [ "$cleared" = false ]; then + fail "LIDAR_SIM fault cleared after cleanup" "fault still present after 5s" +fi + +# --- Summary --- + +print_summary diff --git a/tests/smoke_test_moveit.sh b/tests/smoke_test_moveit.sh new file mode 100755 index 0000000..a4a6180 --- /dev/null +++ b/tests/smoke_test_moveit.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Smoke tests for moveit_pick_place demo +# Runs from the host against the containerized gateway on localhost:8080 +# +# Tests: health, entity discovery (areas, components, apps from manifest) +# Uses demo.launch.py (fake hardware, no Gazebo) for CI stability +# +# Usage: ./tests/smoke_test_moveit.sh [GATEWAY_URL] +# Default GATEWAY_URL: http://localhost:8080 + +GATEWAY_URL="${1:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=tests/smoke_lib.sh +source "${SCRIPT_DIR}/smoke_lib.sh" + +# --- Wait for gateway startup --- + +wait_for_gateway 120 + +# Wait for runtime node linking +wait_for_runtime_linking "/apps/medkit-gateway/data" 90 + +# --- Tests --- + +section "Health" + +if api_get "/health"; then + pass "GET /health returns 200" +else + fail "GET /health returns 200" "unexpected status code" +fi + +test_entity_discovery "areas" manipulation planning diagnostics bridge +test_entity_discovery "components" panda-arm panda-gripper moveit-planning pick-place-loop gateway fault-manager diagnostic-bridge +test_entity_discovery "apps" joint-state-broadcaster panda-arm-controller panda-hand-controller robot-state-publisher move-group pick-place-node medkit-gateway medkit-fault-manager diagnostic-bridge-app manipulation-monitor + +# --- Summary --- + +print_summary diff --git a/tests/smoke_test_turtlebot3.sh b/tests/smoke_test_turtlebot3.sh new file mode 100755 index 0000000..e54a3b4 --- /dev/null +++ b/tests/smoke_test_turtlebot3.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Smoke tests for turtlebot3_integration demo +# Runs from the host against the containerized gateway on localhost:8080 +# +# Tests: health, entity discovery (areas, components, apps from manifest) +# No fault injection - Gazebo-based demo is too complex for reliable CI fault testing +# +# Usage: ./tests/smoke_test_turtlebot3.sh [GATEWAY_URL] +# Default GATEWAY_URL: http://localhost:8080 + +GATEWAY_URL="${1:-http://localhost:8080}" +API_BASE="${GATEWAY_URL}/api/v1" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=tests/smoke_lib.sh +source "${SCRIPT_DIR}/smoke_lib.sh" + +# --- Wait for gateway startup --- + +# Turtlebot3 needs Gazebo + Nav2 - allow extra startup time +wait_for_gateway 120 + +# Wait for runtime node linking +wait_for_runtime_linking "/apps/medkit-gateway/data" 90 + +# --- Tests --- + +section "Health" + +if api_get "/health"; then + pass "GET /health returns 200" +else + fail "GET /health returns 200" "unexpected status code" +fi + +test_entity_discovery "areas" robot navigation diagnostics bridge +test_entity_discovery "components" turtlebot3-base lidar-sensor nav2-stack gateway fault-manager diagnostic-bridge-unit +test_entity_discovery "apps" turtlebot3-node robot-state-publisher amcl bt-navigator controller-server planner-server velocity-smoother medkit-gateway medkit-fault-manager diagnostic-bridge anomaly-detector + +# --- Summary --- + +print_summary