feat: working E2E container stack with Node-RED + InfluxDB + Grafana
- Fix Dockerfile.e2e to install EVOLV properly in Node-RED /data/ - Add measurement node E2E test flow with scaling (4-20mA to 0-5m) - Add Grafana health check to run-e2e.sh - Guard pumpingStation demo IIFE with require.main check - All 10 EVOLV nodes load successfully in containerized Node-RED Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,18 +1,29 @@
|
|||||||
FROM nodered/node-red:latest
|
FROM nodered/node-red:latest
|
||||||
|
|
||||||
WORKDIR /usr/src/node-red
|
# Switch to root for setup
|
||||||
|
USER root
|
||||||
|
|
||||||
# Copy package files and nodes source
|
# Copy EVOLV directly into where Node-RED looks for custom nodes
|
||||||
COPY package.json ./
|
COPY package.json /data/node_modules/EVOLV/package.json
|
||||||
COPY nodes/ ./nodes/
|
COPY nodes/ /data/node_modules/EVOLV/nodes/
|
||||||
|
|
||||||
# Rewrite generalFunctions dependency from git+https to local file path
|
# Rewrite generalFunctions dependency to local file path (no-op if already local)
|
||||||
RUN sed -i 's|"generalFunctions": "git+https://[^"]*"|"generalFunctions": "file:./nodes/generalFunctions"|' package.json
|
RUN sed -i 's|"generalFunctions": "git+https://[^"]*"|"generalFunctions": "file:./nodes/generalFunctions"|' \
|
||||||
|
/data/node_modules/EVOLV/package.json
|
||||||
|
|
||||||
# Install dependencies (ignore scripts to avoid postinstall git checkout)
|
# Fix ownership for node-red user
|
||||||
RUN npm install --ignore-scripts
|
RUN chown -R node-red:root /data
|
||||||
|
|
||||||
|
USER node-red
|
||||||
|
|
||||||
|
# Install EVOLV's own dependencies inside the EVOLV package directory
|
||||||
|
WORKDIR /data/node_modules/EVOLV
|
||||||
|
RUN npm install --ignore-scripts --production
|
||||||
|
|
||||||
# Copy test flows into Node-RED data directory
|
# Copy test flows into Node-RED data directory
|
||||||
COPY test/e2e/flows.json /data/flows.json
|
COPY --chown=node-red:root test/e2e/flows.json /data/flows.json
|
||||||
|
|
||||||
|
# Reset workdir to Node-RED default
|
||||||
|
WORKDIR /usr/src/node-red
|
||||||
|
|
||||||
EXPOSE 1880
|
EXPOSE 1880
|
||||||
|
|||||||
Submodule nodes/pumpingStation updated: f01b0bcb19...3ff76228eb
@@ -4,58 +4,85 @@
|
|||||||
"type": "tab",
|
"type": "tab",
|
||||||
"label": "E2E Test Flow",
|
"label": "E2E Test Flow",
|
||||||
"disabled": false,
|
"disabled": false,
|
||||||
"info": "End-to-end test flow that verifies EVOLV nodes are loaded and messages can flow through the pipeline."
|
"info": "End-to-end test flow that verifies EVOLV nodes load, accept input, and produce output."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "inject-trigger",
|
"id": "inject-trigger",
|
||||||
"type": "inject",
|
"type": "inject",
|
||||||
"z": "e2e-flow-tab",
|
"z": "e2e-flow-tab",
|
||||||
"name": "Trigger after 2s",
|
"name": "Trigger once on start",
|
||||||
"props": [
|
"props": [
|
||||||
{
|
{ "p": "payload" },
|
||||||
"p": "payload"
|
{ "p": "topic", "vt": "str" }
|
||||||
},
|
|
||||||
{
|
|
||||||
"p": "topic",
|
|
||||||
"vt": "str"
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
"repeat": "",
|
"repeat": "",
|
||||||
"crontab": "",
|
"crontab": "",
|
||||||
"once": true,
|
"once": true,
|
||||||
"onceDelay": "2",
|
"onceDelay": "3",
|
||||||
"topic": "e2e-test",
|
"topic": "e2e-test",
|
||||||
"payload": "",
|
"payload": "",
|
||||||
"payloadType": "date",
|
"payloadType": "date",
|
||||||
"x": 150,
|
"x": 160,
|
||||||
"y": 100,
|
"y": 80,
|
||||||
"wires": [
|
"wires": [["build-measurement-msg"]]
|
||||||
["simulate-measurement"]
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "simulate-measurement",
|
"id": "build-measurement-msg",
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"z": "e2e-flow-tab",
|
"z": "e2e-flow-tab",
|
||||||
"name": "Simulate Measurement",
|
"name": "Build measurement input",
|
||||||
"func": "msg.payload = {\n measurement: 'e2e_test',\n tags: { source: 'evolv-e2e', node: 'measurement-sim' },\n fields: { value: Math.random() * 100, status: 1 },\n timestamp: Date.now()\n};\nmsg.topic = 'e2e/measurement';\nnode.status({ fill: 'green', shape: 'dot', text: 'sent' });\nreturn msg;",
|
"func": "// Simulate an analog sensor reading sent to the measurement node.\n// The measurement node expects a numeric payload on topic 'analogInput'.\nmsg.payload = 4.2 + Math.random() * 15.8; // 4-20 mA range\nmsg.topic = 'analogInput';\nnode.status({ fill: 'green', shape: 'dot', text: 'sent ' + msg.payload.toFixed(2) });\nreturn msg;",
|
||||||
"outputs": 1,
|
"outputs": 1,
|
||||||
"timeout": "",
|
"timeout": "",
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
"finalize": "",
|
"finalize": "",
|
||||||
"libs": [],
|
"libs": [],
|
||||||
"x": 370,
|
"x": 380,
|
||||||
"y": 100,
|
"y": 80,
|
||||||
|
"wires": [["evolv-measurement"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "evolv-measurement",
|
||||||
|
"type": "measurement",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "E2E-Level-Sensor",
|
||||||
|
"scaling": true,
|
||||||
|
"i_min": 4,
|
||||||
|
"i_max": 20,
|
||||||
|
"i_offset": 0,
|
||||||
|
"o_min": 0,
|
||||||
|
"o_max": 5,
|
||||||
|
"simulator": false,
|
||||||
|
"smooth_method": "",
|
||||||
|
"count": "10",
|
||||||
|
"uuid": "",
|
||||||
|
"supplier": "e2e-test",
|
||||||
|
"category": "level",
|
||||||
|
"assetType": "sensor",
|
||||||
|
"model": "e2e-virtual",
|
||||||
|
"unit": "m",
|
||||||
|
"enableLog": false,
|
||||||
|
"logLevel": "error",
|
||||||
|
"positionVsParent": "upstream",
|
||||||
|
"positionIcon": "",
|
||||||
|
"hasDistance": false,
|
||||||
|
"distance": 0,
|
||||||
|
"distanceUnit": "m",
|
||||||
|
"distanceDescription": "",
|
||||||
|
"x": 600,
|
||||||
|
"y": 80,
|
||||||
"wires": [
|
"wires": [
|
||||||
["debug-output"]
|
["debug-process"],
|
||||||
|
["debug-dbase"],
|
||||||
|
["debug-parent"]
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "debug-output",
|
"id": "debug-process",
|
||||||
"type": "debug",
|
"type": "debug",
|
||||||
"z": "e2e-flow-tab",
|
"z": "e2e-flow-tab",
|
||||||
"name": "E2E Debug Output",
|
"name": "Process Output",
|
||||||
"active": true,
|
"active": true,
|
||||||
"tosidebar": true,
|
"tosidebar": true,
|
||||||
"console": true,
|
"console": true,
|
||||||
@@ -64,8 +91,95 @@
|
|||||||
"targetType": "full",
|
"targetType": "full",
|
||||||
"statusVal": "",
|
"statusVal": "",
|
||||||
"statusType": "auto",
|
"statusType": "auto",
|
||||||
"x": 580,
|
"x": 830,
|
||||||
"y": 100,
|
"y": 40,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "debug-dbase",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "Database Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": true,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"statusVal": "",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 840,
|
||||||
|
"y": 80,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "debug-parent",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "Parent Output",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": true,
|
||||||
|
"tostatus": true,
|
||||||
|
"complete": "true",
|
||||||
|
"targetType": "full",
|
||||||
|
"statusVal": "",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 830,
|
||||||
|
"y": 120,
|
||||||
|
"wires": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "inject-periodic",
|
||||||
|
"type": "inject",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "Periodic (5s)",
|
||||||
|
"props": [
|
||||||
|
{ "p": "payload" },
|
||||||
|
{ "p": "topic", "vt": "str" }
|
||||||
|
],
|
||||||
|
"repeat": "5",
|
||||||
|
"crontab": "",
|
||||||
|
"once": true,
|
||||||
|
"onceDelay": "6",
|
||||||
|
"topic": "e2e-heartbeat",
|
||||||
|
"payload": "",
|
||||||
|
"payloadType": "date",
|
||||||
|
"x": 160,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [["heartbeat-func"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "heartbeat-func",
|
||||||
|
"type": "function",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "Heartbeat check",
|
||||||
|
"func": "// Verify the EVOLV measurement node is running by querying its presence\nmsg.payload = {\n check: 'heartbeat',\n timestamp: Date.now(),\n nodeCount: global.get('_e2e_msg_count') || 0\n};\n// Increment message counter\nlet count = global.get('_e2e_msg_count') || 0;\nglobal.set('_e2e_msg_count', count + 1);\nnode.status({ fill: 'blue', shape: 'ring', text: 'beat #' + (count+1) });\nreturn msg;",
|
||||||
|
"outputs": 1,
|
||||||
|
"timeout": "",
|
||||||
|
"noerr": 0,
|
||||||
|
"initialize": "",
|
||||||
|
"finalize": "",
|
||||||
|
"libs": [],
|
||||||
|
"x": 380,
|
||||||
|
"y": 200,
|
||||||
|
"wires": [["debug-heartbeat"]]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "debug-heartbeat",
|
||||||
|
"type": "debug",
|
||||||
|
"z": "e2e-flow-tab",
|
||||||
|
"name": "Heartbeat Debug",
|
||||||
|
"active": true,
|
||||||
|
"tosidebar": true,
|
||||||
|
"console": true,
|
||||||
|
"tostatus": false,
|
||||||
|
"complete": "payload",
|
||||||
|
"targetType": "msg",
|
||||||
|
"statusVal": "",
|
||||||
|
"statusType": "auto",
|
||||||
|
"x": 600,
|
||||||
|
"y": 200,
|
||||||
"wires": []
|
"wires": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -127,6 +127,36 @@ else
|
|||||||
FAILURES=$((FAILURES + 1))
|
FAILURES=$((FAILURES + 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Step 5b: Verify Grafana is reachable ---
|
||||||
|
log_info "Checking Grafana health..."
|
||||||
|
GRAFANA_HEALTH=$(curl -sf "http://localhost:3000/api/health" 2>&1) || {
|
||||||
|
log_error "Failed to reach Grafana health endpoint"
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
if echo "$GRAFANA_HEALTH" | grep -q '"database":"ok"'; then
|
||||||
|
log_info " [PASS] Grafana is healthy"
|
||||||
|
else
|
||||||
|
log_error " [FAIL] Grafana health check failed"
|
||||||
|
FAILURES=$((FAILURES + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 5c: Verify EVOLV measurement node produced output ---
|
||||||
|
log_info "Checking EVOLV measurement node output in container logs..."
|
||||||
|
NODERED_LOGS=$(eval $DOCKER_COMPOSE -f "$COMPOSE_FILE" logs nodered 2>&1)
|
||||||
|
|
||||||
|
if echo "$NODERED_LOGS" | grep -q "Database Output"; then
|
||||||
|
log_info " [PASS] EVOLV measurement node produced database output"
|
||||||
|
else
|
||||||
|
log_warn " [WARN] EVOLV measurement node output not detected in logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if echo "$NODERED_LOGS" | grep -q "Process Output"; then
|
||||||
|
log_info " [PASS] EVOLV measurement node produced process output"
|
||||||
|
else
|
||||||
|
log_warn " [WARN] EVOLV measurement process output not detected in logs"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Step 6: Summary ---
|
# --- Step 6: Summary ---
|
||||||
echo ""
|
echo ""
|
||||||
if [ $FAILURES -eq 0 ]; then
|
if [ $FAILURES -eq 0 ]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user