diff --git a/.env b/.env index 17351e62..2ea7d15b 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -TAG=0.0.5 +TAG=0.0.6 diff --git a/README.md b/README.md index 935c6bd7..2b5d53c4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ The use cases supported by the three demos are summarized in conceptual block di - 🚨 AC Charging ⚡: `curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-ac.sh | bash` - 🚨 ISO 15118 DC Charging ⚡: `curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-iso15118-2-dc.sh | bash` - 🚨 Two EVSE Charging ⚡: `curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-two-evse.sh | bash` + - 🚨 E2E Automated Tests ⚡: `curl https://raw.githubusercontent.com/everest/everest-demo/main/demo-automated-testing.sh | bash` ### STEP 2: Interact with the demo - Open the `nodered` flows to understand the module flows at http://127.0.0.1:1880 diff --git a/demo-automated-testing.sh b/demo-automated-testing.sh new file mode 100755 index 00000000..54cd3f17 --- /dev/null +++ b/demo-automated-testing.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +DEMO_COMPOSE_FILE_NAME='docker-compose.automated-tests.yml' +DEMO_DIR="$(mktemp -d)" + +delete_temporary_directory() { rm -rf "${DEMO_DIR}"; } +trap delete_temporary_directory EXIT + +if [[ ! "${DEMO_DIR}" || ! -d "${DEMO_DIR}" ]]; then + echo 'Error: Failed to create a temporary directory for the demo.' + exit 1 +fi + +download_demo_file() { + local -r repo_file_path="$1" + local -r repo_raw_url='https://raw.githubusercontent.com/everest/everest-demo/main' + local -r destination_path="${DEMO_DIR}/${repo_file_path}" + + mkdir -p "$(dirname ${destination_path})" + curl -s -o "${destination_path}" "${repo_raw_url}/${repo_file_path}" + if [[ "$?" != 0 ]]; then + echo "Error: Failed to retrieve \"${repo_file_path}\" from the demo" + echo 'repository. If this issue persists, please report this as an' + echo 'issue in the EVerest project:' + echo ' https://github.com/EVerest/EVerest/issues' + exit 1 + fi +} + +download_demo_file "${DEMO_COMPOSE_FILE_NAME}" +download_demo_file .env + +docker compose --project-name everest-ac-demo \ + --file "${DEMO_DIR}/${DEMO_COMPOSE_FILE_NAME}" up diff --git a/docker-compose.automated-tests.yml b/docker-compose.automated-tests.yml new file mode 100644 index 00000000..c81dc2e0 --- /dev/null +++ b/docker-compose.automated-tests.yml @@ -0,0 +1,18 @@ +version: "3.6" + +services: + mqtt-server: + image: ghcr.io/us-joet/everest-demo/mqtt-server:${TAG} + logging: + driver: none + + manager: + image: ghcr.io/us-joet/everest-demo/manager:${TAG} + depends_on: + - mqtt-server + environment: + - MQTT_SERVER_ADDRESS=mqtt-server + working_dir: /ext/source/tests + entrypoint: "sh ./run-test.sh" + sysctls: + - net.ipv6.conf.all.disable_ipv6=0 diff --git a/manager/Dockerfile b/manager/Dockerfile index 194d4b3b..e9c5d167 100644 --- a/manager/Dockerfile +++ b/manager/Dockerfile @@ -18,9 +18,12 @@ RUN git clone https://github.com/EVerest/everest-core.git \ # Don't run the test-and-install script since it deletes the build directory! && /entrypoint.sh run-script install -# Copy over the custom config *after* the compile +# Copy over the custom config *after* compilation and installation COPY config-docker.json ./dist/share/everest/modules/OCPP/config-docker.json -COPY user-config/ /ext/source/config/user-config/ +# TODO: This should be removed once added to everest-core +COPY ./tests/startup_tests.py /ext/source/tests/core_tests/startup_tests.py + +COPY run-test.sh /ext/source/tests/run-test.sh LABEL org.opencontainers.image.source=https://github.com/everest/everest-demo diff --git a/manager/run-test.sh b/manager/run-test.sh new file mode 100644 index 00000000..701fd084 --- /dev/null +++ b/manager/run-test.sh @@ -0,0 +1,3 @@ +#! /bin/sh + +pytest --everest-prefix /workspace/dist core_tests/startup_tests.py \ No newline at end of file diff --git a/manager/tests/startup_tests.py b/manager/tests/startup_tests.py new file mode 100644 index 00000000..1341b8a7 --- /dev/null +++ b/manager/tests/startup_tests.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2023 Contributors to EVerest + +import logging +import pytest +import queue +import threading +import time + +from everest.framework import Module, RuntimeSession +from everest.testing.core_utils.everest_core import EverestCore, Requirement +from everest.testing.core_utils.fixtures import * + + +class ProbeModule: + def __init__(self, session: RuntimeSession): + m = Module("probe", session) + self._setup = m.say_hello() + + # subscribe to session events + evse_manager_ff = self._setup.connections["connector_1"][0] + m.subscribe_variable( + evse_manager_ff, "session_event", self._handle_evse_manager_event + ) + + self._msg_queue = queue.Queue() + + self._ready_event = threading.Event() + self._mod = m + m.init_done(self._ready) + + def _ready(self): + self._ready_event.set() + + def _handle_evse_manager_event(self, args): + self._msg_queue.put(args) + + def _parse_event_dict(self, expected_events: dict, args: dict) -> bool: + keys = expected_events[0].keys() + if len(keys) == 1: + key = list(keys)[0] + if key == "error": + expected_error_code = expected_events[0][key] + error_code = args[key]["error_code"] + return expected_error_code == error_code + else: + logging.error(f"Key: {key} not supported") + else: + logging.warning("Not supporting multiple keys") + + def _pool_for_expected_events( + self, expected_events: list[str], end_of_time: float + ) -> bool: + while len(expected_events) > 0: + time_left = end_of_time - time.time() + + if time_left < 0: + return False + try: + args = self._msg_queue.get(timeout=time_left) + if type(expected_events[0]) is str: + if expected_events[0] == args["event"]: + expected_events.pop(0) + elif type(expected_events[0]) is dict: + if self._parse_event_dict(expected_events, args): + expected_events.pop(0) + except queue.Empty: + return False + + return True + + def test( + self, charging_session_cmd: dict, expected_events: list[str], timeout: float + ) -> bool: + end_of_time = time.time() + timeout + + if not self._ready_event.wait(timeout): + return False + + # fetch fulfillment + car_sim_ff = self._setup.connections["test_control"][0] + + # enable simulator + self._mod.call_command(car_sim_ff, "enable", {"value": True}) + + # start charging simulation + logging.info(charging_session_cmd["value"]) + self._mod.call_command( + car_sim_ff, "executeChargingSession", charging_session_cmd + ) + + return self._pool_for_expected_events(expected_events, end_of_time) + + +@pytest.mark.asyncio +async def test_000_startup_check(everest_core: EverestCore): + logging.info(">>>>>>>>> test_000_startup_check <<<<<<<<<") + everest_core.start() + + +@pytest.mark.everest_core_config("config-sil.yaml") +@pytest.mark.asyncio +async def test_001_start_test_module(everest_core: EverestCore): + logging.info(">>>>>>>>> test_001_start_test_module <<<<<<<<<") + + test_connections = { + "test_control": [Requirement("car_simulator", "main")], + "connector_1": [Requirement("connector_1", "evse")], + } + + everest_core.start(standalone_module="probe", test_connections=test_connections) + logging.info("everest-core ready, waiting for probe module") + + session = RuntimeSession( + str(everest_core.prefix_path), str(everest_core.everest_config_path) + ) + + probe = ProbeModule(session) + + if everest_core.status_listener.wait_for_status(10, ["ALL_MODULES_STARTED"]): + everest_core.all_modules_started_event.set() + logging.info("set all modules started event...") + charging_session_cmd = { + "value": "sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 5" + } + expected_events = ["TransactionStarted", "ChargingStarted"] + + assert probe.test(charging_session_cmd, expected_events, 20) + + +@pytest.mark.asyncio +async def test_000_demo_run(everest_core: EverestCore): + logging.info(">>>>>>>>> test_000_demo_run <<<<<<<<<") + + test_connections = { + "test_control": [Requirement("car_simulator", "main")], + "connector_1": [Requirement("connector_1", "evse")], + } + + everest_core.start(standalone_module="probe", test_connections=test_connections) + + logging.info("everest-core ready, waiting for probe module") + + session = RuntimeSession( + str(everest_core.prefix_path), str(everest_core.everest_config_path) + ) + + probe = ProbeModule(session) + + if everest_core.status_listener.wait_for_status(10, ["ALL_MODULES_STARTED"]): + everest_core.all_modules_started_event.set() + logging.info("set all modules started event...") + + charging_session_cmd = { + "value": "sleep 1;iec_wait_pwr_ready;sleep 1;draw_power_regulated 16,3;sleep 5;pause;sleep 5;diode_fail;sleep 5" + } + expected_events = [ + "TransactionStarted", + "ChargingStarted", + "ChargingPausedEV", + {"error": "CarDiodeFault"}, + ] + + assert probe.test(charging_session_cmd, expected_events, 20) diff --git a/nodered/Dockerfile b/nodered/Dockerfile index 6e748b6b..d77a46a8 100644 --- a/nodered/Dockerfile +++ b/nodered/Dockerfile @@ -4,6 +4,6 @@ RUN npm install node-red-contrib-ui-actions RUN npm install node-red-node-ui-table RUN npm install node-red-contrib-ui-level -COPY config /config +COPY --chown=node-red:root config /config LABEL org.opencontainers.image.source=https://github.com/everest/everest-demo