MTPPy2.0 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- examples/__init__.py +0 -0
- examples/minimal+pid.py +82 -0
- examples/minimal.py +32 -0
- examples/minimal_recipe.py +117 -0
- examples/pea_alarm_base.py +47 -0
- examples/pea_minimal.py +258 -0
- mtppy/__init__.py +1 -0
- mtppy/part1/__init__.py +4 -0
- mtppy/part1/empty_folder_test.py +7 -0
- mtppy/part3/__init__.py +4 -0
- mtppy/part3/data_assembly_set/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
- mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
- mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
- mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
- mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
- mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
- mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
- mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
- mtppy/part3/data_assembly_set/data_assembly.py +65 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
- mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
- mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
- mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
- mtppy/part3/definitions/__init__.py +4 -0
- mtppy/part3/definitions/mtp_access.py +15 -0
- mtppy/part4/__init__.py +4 -0
- mtppy/part4/automation_services/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
- mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
- mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
- mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
- mtppy/part4/automation_services/helpers/__init__.py +4 -0
- mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
- mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
- mtppy/part4/automation_services/helpers/state_machine.py +167 -0
- mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
- mtppy/part4/automation_services/helpers/thread_control.py +39 -0
- mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
- mtppy/part4/automation_services/procedure.py +111 -0
- mtppy/part4/automation_services/procedure_test.py +185 -0
- mtppy/part4/automation_services/service.py +463 -0
- mtppy/part4/automation_services/service_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
- mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
- mtppy/part4/automation_services/service_elements/service_control.py +473 -0
- mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
- mtppy/part4/automation_services/service_elements/service_element.py +20 -0
- mtppy/part4/automation_services/service_test.py +407 -0
- mtppy/part4/definitions/__init__.py +4 -0
- mtppy/part4/definitions/command_codes.py +58 -0
- mtppy/part4/definitions/command_codes_test.py +44 -0
- mtppy/part4/definitions/state_codes.py +65 -0
- mtppy/part4/definitions/state_codes_test.py +44 -0
- mtppy/part4/process_values/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/vqc.py +48 -0
- mtppy/part4/process_values/input_elements/__init__.py +4 -0
- mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
- mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
- mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
- mtppy/part4/process_values/input_elements/input_element.py +40 -0
- mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
- mtppy/part5/__init__.py +4 -0
- mtppy/part5/communication_object.py +99 -0
- mtppy/part5/communication_object_test.py +59 -0
- mtppy/part5/definitions/__init__.py +4 -0
- mtppy/part5/definitions/iec61131_data_types.py +31 -0
- mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
- mtppy/part5/definitions/opcua_data_types.py +36 -0
- mtppy/part5/definitions/opcua_data_types_test.py +90 -0
- mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
- mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
- mtppy/part5/server_assembly_set/__init__.py +5 -0
- mtppy/part6/__init__.py +4 -0
- mtppy/part6/alarm_set/__init__.py +4 -0
- mtppy/part6/alarm_set/alarm.py +132 -0
- mtppy/part6/alarm_set/alarm_test.py +265 -0
- mtppy/pea/__init__.py +19 -0
- mtppy/pea/data_assembly_factory.py +124 -0
- mtppy/pea/data_assembly_factory_test.py +229 -0
- mtppy/pea/mtp_generator/__init__.py +4 -0
- mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
- mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
- mtppy/pea/pea.py +139 -0
- mtppy/pea/pea_opcua_server/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/marshalling.py +60 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
- mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
- mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
- mtppy/pea/pea_test.py +59 -0
- mtppy/pea/rmtp_pea.py +155 -0
- mtppy/pea/rmtp_pea_test.py +27 -0
- mtppy/pea/service_factory.py +178 -0
- mtppy/pea/service_factory_test.py +156 -0
- mtppy/pea/user_logic/__init__.py +4 -0
- mtppy/pea/user_logic/user_logic_pea.py +72 -0
- mtppy/pea/user_logic/user_logic_registry.py +106 -0
- mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
- mtppy/pea/user_logic/user_logic_service.py +228 -0
- mtppy/pea/user_logic/user_logic_service_test.py +289 -0
- mtppy/utils/__init__.py +4 -0
- mtppy/utils/data_assembly_dict.py +61 -0
- mtppy/utils/data_assembly_dict_test.py +107 -0
- mtppy/utils/data_item.py +446 -0
- mtppy/utils/data_item_com_test.py +82 -0
- mtppy/utils/data_item_test.py +325 -0
- mtppy/utils/logging.py +23 -0
- mtppy/utils/logging_test.py +26 -0
- mtppy/utils/mtp_file_handling/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
- mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
- mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
- mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
- mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
- mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
- mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
- mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
- mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
- mtppy/utils/testing.py +31 -0
- mtppy2_0-0.1.5.dist-info/METADATA +75 -0
- mtppy2_0-0.1.5.dist-info/RECORD +214 -0
- mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
- mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
- mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
- mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
- tools/__init__.py +0 -0
- tools/create_bugreport.py +150 -0
- tools/inspect_mtp.py +110 -0
- utils/__init__.py +1 -0
- utils/file_handling/__init__.py +4 -0
- utils/file_handling/cleanup_folder.py +9 -0
- utils/file_handling/get_utc_foldername_with_ms.py +15 -0
- utils/file_handling/get_work_dir.py +9 -0
- utils/file_handling/is_valid_path_for_json.py +11 -0
- utils/file_handling/is_valid_path_to_json.py +17 -0
- utils/file_handling/load_json_file.py +18 -0
- utils/file_handling/read_file_from_file_archive.py +43 -0
- utils/file_handling/read_file_string.py +10 -0
- utils/file_handling/validate_json_data.py +28 -0
- utils/file_handling/write_json_file.py +23 -0
- utils/get_uuid.py +18 -0
- utils/get_uuid_test.py +17 -0
- utils/logging.py +100 -0
- utils/logging_test.py +67 -0
- utils/opcua/check_node.py +79 -0
- utils/resolve_unit/resolve_unit.py +11 -0
- utils/resolve_unit/unit_lib.py +2101 -0
- utils/resolve_unit/unit_lib_test.py +36 -0
- utils/type_conversion.py +236 -0
- utils/type_conversion_test.py +114 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for the UserLogicService class.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import Mock, call, patch
|
|
7
|
+
from mtppy.pea.user_logic.user_logic_service import UserLogicService
|
|
8
|
+
from mtppy.pea.user_logic.user_logic_registry import UserLogicRegistry
|
|
9
|
+
from mtppy.part4.definitions.state_codes import StateCodes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestUserLogicService(unittest.TestCase):
|
|
13
|
+
"""Test cases for the UserLogicService class."""
|
|
14
|
+
|
|
15
|
+
def setUp(self):
|
|
16
|
+
"""Set up test fixtures before each test method."""
|
|
17
|
+
self.service = UserLogicService(
|
|
18
|
+
tag_name="test_service", tag_description="Test service description"
|
|
19
|
+
)
|
|
20
|
+
self.registry = UserLogicRegistry()
|
|
21
|
+
|
|
22
|
+
def test_init(self):
|
|
23
|
+
"""Test that the UserLogicService initializes correctly."""
|
|
24
|
+
self.assertEqual(self.service.name, "test_service")
|
|
25
|
+
self.assertEqual(self.service.tag_description, "Test service description")
|
|
26
|
+
self.assertIsNone(self.service._registry)
|
|
27
|
+
self.assertEqual(self.service._cycle_time, 0.5)
|
|
28
|
+
|
|
29
|
+
def test_init_with_registry(self):
|
|
30
|
+
"""Test that the UserLogicService initializes with a registry."""
|
|
31
|
+
service = UserLogicService(
|
|
32
|
+
tag_name="test_service",
|
|
33
|
+
tag_description="Test service description",
|
|
34
|
+
registry=self.registry,
|
|
35
|
+
)
|
|
36
|
+
self.assertEqual(service._registry, self.registry)
|
|
37
|
+
|
|
38
|
+
def test_set_registry(self):
|
|
39
|
+
"""Test setting the user logic registry."""
|
|
40
|
+
self.service.set_registry(self.registry)
|
|
41
|
+
self.assertEqual(self.service._registry, self.registry)
|
|
42
|
+
|
|
43
|
+
def test_dispatch_with_registry(self):
|
|
44
|
+
"""Test dispatch method with a registry."""
|
|
45
|
+
|
|
46
|
+
def mock_func(service):
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
self.registry.register("test_func")(mock_func)
|
|
50
|
+
self.service.set_registry(self.registry)
|
|
51
|
+
|
|
52
|
+
# Mock the service methods that would normally be called
|
|
53
|
+
with patch.object(
|
|
54
|
+
self.service, "_lookup_procedure_cur_name", return_value="proc1"
|
|
55
|
+
):
|
|
56
|
+
with patch.object(
|
|
57
|
+
self.service, "_user_code_callback_name"
|
|
58
|
+
) as mock_callback_name:
|
|
59
|
+
mock_callback_name.return_value = "test_func"
|
|
60
|
+
|
|
61
|
+
# Mock dispatch to avoid actual execution
|
|
62
|
+
with patch.object(self.registry, "dispatch", return_value=True):
|
|
63
|
+
result = self.service.dispatch("test_func")
|
|
64
|
+
self.assertTrue(result)
|
|
65
|
+
|
|
66
|
+
def test_dispatch_without_registry(self):
|
|
67
|
+
"""Test dispatch method without a registry raises RuntimeError."""
|
|
68
|
+
with self.assertRaises(RuntimeError):
|
|
69
|
+
self.service.dispatch("test_func")
|
|
70
|
+
|
|
71
|
+
def test_get_param(self):
|
|
72
|
+
"""Test get_param method."""
|
|
73
|
+
# Create a mock procedure with parameters
|
|
74
|
+
mock_proc = Mock()
|
|
75
|
+
mock_proc.procedure_parameters = {"param1": "value1"}
|
|
76
|
+
mock_proc.process_value_ins = {}
|
|
77
|
+
mock_proc.process_value_outs = {}
|
|
78
|
+
mock_proc.report_values = {}
|
|
79
|
+
mock_proc.required_equipment = {}
|
|
80
|
+
|
|
81
|
+
# Mock the get_current_procedure method
|
|
82
|
+
with patch.object(
|
|
83
|
+
self.service, "get_current_procedure", return_value=mock_proc
|
|
84
|
+
):
|
|
85
|
+
result = self.service.get_param("param1")
|
|
86
|
+
self.assertEqual(result, "value1")
|
|
87
|
+
|
|
88
|
+
def test_get_param_not_found(self):
|
|
89
|
+
"""Test get_param method when parameter is not found."""
|
|
90
|
+
# Create a mock procedure with no parameters
|
|
91
|
+
mock_proc = Mock()
|
|
92
|
+
mock_proc.procedure_parameters = {}
|
|
93
|
+
mock_proc.process_value_ins = {}
|
|
94
|
+
mock_proc.process_value_outs = {}
|
|
95
|
+
mock_proc.report_values = {}
|
|
96
|
+
mock_proc.required_equipment = {}
|
|
97
|
+
|
|
98
|
+
# Mock the get_current_procedure method
|
|
99
|
+
with patch.object(
|
|
100
|
+
self.service, "get_current_procedure", return_value=mock_proc
|
|
101
|
+
):
|
|
102
|
+
result = self.service.get_param("nonexistent")
|
|
103
|
+
self.assertIsNone(result)
|
|
104
|
+
|
|
105
|
+
def test_user_code_callback_name(self):
|
|
106
|
+
"""Test _user_code_callback_name method."""
|
|
107
|
+
callback_name = self.service._user_code_callback_name(
|
|
108
|
+
"proc1", StateCodes.IDLE.string, UserLogicService.ENTRY
|
|
109
|
+
)
|
|
110
|
+
self.assertEqual(
|
|
111
|
+
callback_name,
|
|
112
|
+
f"test_service.proc1.{StateCodes.IDLE.string}.{UserLogicService.ENTRY}",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def test_transient_state(self):
|
|
116
|
+
"""Test _transient_state method."""
|
|
117
|
+
# Mock the helper methods
|
|
118
|
+
with patch.object(
|
|
119
|
+
self.service, "_lookup_procedure_cur_name", return_value="proc1"
|
|
120
|
+
):
|
|
121
|
+
with patch.object(self.service, "dispatch") as mock_dispatch:
|
|
122
|
+
with patch.object(self.service, "state_change"):
|
|
123
|
+
with patch("mtppy.pea.user_logic.user_logic_service.logger"):
|
|
124
|
+
self.service._transient_state(StateCodes.IDLE.string)
|
|
125
|
+
|
|
126
|
+
# Verify that dispatch was called for entry, do, and exit
|
|
127
|
+
self.assertEqual(mock_dispatch.call_count, 3)
|
|
128
|
+
|
|
129
|
+
def test_transient_state_dispatches_entry_do_and_exit_in_order(self):
|
|
130
|
+
"""Test that a transient state dispatches entry, do, and exit callbacks."""
|
|
131
|
+
with patch.object(
|
|
132
|
+
self.service, "_lookup_procedure_cur_name", return_value="proc1"
|
|
133
|
+
):
|
|
134
|
+
with patch.object(self.service, "dispatch") as mock_dispatch:
|
|
135
|
+
with patch.object(self.service, "state_change"):
|
|
136
|
+
with patch("mtppy.pea.user_logic.user_logic_service.logger"):
|
|
137
|
+
self.service._transient_state(StateCodes.IDLE.string)
|
|
138
|
+
|
|
139
|
+
self.assertEqual(
|
|
140
|
+
mock_dispatch.call_args_list,
|
|
141
|
+
[
|
|
142
|
+
call(
|
|
143
|
+
self.service._user_code_callback_name(
|
|
144
|
+
"proc1", StateCodes.IDLE.string, UserLogicService.ENTRY
|
|
145
|
+
)
|
|
146
|
+
),
|
|
147
|
+
call(
|
|
148
|
+
self.service._user_code_callback_name(
|
|
149
|
+
"proc1", StateCodes.IDLE.string, UserLogicService.DO
|
|
150
|
+
)
|
|
151
|
+
),
|
|
152
|
+
call(
|
|
153
|
+
self.service._user_code_callback_name(
|
|
154
|
+
"proc1", StateCodes.IDLE.string, UserLogicService.EXIT
|
|
155
|
+
)
|
|
156
|
+
),
|
|
157
|
+
],
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def test_cyclic_state(self):
|
|
161
|
+
"""Test _cyclic_state method."""
|
|
162
|
+
# Mock the helper methods
|
|
163
|
+
with patch.object(
|
|
164
|
+
self.service, "_lookup_procedure_cur_name", return_value="proc1"
|
|
165
|
+
):
|
|
166
|
+
with patch.object(self.service, "dispatch") as mock_dispatch:
|
|
167
|
+
with patch.object(self.service, "is_state", side_effect=[True, False]):
|
|
168
|
+
with patch.object(self.service, "thread_ctrl") as mock_thread_ctrl:
|
|
169
|
+
mock_thread_ctrl.is_stopped.return_value = False
|
|
170
|
+
with patch(
|
|
171
|
+
"mtppy.pea.user_logic.user_logic_service.time.sleep"
|
|
172
|
+
):
|
|
173
|
+
with patch(
|
|
174
|
+
"mtppy.pea.user_logic.user_logic_service.logger"
|
|
175
|
+
):
|
|
176
|
+
self.service._cyclic_state(StateCodes.IDLE.string)
|
|
177
|
+
|
|
178
|
+
# Verify that dispatch was called for entry and do at least once
|
|
179
|
+
self.assertGreaterEqual(mock_dispatch.call_count, 2)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class TestUserLogicServiceStateMethods(unittest.TestCase):
|
|
183
|
+
"""Test state methods of UserLogicService."""
|
|
184
|
+
|
|
185
|
+
def setUp(self):
|
|
186
|
+
"""Set up test fixtures."""
|
|
187
|
+
self.service = UserLogicService(
|
|
188
|
+
tag_name="test_service", tag_description="Test service description"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def test_idle_method(self):
|
|
192
|
+
"""Test idle method calls _cyclic_state."""
|
|
193
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
194
|
+
self.service.idle()
|
|
195
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.IDLE.string)
|
|
196
|
+
|
|
197
|
+
def test_starting_method(self):
|
|
198
|
+
"""Test starting method calls _transient_state."""
|
|
199
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
200
|
+
self.service.starting()
|
|
201
|
+
mock_transient_state.assert_called_once_with(StateCodes.STARTING.string)
|
|
202
|
+
|
|
203
|
+
def test_execute_method(self):
|
|
204
|
+
"""Test execute method calls _cyclic_state."""
|
|
205
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
206
|
+
self.service.execute()
|
|
207
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.EXECUTE.string)
|
|
208
|
+
|
|
209
|
+
def test_completing_method(self):
|
|
210
|
+
"""Test completing method calls _transient_state."""
|
|
211
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
212
|
+
self.service.completing()
|
|
213
|
+
mock_transient_state.assert_called_once_with(StateCodes.COMPLETING.string)
|
|
214
|
+
|
|
215
|
+
def test_completed_method(self):
|
|
216
|
+
"""Test completed method calls _cyclic_state."""
|
|
217
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
218
|
+
self.service.completed()
|
|
219
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.COMPLETING.string)
|
|
220
|
+
|
|
221
|
+
def test_pausing_method(self):
|
|
222
|
+
"""Test pausing method calls _transient_state."""
|
|
223
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
224
|
+
self.service.pausing()
|
|
225
|
+
mock_transient_state.assert_called_once_with(StateCodes.PAUSING.string)
|
|
226
|
+
|
|
227
|
+
def test_paused_method(self):
|
|
228
|
+
"""Test paused method calls _cyclic_state."""
|
|
229
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
230
|
+
self.service.paused()
|
|
231
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.PAUSED.string)
|
|
232
|
+
|
|
233
|
+
def test_resuming_method(self):
|
|
234
|
+
"""Test resuming method calls _transient_state."""
|
|
235
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
236
|
+
self.service.resuming()
|
|
237
|
+
mock_transient_state.assert_called_once_with(StateCodes.RESUMING.string)
|
|
238
|
+
|
|
239
|
+
def test_holding_method(self):
|
|
240
|
+
"""Test holding method calls _transient_state."""
|
|
241
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
242
|
+
self.service.holding()
|
|
243
|
+
mock_transient_state.assert_called_once_with(StateCodes.HOLDING.string)
|
|
244
|
+
|
|
245
|
+
def test_held_method(self):
|
|
246
|
+
"""Test held method calls _cyclic_state."""
|
|
247
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
248
|
+
self.service.held()
|
|
249
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.HELD.string)
|
|
250
|
+
|
|
251
|
+
def test_unholding_method(self):
|
|
252
|
+
"""Test unholding method calls _transient_state."""
|
|
253
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
254
|
+
self.service.unholding()
|
|
255
|
+
mock_transient_state.assert_called_once_with(StateCodes.UNHOLDING.string)
|
|
256
|
+
|
|
257
|
+
def test_stopping_method(self):
|
|
258
|
+
"""Test stopping method calls _transient_state."""
|
|
259
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
260
|
+
self.service.stopping()
|
|
261
|
+
mock_transient_state.assert_called_once_with(StateCodes.STOPPING.string)
|
|
262
|
+
|
|
263
|
+
def test_stopped_method(self):
|
|
264
|
+
"""Test stopped method calls _cyclic_state."""
|
|
265
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
266
|
+
self.service.stopped()
|
|
267
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.STOPPED.string)
|
|
268
|
+
|
|
269
|
+
def test_aborting_method(self):
|
|
270
|
+
"""Test aborting method calls _transient_state."""
|
|
271
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
272
|
+
self.service.aborting()
|
|
273
|
+
mock_transient_state.assert_called_once_with(StateCodes.ABORTING.string)
|
|
274
|
+
|
|
275
|
+
def test_aborted_method(self):
|
|
276
|
+
"""Test aborted method calls _cyclic_state."""
|
|
277
|
+
with patch.object(self.service, "_cyclic_state") as mock_cyclic_state:
|
|
278
|
+
self.service.aborted()
|
|
279
|
+
mock_cyclic_state.assert_called_once_with(StateCodes.ABORTED.string)
|
|
280
|
+
|
|
281
|
+
def test_resetting_method(self):
|
|
282
|
+
"""Test resetting method calls _transient_state."""
|
|
283
|
+
with patch.object(self.service, "_transient_state") as mock_transient_state:
|
|
284
|
+
self.service.resetting()
|
|
285
|
+
mock_transient_state.assert_called_once_with(StateCodes.RESETTING.string)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
if __name__ == "__main__":
|
|
289
|
+
unittest.main()
|
mtppy/utils/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""helper for adding typed items to a dict from different sources."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from collections.abc import Mapping, Sequence
|
|
9
|
+
from typing import Any, Protocol, get_type_hints
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TagNamed(Protocol):
|
|
13
|
+
@property
|
|
14
|
+
def tag_name(self) -> str: ... # type-only declaration (... instead of pass)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
type ElementSource[T: TagNamed] = (
|
|
18
|
+
T | Sequence[ElementSource[T]] | Mapping[Any, ElementSource[T]] | None
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DataAssemblyDict[T: TagNamed](dict[str, T]):
|
|
23
|
+
"""Dictionary that collects elements by their ``tag_name`` attribute."""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
element_type: type[T],
|
|
28
|
+
element_name: str | None = None,
|
|
29
|
+
element: ElementSource[T] = None,
|
|
30
|
+
) -> None:
|
|
31
|
+
if not self._has_tag_name(element_type):
|
|
32
|
+
raise TypeError(f"{element_type.__name__} must have a tag_name attribute")
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.element_type = element_type
|
|
35
|
+
self.element_name = element_name or element_type.__name__
|
|
36
|
+
self.add(element)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _has_tag_name(element_type: type[T]) -> bool:
|
|
40
|
+
return hasattr(element_type, "tag_name") or "tag_name" in get_type_hints(
|
|
41
|
+
element_type,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def add(self, element: ElementSource[T]) -> None:
|
|
45
|
+
"""Add an element or nested collection of elements."""
|
|
46
|
+
if element is None:
|
|
47
|
+
return
|
|
48
|
+
if isinstance(element, self.element_type):
|
|
49
|
+
self[element.tag_name] = element
|
|
50
|
+
elif isinstance(element, Sequence) and not isinstance(
|
|
51
|
+
element,
|
|
52
|
+
(str, bytes, bytearray),
|
|
53
|
+
):
|
|
54
|
+
for nested_element in element:
|
|
55
|
+
self.add(nested_element)
|
|
56
|
+
elif isinstance(element, Mapping):
|
|
57
|
+
for nested_element in element.values():
|
|
58
|
+
self.add(nested_element)
|
|
59
|
+
else:
|
|
60
|
+
msg = f"{getattr(element, 'tag_name', repr(element))} is not a {self.element_name}"
|
|
61
|
+
raise TypeError(msg)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Tests for data assembly dictionary helpers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from mtppy.utils.data_assembly_dict import DataAssemblyDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Element:
|
|
12
|
+
tag_name: str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OtherElement:
|
|
16
|
+
def __init__(self, tag_name: str) -> None:
|
|
17
|
+
self.tag_name = tag_name
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_data_assembly_dict_stores_element_type_once() -> None:
|
|
21
|
+
elements = DataAssemblyDict(Element)
|
|
22
|
+
first = Element("Element1")
|
|
23
|
+
second = Element("Element2")
|
|
24
|
+
|
|
25
|
+
elements.add([first, {"second": second}])
|
|
26
|
+
|
|
27
|
+
assert elements == {"Element1": first, "Element2": second}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_data_assembly_dict_requires_tag_name_on_element_class() -> None:
|
|
31
|
+
with pytest.raises(TypeError, match="OtherElement must have a tag_name attribute"):
|
|
32
|
+
DataAssemblyDict(OtherElement)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_data_assembly_dict_adds_element_by_tag_name() -> None:
|
|
36
|
+
target = DataAssemblyDict(Element)
|
|
37
|
+
element = Element("Element1")
|
|
38
|
+
|
|
39
|
+
target.add(element)
|
|
40
|
+
|
|
41
|
+
assert target == {"Element1": element}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_data_assembly_dict_ignores_none() -> None:
|
|
45
|
+
target = DataAssemblyDict(Element)
|
|
46
|
+
|
|
47
|
+
target.add(None)
|
|
48
|
+
|
|
49
|
+
assert target == {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_data_assembly_dict_recursively_adds_elements_from_a_list() -> None:
|
|
53
|
+
target = DataAssemblyDict(Element)
|
|
54
|
+
elements = [Element("Element1"), Element("Element2")]
|
|
55
|
+
|
|
56
|
+
target.add(elements)
|
|
57
|
+
|
|
58
|
+
assert target == {element.tag_name: element for element in elements}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_data_assembly_dict_recursively_adds_elements_from_a_dictionary() -> None:
|
|
62
|
+
target = DataAssemblyDict(Element)
|
|
63
|
+
elements = {"first": Element("Element1"), "second": Element("Element2")}
|
|
64
|
+
|
|
65
|
+
target.add(elements)
|
|
66
|
+
|
|
67
|
+
assert target == {element.tag_name: element for element in elements.values()}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_data_assembly_dict_recurses_through_nested_lists_and_dictionaries() -> None:
|
|
71
|
+
target = DataAssemblyDict(Element)
|
|
72
|
+
first = Element("Element1")
|
|
73
|
+
second = Element("Element2")
|
|
74
|
+
nested_elements: list[object] = [first, {"second": second}, None]
|
|
75
|
+
|
|
76
|
+
target.add(nested_elements) # type: ignore[arg-type]
|
|
77
|
+
|
|
78
|
+
assert target == {"Element1": first, "Element2": second}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_data_assembly_dict_overwrites_existing_tag_name() -> None:
|
|
82
|
+
target = DataAssemblyDict(Element)
|
|
83
|
+
target.add(Element("Element1"))
|
|
84
|
+
element = Element("Element1")
|
|
85
|
+
|
|
86
|
+
target.add(element)
|
|
87
|
+
|
|
88
|
+
assert target == {"Element1": element}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.parametrize("invalid_element", ["invalid", 1, OtherElement("Other")])
|
|
92
|
+
def test_data_assembly_dict_rejects_invalid_elements(invalid_element: object) -> None:
|
|
93
|
+
target = DataAssemblyDict(Element)
|
|
94
|
+
|
|
95
|
+
with pytest.raises(TypeError, match="is not a Element"):
|
|
96
|
+
target.add(invalid_element) # type: ignore[arg-type]
|
|
97
|
+
|
|
98
|
+
assert target == {}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_data_assembly_dict_rejects_invalid_nested_elements() -> None:
|
|
102
|
+
target = DataAssemblyDict(Element)
|
|
103
|
+
|
|
104
|
+
with pytest.raises(TypeError, match="'invalid' is not a Element"):
|
|
105
|
+
target.add([Element("Element1"), "invalid"]) # type: ignore[arg-type]
|
|
106
|
+
|
|
107
|
+
assert target == {"Element1": target["Element1"]}
|