From a8ac948bc11eac354126318d83b05929477f674b Mon Sep 17 00:00:00 2001 From: Coury Richards Date: Fri, 27 Oct 2023 10:23:01 -0400 Subject: [PATCH 1/4] Add automated tests demo and revise Docker registry Signed-off-by: Daniel Moore <9156191+drmrd@users.noreply.github.com> --- README.md | 1 + demo-automated-testing.sh | 34 ++++++++++++++++++++++++++++++ docker-compose.automated-tests.yml | 18 ++++++++++++++++ manager/Dockerfile | 4 +++- manager/run-test.sh | 3 +++ nodered/Dockerfile | 2 +- 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100755 demo-automated-testing.sh create mode 100644 docker-compose.automated-tests.yml create mode 100644 manager/run-test.sh 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..840d6ae5 --- /dev/null +++ b/docker-compose.automated-tests.yml @@ -0,0 +1,18 @@ +version: "3.6" + +services: + mqtt-server: + image: ghcr.io/everest/everest-demo/mqtt-server:0.0.1 + logging: + driver: none + + manager: + image: ghcr.io/everest/everest-demo/manager:0.0.2 + 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..acc98f0f 100644 --- a/manager/Dockerfile +++ b/manager/Dockerfile @@ -18,9 +18,11 @@ 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/ +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/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 From 381a3cec855339cd6332cc751c0f1e9594101276 Mon Sep 17 00:00:00 2001 From: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Date: Wed, 22 Nov 2023 11:22:32 -0500 Subject: [PATCH 2/4] Added test file with updated parsing Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Signed-off-by: Daniel Moore <9156191+drmrd@users.noreply.github.com> --- docker-compose.automated-tests.yml | 4 +- manager/Dockerfile | 3 +- manager/tests/startup_tests.py | 146 +++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 manager/tests/startup_tests.py diff --git a/docker-compose.automated-tests.yml b/docker-compose.automated-tests.yml index 840d6ae5..c81dc2e0 100644 --- a/docker-compose.automated-tests.yml +++ b/docker-compose.automated-tests.yml @@ -2,12 +2,12 @@ version: "3.6" services: mqtt-server: - image: ghcr.io/everest/everest-demo/mqtt-server:0.0.1 + image: ghcr.io/us-joet/everest-demo/mqtt-server:${TAG} logging: driver: none manager: - image: ghcr.io/everest/everest-demo/manager:0.0.2 + image: ghcr.io/us-joet/everest-demo/manager:${TAG} depends_on: - mqtt-server environment: diff --git a/manager/Dockerfile b/manager/Dockerfile index acc98f0f..e9c5d167 100644 --- a/manager/Dockerfile +++ b/manager/Dockerfile @@ -21,7 +21,8 @@ RUN git clone https://github.com/EVerest/everest-core.git \ # 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 diff --git a/manager/tests/startup_tests.py b/manager/tests/startup_tests.py new file mode 100644 index 00000000..c1bdd4e6 --- /dev/null +++ b/manager/tests/startup_tests.py @@ -0,0 +1,146 @@ +#!/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) From 4594ba7b3cf1cdcbc3fdd945fa93e801124c0bdc Mon Sep 17 00:00:00 2001 From: Daniel Moore <9156191+drmrd@users.noreply.github.com> Date: Tue, 5 Dec 2023 20:56:37 -0500 Subject: [PATCH 3/4] Update Docker version tag Signed-off-by: Daniel Moore <9156191+drmrd@users.noreply.github.com> --- .env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env b/.env index 17351e62..2ea7d15b 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -TAG=0.0.5 +TAG=0.0.6 From 954b5d2dc44237e1bdf93eae3e5c1004fe268526 Mon Sep 17 00:00:00 2001 From: Daniel Moore <9156191+drmrd@users.noreply.github.com> Date: Tue, 5 Dec 2023 21:07:22 -0500 Subject: [PATCH 4/4] Clean up startup_tests.py formatting Signed-off-by: Daniel Moore <9156191+drmrd@users.noreply.github.com> --- manager/tests/startup_tests.py | 77 +++++++++++++++++++++------------- 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/manager/tests/startup_tests.py b/manager/tests/startup_tests.py index c1bdd4e6..1341b8a7 100644 --- a/manager/tests/startup_tests.py +++ b/manager/tests/startup_tests.py @@ -15,13 +15,14 @@ class ProbeModule: def __init__(self, session: RuntimeSession): - m = Module('probe', session) + 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) + 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() @@ -39,16 +40,18 @@ 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': + if key == "error": expected_error_code = expected_events[0][key] - error_code = args[key]['error_code'] + error_code = args[key]["error_code"] return expected_error_code == error_code else: - logging.error(f'Key: {key} not supported') + 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: + 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() @@ -57,7 +60,7 @@ def _pool_for_expected_events(self, expected_events: list[str], end_of_time: flo try: args = self._msg_queue.get(timeout=time_left) if type(expected_events[0]) is str: - if expected_events[0] == args['event']: + 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): @@ -67,22 +70,25 @@ def _pool_for_expected_events(self, expected_events: list[str], end_of_time: flo return True - def test(self, charging_session_cmd: dict, expected_events: list[str], timeout: float) -> bool: + 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] + car_sim_ff = self._setup.connections["test_control"][0] # enable simulator - self._mod.call_command(car_sim_ff, 'enable', {'value': True}) + 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) + 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) @@ -93,30 +99,34 @@ async def test_000_startup_check(everest_core: EverestCore): everest_core.start() -@pytest.mark.everest_core_config('config-sil.yaml') +@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')] + "test_control": [Requirement("car_simulator", "main")], + "connector_1": [Requirement("connector_1", "evse")], } - everest_core.start(standalone_module='probe', test_connections=test_connections) + 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)) + 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'] + 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) + assert probe.test(charging_session_cmd, expected_events, 20) @pytest.mark.asyncio @@ -124,15 +134,17 @@ 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')] + "test_control": [Requirement("car_simulator", "main")], + "connector_1": [Requirement("connector_1", "evse")], } - everest_core.start(standalone_module='probe',test_connections=test_connections) + 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)) + session = RuntimeSession( + str(everest_core.prefix_path), str(everest_core.everest_config_path) + ) probe = ProbeModule(session) @@ -140,7 +152,14 @@ async def test_000_demo_run(everest_core: EverestCore): 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'}] + 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)