Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ Each demo has automated smoke tests that verify the gateway starts and the REST

```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)
./tests/smoke_test.sh # Sensor diagnostics (full API coverage + fault injection + beacons)
./tests/smoke_test_turtlebot3.sh # TurtleBot3 (discovery, data, operations, scripts, triggers, logs)
./tests/smoke_test_moveit.sh # MoveIt pick-and-place (discovery, data, operations, scripts, triggers, logs)
```

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).
Expand Down
197 changes: 195 additions & 2 deletions tests/smoke_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -144,12 +144,23 @@ test_entity_discovery() {
local entity_type="$1"
shift
local entity_ids=("$@")
local expected_count=${#entity_ids[@]}
local label
label="${entity_type^}"

section "Entity Discovery - ${label}"

if api_get "/${entity_type}"; then
# Verify exact count - catches duplicate/synthetic entities leaking through
local actual_count
actual_count=$(echo "$RESPONSE" | jq '.items | length')
if [ "$actual_count" = "$expected_count" ]; then
pass "${entity_type} count is ${expected_count}"
else
local actual_ids
actual_ids=$(echo "$RESPONSE" | jq -r '[.items[].id] | sort | join(", ")')
fail "${entity_type} count is ${expected_count}" "got ${actual_count}: ${actual_ids}"
fi
for entity_id in "${entity_ids[@]}"; do
if echo "$RESPONSE" | items_contain_id "$entity_id"; then
pass "${entity_type} contains '${entity_id}'"
Expand All @@ -162,8 +173,190 @@ test_entity_discovery() {
fi
}

# Print test summary and exit with appropriate code
# Test that procfs introspection data is available for an app
# Usage: assert_procfs_introspection "lidar-sim"
assert_procfs_introspection() {
local app_id="$1"
local endpoint="/apps/${app_id}/x-medkit-procfs"
if api_get "$endpoint"; then
if echo "$RESPONSE" | jq -e '.pid' > /dev/null 2>&1; then
pass "GET ${endpoint} returns procfs data with pid"
else
fail "GET ${endpoint} returns procfs data with pid" "pid field missing"
fi
else
fail "GET ${endpoint} returns 200" "unexpected status code"
fi
}

# Test that scripts are listed for a component
# Usage: assert_scripts_list "compute-unit" "run-diagnostics"
assert_scripts_list() {
local component_id="$1"
local expected_script="$2"
local endpoint="/components/${component_id}/scripts"
if api_get "$endpoint"; then
if echo "$RESPONSE" | jq -e '.items | length > 0' > /dev/null 2>&1; then
pass "GET ${endpoint} returns non-empty items"
else
fail "GET ${endpoint} returns non-empty items" "items is empty"
fi
if echo "$RESPONSE" | jq -e --arg s "$expected_script" '.items[] | select(.id == $s)' > /dev/null 2>&1; then
pass "scripts contains '${expected_script}'"
else
fail "scripts contains '${expected_script}'" "not found in response"
fi
else
fail "GET ${endpoint} returns 200" "unexpected status code"
fi
}

# Execute a script and verify it completes
# Usage: assert_script_execution "compute-unit" "run-diagnostics" [max_wait]
assert_script_execution() {
local component_id="$1"
local script_id="$2"
local max_wait="${3:-30}"
local exec_endpoint="/components/${component_id}/scripts/${script_id}/executions"

# Start execution
local exec_response
exec_response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${exec_endpoint}" \
-H "Content-Type: application/json" \
-d '{"execution_type": "now"}' 2>/dev/null) || true
local exec_http
exec_http=$(echo "$exec_response" | tail -1)
local exec_body
exec_body=$(echo "$exec_response" | sed '$d')

if [ "$exec_http" != "201" ] && [ "$exec_http" != "200" ] && [ "$exec_http" != "202" ]; then
fail "POST ${exec_endpoint} starts execution" "got HTTP ${exec_http}"
return
fi
pass "POST ${exec_endpoint} starts execution"

local exec_id
exec_id=$(echo "$exec_body" | jq -r '.id')
if [ -z "$exec_id" ] || [ "$exec_id" = "null" ]; then
fail "script execution returns valid id" "id is null or empty"
return
fi
pass "script execution returns valid id"

# Poll until completed
echo " Waiting for script '${script_id}' to complete (max ${max_wait}s)..."
local elapsed=0
while [ $elapsed -lt "$max_wait" ]; do
if api_get "${exec_endpoint}/${exec_id}"; then
local status
status=$(echo "$RESPONSE" | jq -r '.status')
case "$status" in
completed)
pass "script '${script_id}' completed successfully"
return
;;
failed|terminated)
fail "script '${script_id}' completed successfully" "status: ${status}"
return
;;
esac
fi
sleep 1
elapsed=$((elapsed + 1))
done
fail "script '${script_id}' completed successfully" "timed out after ${max_wait}s"
}

# Test trigger CRUD lifecycle on an entity
# Usage: assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/faults"
assert_triggers_crud() {
local entity_type="$1"
local entity_id="$2"
local resource_uri="$3"
local triggers_endpoint="/${entity_type}/${entity_id}/triggers"

# Create trigger
echo " Creating OnChange trigger on ${entity_type}/${entity_id}..."
local payload
payload=$(jq -n --arg resource "$resource_uri" \
'{resource: $resource, trigger_condition: {condition_type: "OnChange"}, multishot: true, lifetime: 60}')
local create_response
create_response=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}${triggers_endpoint}" \
-H "Content-Type: application/json" \
-d "$payload" 2>/dev/null) || true

local create_http
create_http=$(echo "$create_response" | tail -1)
local create_body
create_body=$(echo "$create_response" | sed '$d')

if [ "$create_http" = "201" ]; then
pass "POST ${triggers_endpoint} returns 201"
else
fail "POST ${triggers_endpoint} returns 201" "got HTTP ${create_http}"
return
fi

local trigger_id
trigger_id=$(echo "$create_body" | jq -r '.id')
if [ -n "$trigger_id" ] && [ "$trigger_id" != "null" ]; then
pass "trigger response contains valid id"
else
fail "trigger response contains valid id" "id is null or empty"
return
fi

local trigger_status
trigger_status=$(echo "$create_body" | jq -r '.status')
if [ "$trigger_status" = "active" ]; then
pass "trigger status is 'active'"
else
fail "trigger status is 'active'" "got '${trigger_status}'"
fi

# List triggers - verify it appears
if api_get "${triggers_endpoint}"; then
if echo "$RESPONSE" | jq -e --arg id "$trigger_id" '.items[] | select(.id == $id)' > /dev/null 2>&1; then
pass "GET ${triggers_endpoint} lists created trigger"
else
fail "GET ${triggers_endpoint} lists created trigger" "trigger ${trigger_id} not found"
fi
else
fail "GET ${triggers_endpoint} returns 200" "unexpected status code"
fi

# Delete trigger
local delete_status
delete_status=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
"${API_BASE}${triggers_endpoint}/${trigger_id}" 2>/dev/null) || true

if [ "$delete_status" = "204" ]; then
pass "DELETE trigger ${trigger_id} returns 204"
else
fail "DELETE trigger ${trigger_id} returns 204" "got HTTP ${delete_status}"
fi

# Verify trigger is gone
if api_get "${triggers_endpoint}"; then
if ! echo "$RESPONSE" | jq -e --arg id "$trigger_id" '.items[] | select(.id == $id)' > /dev/null 2>&1; then
pass "trigger no longer listed after deletion"
else
fail "trigger no longer listed after deletion" "still found in list"
fi
else
fail "GET ${triggers_endpoint} returns 200 after delete" "unexpected status code"
fi
}

# Print test summary (called via EXIT trap - do not call exit here)
SUMMARY_PRINTED=false
print_summary() {
# Guard against double-printing when called as both trap and explicit call
if [ "$SUMMARY_PRINTED" = true ]; then
return
fi
SUMMARY_PRINTED=true

echo ""
echo -e "${BLUE}================================${NC}"
local total=$((PASS_COUNT + FAIL_COUNT))
Expand All @@ -172,7 +365,7 @@ print_summary() {
if [ "$FAIL_COUNT" -gt 0 ]; then
echo -e "\n ${RED}Failed tests:${FAILED_TESTS}${NC}"
echo -e "${BLUE}================================${NC}"
exit 1
return
fi

echo -e "${BLUE}================================${NC}"
Expand Down
130 changes: 68 additions & 62 deletions tests/smoke_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=tests/smoke_lib.sh
source "${SCRIPT_DIR}/smoke_lib.sh"

trap print_summary EXIT

# --- Wait for gateway startup ---

wait_for_gateway 90
Expand All @@ -31,9 +33,18 @@ 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
test_entity_discovery "areas" sensors processing diagnostics bridge
test_entity_discovery "components" lidar-unit imu-unit gps-unit camera-unit compute-unit gateway fault-manager diagnostic-bridge-unit
test_entity_discovery "apps" lidar-sim imu-sim gps-sim camera-sim anomaly-detector medkit-gateway medkit-fault-manager diagnostic-bridge
test_entity_discovery "functions" sensor-monitoring anomaly-detection fault-management

section "Discovery Relationships"

assert_non_empty_items "/areas/sensors/components"

section "Linux Introspection"

assert_procfs_introspection "lidar-sim"

section "Data Access"

Expand All @@ -49,6 +60,30 @@ else
fail "configurations contains 'noise_stddev' parameter" "not found in response"
fi

section "Operations"

# fault_manager services may take extra time to be discovered via runtime graph introspection
echo " Waiting for fault-manager operations to appear (max 30s)..."
if poll_until "/apps/medkit-fault-manager/operations" '.items | length > 0' 30; then
pass "GET /apps/medkit-fault-manager/operations returns non-empty items"
else
fail "GET /apps/medkit-fault-manager/operations returns non-empty items" "items still empty after 30s"
fi

section "Scripts"

assert_scripts_list "compute-unit" "run-diagnostics"
assert_script_execution "compute-unit" "run-diagnostics" 30

section "Bulk Data"

# Bulk data endpoint should return 200 with categories list (may be empty without faults)
if api_get "/apps/diagnostic-bridge/bulk-data"; then
pass "GET /apps/diagnostic-bridge/bulk-data returns 200"
else
fail "GET /apps/diagnostic-bridge/bulk-data returns 200" "unexpected status code"
fi

section "Logs"

assert_non_empty_items "/apps/medkit-gateway/logs"
Expand Down Expand Up @@ -116,67 +151,38 @@ fi

section "Triggers"

# Create a trigger on diagnostic-bridge faults
echo " Creating OnChange fault trigger..."
TRIGGER_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "${API_BASE}/apps/diagnostic-bridge/triggers" \
-H "Content-Type: application/json" \
-d '{"resource":"/api/v1/apps/diagnostic-bridge/faults","trigger_condition":{"condition_type":"OnChange"},"multishot":true,"lifetime":60}' 2>/dev/null) || true

TRIGGER_HTTP=$(echo "$TRIGGER_RESPONSE" | tail -1)
TRIGGER_BODY=$(echo "$TRIGGER_RESPONSE" | sed '$d')

if [ "$TRIGGER_HTTP" = "201" ]; then
pass "POST /apps/diagnostic-bridge/triggers returns 201"
else
fail "POST /apps/diagnostic-bridge/triggers returns 201" "got HTTP $TRIGGER_HTTP"
fi

TRIGGER_ID=$(echo "$TRIGGER_BODY" | jq -r '.id')
if [ -n "$TRIGGER_ID" ] && [ "$TRIGGER_ID" != "null" ]; then
pass "trigger response contains valid id"
else
fail "trigger response contains valid id" "id is null or empty"
fi

TRIGGER_STATUS=$(echo "$TRIGGER_BODY" | jq -r '.status')
if [ "$TRIGGER_STATUS" = "active" ]; then
pass "trigger status is 'active'"
else
fail "trigger status is 'active'" "got '$TRIGGER_STATUS'"
fi

# List triggers - verify it appears
if api_get "/apps/diagnostic-bridge/triggers"; then
if echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then
pass "GET /apps/diagnostic-bridge/triggers lists created trigger"
else
fail "GET /apps/diagnostic-bridge/triggers lists created trigger" "trigger $TRIGGER_ID not found"
fi
else
fail "GET /apps/diagnostic-bridge/triggers returns 200" "unexpected status code"
fi

# Delete trigger
TRIGGER_DELETE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
"${API_BASE}/apps/diagnostic-bridge/triggers/${TRIGGER_ID}" 2>/dev/null) || true

if [ "$TRIGGER_DELETE_STATUS" = "204" ]; then
pass "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204"
else
fail "DELETE /apps/diagnostic-bridge/triggers/$TRIGGER_ID returns 204" "got HTTP $TRIGGER_DELETE_STATUS"
fi

# Verify trigger is gone
if api_get "/apps/diagnostic-bridge/triggers"; then
if ! echo "$RESPONSE" | jq -e --arg id "$TRIGGER_ID" '.items[] | select(.id == $id)' > /dev/null 2>&1; then
pass "trigger no longer listed after deletion"
else
fail "trigger no longer listed after deletion" "still found in list"
assert_triggers_crud "apps" "diagnostic-bridge" "/api/v1/apps/diagnostic-bridge/faults"

section "Beacon Discovery"

# Beacon data is exposed at vendor extension endpoints:
# /apps/{id}/x-medkit-topic-beacon (BEACON_MODE=topic)
# /apps/{id}/x-medkit-param-beacon (BEACON_MODE=param)
# When BEACON_MODE=none (CI default), these endpoints return 404.
beacon_found=false
for beacon_type in topic-beacon param-beacon; do
if api_get "/apps/lidar-sim/x-medkit-${beacon_type}"; then
beacon_found=true
pass "GET /apps/lidar-sim/x-medkit-${beacon_type} returns 200"
if echo "$RESPONSE" | jq -e '.status' > /dev/null 2>&1; then
pass "beacon response contains 'status' field"
else
fail "beacon response contains 'status' field" "field missing"
fi
if echo "$RESPONSE" | jq -e '.entity_id' > /dev/null 2>&1; then
pass "beacon response contains 'entity_id' field"
else
fail "beacon response contains 'entity_id' field" "field missing"
fi
break
fi
else
fail "GET /apps/diagnostic-bridge/triggers returns 200 after delete" "unexpected status code"
done
if [ "$beacon_found" = false ]; then
# Not a failure - beacons are optional depending on BEACON_MODE
echo -e " ${BLUE}SKIP${NC} beacon not active (BEACON_MODE=none or plugin not loaded)"
fi

# --- Summary ---

print_summary
# print_summary runs via EXIT trap; exit code reflects test results
[ "$FAIL_COUNT" -eq 0 ]
Loading
Loading