diff --git a/Dockerfile.e2e b/Dockerfile.e2e index ec703b6..3d7ff86 100644 --- a/Dockerfile.e2e +++ b/Dockerfile.e2e @@ -1,18 +1,29 @@ FROM nodered/node-red:latest -WORKDIR /usr/src/node-red +# Switch to root for setup +USER root -# Copy package files and nodes source -COPY package.json ./ -COPY nodes/ ./nodes/ +# Copy EVOLV directly into where Node-RED looks for custom nodes +COPY package.json /data/node_modules/EVOLV/package.json +COPY nodes/ /data/node_modules/EVOLV/nodes/ -# Rewrite generalFunctions dependency from git+https to local file path -RUN sed -i 's|"generalFunctions": "git+https://[^"]*"|"generalFunctions": "file:./nodes/generalFunctions"|' package.json +# Rewrite generalFunctions dependency to local file path (no-op if already local) +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) -RUN npm install --ignore-scripts +# Fix ownership for node-red user +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/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 diff --git a/nodes/pumpingStation b/nodes/pumpingStation index f01b0bc..3ff7622 160000 --- a/nodes/pumpingStation +++ b/nodes/pumpingStation @@ -1 +1 @@ -Subproject commit f01b0bcb19d83523bcb1bac111bbbdb49b5c18f7 +Subproject commit 3ff76228eb61b5e485132c719aa2e8036721c9c1 diff --git a/test/e2e/flows.json b/test/e2e/flows.json index 94a55e0..e8e7088 100644 --- a/test/e2e/flows.json +++ b/test/e2e/flows.json @@ -4,58 +4,85 @@ "type": "tab", "label": "E2E Test Flow", "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", "type": "inject", "z": "e2e-flow-tab", - "name": "Trigger after 2s", + "name": "Trigger once on start", "props": [ - { - "p": "payload" - }, - { - "p": "topic", - "vt": "str" - } + { "p": "payload" }, + { "p": "topic", "vt": "str" } ], "repeat": "", "crontab": "", "once": true, - "onceDelay": "2", + "onceDelay": "3", "topic": "e2e-test", "payload": "", "payloadType": "date", - "x": 150, - "y": 100, - "wires": [ - ["simulate-measurement"] - ] + "x": 160, + "y": 80, + "wires": [["build-measurement-msg"]] }, { - "id": "simulate-measurement", + "id": "build-measurement-msg", "type": "function", "z": "e2e-flow-tab", - "name": "Simulate Measurement", - "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;", + "name": "Build measurement input", + "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, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], - "x": 370, - "y": 100, + "x": 380, + "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": [ - ["debug-output"] + ["debug-process"], + ["debug-dbase"], + ["debug-parent"] ] }, { - "id": "debug-output", + "id": "debug-process", "type": "debug", "z": "e2e-flow-tab", - "name": "E2E Debug Output", + "name": "Process Output", "active": true, "tosidebar": true, "console": true, @@ -64,8 +91,95 @@ "targetType": "full", "statusVal": "", "statusType": "auto", - "x": 580, - "y": 100, + "x": 830, + "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": [] } ] diff --git a/test/e2e/run-e2e.sh b/test/e2e/run-e2e.sh index 94cb5b3..de2035a 100755 --- a/test/e2e/run-e2e.sh +++ b/test/e2e/run-e2e.sh @@ -127,6 +127,36 @@ else FAILURES=$((FAILURES + 1)) 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 --- echo "" if [ $FAILURES -eq 0 ]; then