diff --git a/.gitignore b/.gitignore index ed7e36d..241553b 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ relPaths.sh /doc/ *_info_positions.req *_info_settings.req +__pycache__/ +*.py[cod] +*$py.class +test-reports/ \ No newline at end of file diff --git a/Makefile b/Makefile index a484532..077c9f2 100644 --- a/Makefile +++ b/Makefile @@ -32,3 +32,7 @@ iocBoot_DEPEND_DIRS += $(filter %App,$(DIRS)) # Add any additional dependency rules here: include $(TOP)/configure/RULES_TOP + + +ioctests: + .\system_tests\run_tests.bat diff --git a/system_tests/__init__.py b/system_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/system_tests/lewis_emulators/__init__.py b/system_tests/lewis_emulators/__init__.py new file mode 100644 index 0000000..ad92558 --- /dev/null +++ b/system_tests/lewis_emulators/__init__.py @@ -0,0 +1,2 @@ +# DO NOT DELETE THIS FILE - LEWIS FRAMEWORK REQUIRES THE DIRECTORY TO BE IMPORTABLE +from __future__ import absolute_import diff --git a/system_tests/lewis_emulators/lewis_versions.py b/system_tests/lewis_emulators/lewis_versions.py new file mode 100644 index 0000000..0f6c45a --- /dev/null +++ b/system_tests/lewis_emulators/lewis_versions.py @@ -0,0 +1,2 @@ +LEWIS_1_3_0 = "1.3.0" +LEWIS_LATEST = LEWIS_1_3_0 diff --git a/system_tests/lewis_emulators/tti355/__init__.py b/system_tests/lewis_emulators/tti355/__init__.py new file mode 100644 index 0000000..8318ff0 --- /dev/null +++ b/system_tests/lewis_emulators/tti355/__init__.py @@ -0,0 +1,5 @@ +from .device import SimulatedTti355 +from ..lewis_versions import LEWIS_LATEST + +framework_version = LEWIS_LATEST +__all__ = ['SimulatedTti355'] diff --git a/system_tests/lewis_emulators/tti355/device.py b/system_tests/lewis_emulators/tti355/device.py new file mode 100644 index 0000000..bf0b505 --- /dev/null +++ b/system_tests/lewis_emulators/tti355/device.py @@ -0,0 +1,106 @@ +from collections import OrderedDict +from .states import DefaultState +from lewis.devices import StateMachineDevice +from random import random + + +class SimulatedTti355(StateMachineDevice): + + def _initialize_data(self): + """ + Initialize all of the device's attributes. + """ + self.identity = "Thurlby Thandar,EL302P,0,v1.14" + self.voltage = 0.00 + self.voltage_sp = 1.00 + self.current = 0.00 + self.current_limit_sp = 1.00 + self.output_status = "OUT OFF" + self.output_mode = "M CV" + self.error = "ERR 0" + self._max_voltage = 35.0 + self._max_current = 5.0 + self.load_resistance = 10 + + def reset(self): + self._initialize_data() + + def _get_state_handlers(self): + return { + 'default': DefaultState(), + } + + def _get_initial_state(self): + return 'default' + + def _get_transition_handlers(self): + return OrderedDict([ + ]) + + def calculate_potential_current(self, voltage): + return voltage/self.load_resistance + + def calculate_actual_voltage(self): + return self.get_current() * self.load_resistance + + def voltage_within_limits(self, voltage): + return voltage <= self._max_voltage + + def get_voltage(self): + if self.output_status == "OUT ON": + if self.output_mode == "M CI": + self.voltage = self.calculate_actual_voltage() + else: + self.voltage = self.voltage_sp + ((random()-0.5)/1000) + else: + self.voltage = ((random()-0.5)/1000) + return self.voltage + + def set_voltage_sp(self, voltage): + voltage = round(float(voltage), 2) + if not self.voltage_within_limits(voltage): + self.error = "ERR 2" + else: + self.voltage_sp = voltage + if self.calculate_potential_current(voltage) > self.current_limit_sp and self.output_status == "OUT ON": + self.output_mode = "M CI" + + def current_within_limits(self, current): + return current <= self._max_current + + def get_current(self): + if self.output_status == "OUT ON" and self.output_mode == "M CI": + self.current = self.current_limit_sp + ((random()-0.5)/1000) + else: + self.current = ((random()-0.5)/1000) + return self.current + + def set_current_limit_sp(self, current): + current = round(float(current), 2) + if not self.current_within_limits(current): + self.error = "ERR 2" + else: + self.current_limit_sp = current + + def set_output_status(self, status): + if status == "ON": + self.output_status = "OUT ON" + elif status == "OFF": + self.output_status = "OUT OFF" + self.reset() + + def get_output_status(self): + return self.output_status + + def get_output_mode(self): + return self.output_mode + + def get_error_status(self): + if self.error == "ERR 1": + self.error = "ERR 0" + return "ERR 1" + elif self.error == "ERR 2": + self.error = "ERR 0" + return "ERR 2" + else: + return self.error diff --git a/system_tests/lewis_emulators/tti355/interfaces/__init__.py b/system_tests/lewis_emulators/tti355/interfaces/__init__.py new file mode 100644 index 0000000..9323ad2 --- /dev/null +++ b/system_tests/lewis_emulators/tti355/interfaces/__init__.py @@ -0,0 +1,3 @@ +from .stream_interface import Tti355StreamInterface + +__all__ = ['Tti355StreamInterface'] diff --git a/system_tests/lewis_emulators/tti355/interfaces/stream_interface.py b/system_tests/lewis_emulators/tti355/interfaces/stream_interface.py new file mode 100644 index 0000000..143d5a7 --- /dev/null +++ b/system_tests/lewis_emulators/tti355/interfaces/stream_interface.py @@ -0,0 +1,85 @@ +from lewis.adapters.stream import StreamInterface +from lewis.utils.command_builder import CmdBuilder + + +class Tti355StreamInterface(StreamInterface): + + in_terminator = "\n" + out_terminator = "\r\n" + + def __init__(self): + + super(Tti355StreamInterface, self).__init__() + + # Commands that we expect via serial during normal operation + self.commands = { + CmdBuilder(self.get_identity).escape("*IDN?").eos().build(), + CmdBuilder(self.reset).escape("*RST").eos().build(), + CmdBuilder(self.get_voltage_sp).escape("V?").eos().build(), + CmdBuilder(self.set_voltage_sp).escape("V ").float().eos().build(), + CmdBuilder(self.get_voltage).escape("VO?").eos().build(), + + CmdBuilder(self.get_current_sp).escape("I?").eos().build(), + CmdBuilder(self.set_current_limit_sp).escape("I ").float().eos().build(), + CmdBuilder(self.get_current).escape("IO?").eos().build(), + + CmdBuilder(self.get_outputstatus).escape("OUT?").eos().build(), + CmdBuilder(self.set_outputstatus_on).escape("ON").eos().build(), + CmdBuilder(self.set_outputstatus_off).escape("OFF").eos().build(), + + CmdBuilder(self.get_output_mode).escape("M?").eos().build(), + CmdBuilder(self.get_error_status).escape("ERR?").eos().build(), + + } + + def handle_error(self, request, error): + err_string = "command was: {}, error was: {}: {}\n".format(request, error.__class__.__name__, error) + print(err_string) + self.log.error(err_string) + return err_string + + def reset(self): + self.device.reset() + return self.out_terminator + + def get_voltage(self): + volt = self.device.get_voltage() + return "V{:.2f}".format(volt) + + def get_voltage_sp(self): + return "V{:.2f}".format(self.device.voltage_sp) + + def set_voltage_sp(self, voltage_sp): + self.device.set_voltage_sp(float(voltage_sp)) + return self.out_terminator + + def get_current(self): + current = self.device.get_current() + return "I{:.2f}".format(current) + + def get_current_sp(self): + return "I{:.2f}".format(self.device.current_limit_sp) + + def set_current_limit_sp(self, current_limit_sp): + self.device.set_current_limit_sp(float(current_limit_sp)) + return self.out_terminator + + def get_outputstatus(self): + return self.device.get_output_status() + + def set_outputstatus_on(self): + self.device.set_output_status("ON") + return self.out_terminator + + def set_outputstatus_off(self): + self.device.set_output_status("OFF") + return self.out_terminator + + def get_output_mode(self): + return self.device.get_output_mode() + + def get_error_status(self): + return self.device.get_error_status() + + def get_identity(self): + return self.device.identity diff --git a/system_tests/lewis_emulators/tti355/states.py b/system_tests/lewis_emulators/tti355/states.py new file mode 100644 index 0000000..e4ca48e --- /dev/null +++ b/system_tests/lewis_emulators/tti355/states.py @@ -0,0 +1,5 @@ +from lewis.core.statemachine import State + + +class DefaultState(State): + pass diff --git a/system_tests/run_tests.bat b/system_tests/run_tests.bat new file mode 100644 index 0000000..65ab1d6 --- /dev/null +++ b/system_tests/run_tests.bat @@ -0,0 +1,10 @@ +@echo off +setlocal +REM Run this directory's tests using the IOC Testing Framework + +call "%~dp0..\..\..\..\config_env.bat" + +REM Command line arguments always passed to the test script +SET ARGS=--test_and_emulator %~dp0 +call %PYTHON3% -u "%EPICS_KIT_ROOT%\support\IocTestFramework\master\run_tests.py" %ARGS% %* +IF %ERRORLEVEL% NEQ 0 EXIT /b %ERRORLEVEL% diff --git a/system_tests/tests/__init__.py b/system_tests/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/system_tests/tests/tti355.py b/system_tests/tests/tti355.py new file mode 100644 index 0000000..4596634 --- /dev/null +++ b/system_tests/tests/tti355.py @@ -0,0 +1,78 @@ +import unittest +from parameterized import parameterized + +from common_tests.tti_common import TtiCommon +from utils.channel_access import ChannelAccess +from utils.ioc_launcher import get_default_ioc_dir +from utils.test_modes import TestModes +from utils.testing import get_running_lewis_and_ioc, skip_if_recsim + + +DEVICE_PREFIX = "TTI355_01" +DEVICE_NAME = "tti355" + +VOLT_LOW_LIMIT = 0.0 +VOLT_HIGH_LIMIT = 35.0 +CURR_LOW_LIMIT = 0.01 +CURR_HIGH_LIMIT = 5.0 + +IOCS = [ + { + "name": DEVICE_PREFIX, + "directory": get_default_ioc_dir("TTI355"), + "macros": { + "MIN_VOLT": VOLT_LOW_LIMIT, + "MAX_VOLT": VOLT_HIGH_LIMIT, + "MIN_CURR": CURR_LOW_LIMIT, + "MAX_CURR": CURR_HIGH_LIMIT, + }, + "emulator": DEVICE_NAME, + }, +] + + +TEST_MODES = [TestModes.RECSIM, TestModes.DEVSIM] + + +class Tti355Tests(TtiCommon, unittest.TestCase): + """ + Tests for the Tti355 IOC. + """ + def setUp(self): + self._lewis, self._ioc = get_running_lewis_and_ioc(DEVICE_NAME, DEVICE_PREFIX) + self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX) + self._lewis.backdoor_run_function_on_device("reset") + + def get_on_state_name(self): + return "ON" + + def get_off_state_name(self): + return "OFF" + + @parameterized.expand([ + ("lt_low_limit", VOLT_LOW_LIMIT-1.0, "low_limit", VOLT_LOW_LIMIT), + ("gt_high_limit", VOLT_HIGH_LIMIT+1, "high_limit", VOLT_HIGH_LIMIT)]) + @skip_if_recsim("Behaviour cannot be simulated in Recsim") + def test_WHEN_voltage_setpoint_is_set_outside_max_limits_THEN_setpoint_within(self, case, case_value, limit, limit_value): + self.ca.set_pv_value("VOLTAGE:SP", case_value) + self.ca.assert_that_pv_is("VOLTAGE:SP", limit_value) + + @parameterized.expand([ + ("lt_low_limit", CURR_LOW_LIMIT-1, "low_limit", CURR_LOW_LIMIT), + ("gt_high_limit", CURR_HIGH_LIMIT+1, "high_limit", CURR_HIGH_LIMIT)]) + @skip_if_recsim("Behaviour cannot be simulated in Recsim") + def test_WHEN_voltage_setpoint_is_set_outside_max_limits_THEN_setpoint_within(self, case, case_value, limit, limit_value): + self.ca.set_pv_value("CURRENT:SP", case_value) + self.ca.assert_that_pv_is("CURRENT:SP", limit_value) + + @skip_if_recsim("Behaviour cannot be simulated in Recsim") + def test_WHEN_identity_requested_THEN_correct_identity_returned(self): + expected_identity = "Thurlby Thandar,EL302P,0,v1.14" + self.ca.assert_that_pv_is("IDENT", expected_identity) + + @skip_if_recsim("Behaviour cannot be simulated in Recsim") + def test_WHEN_ioc_in_error_state_2_THEN_correct_error_state_returned(self): + expected_value = "Cmd outside limits" + self._lewis.backdoor_set_on_device("error", "ERR 2") + self.ca.set_pv_value("ERROR.PROC", 1) + self.ca.assert_that_pv_is("ERROR", expected_value)