From 2969edff1c2f262e6f874f7d16ffd953a6e83b2e Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Wed, 13 Nov 2024 11:28:24 -0500 Subject: [PATCH 1/8] Adding records for tracking version of IOC and PandA firmware --- src/pandablocks_ioc/ioc.py | 54 +++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index 35fae5c0..f2b8f63e 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -14,6 +14,7 @@ Arm, ChangeGroup, Disarm, + Get, GetBlockInfo, GetChanges, GetFieldInfo, @@ -66,6 +67,7 @@ trim_description, trim_string_value, ) +from ._version import __version__ # TODO: Try turning python.analysis.typeCheckingMode on, as it does highlight a couple # of possible errors @@ -175,6 +177,41 @@ def create_softioc( asyncio.run_coroutine_threadsafe(client.close(), dispatcher.loop).result() +async def get_panda_ver_info( + client: AsyncioClient, +) -> tuple[str]: + """Function that gets version information from the PandA using the IDN command + + Args: + client (AsyncioClient): Client used for commuication with the PandA + + Returns: + Dict[str, str]: Dictionary mapping firmware name to version + """ + + idn = await client.send(Get("*IDN")) + + # Currently, IDN reports sw, fpga, and rootfs versions + firmware_versions = {"PandA SW": "Unknown", "FPGA": "Unknown", "rootfs": "Unknown"} + + prev_key = None + firmware_list = list(firmware_versions.keys()) + for i, key in enumerate(list(firmware_list)): + try: + # Start of string to next key name is version of previous key + if prev_key is not None: + firmware_versions[prev_key] = idn.split(f"{key}: ")[0] + idn = idn.split(f"{key}: ")[1] + prev_key = key + # If we've gone through the list, last key's version is remaining string + if (i + 1) == len(firmware_list): + firmware_versions[key] = idn + except IndexError: + logging.warning(f"Failed to get {key} version information!") + + return firmware_versions + + async def introspect_panda( client: AsyncioClient, ) -> tuple[dict[str, _BlockAndFieldInfo], dict[EpicsName, RecordValue]]: @@ -1824,6 +1861,18 @@ def create_block_records( return record_dict + def create_version_records(self, fw_vers_dict: dict[str, str]): + """Creates handful of records for tracking versions of IOC/Firmware via EPICS + + Args: + fw_vers_dict (dict[str, str]): Dictionary mapping firmwares to versions + """ + + builder.stringIn("VERSION", initial_value=__version__) + for item in fw_vers_dict: + basename = item.replace(" ", "_").upper() # Normalize name + builder.stringIn(f"{basename}_VERSION", initial_value=fw_vers_dict[item]) + def initialise(self, dispatcher: asyncio_dispatcher.AsyncioDispatcher) -> None: """Perform any final initialisation code to create the records. No new records may be created after this method is called. @@ -1851,6 +1900,7 @@ async def create_records( """Query the PandA and create the relevant records based on the information returned""" + fw_vers_dict = await get_panda_ver_info(client) (panda_dict, all_values_dict) = await introspect_panda(client) # Dictionary containing every record of every type @@ -1858,8 +1908,10 @@ async def create_records( record_factory = IocRecordFactory(client, record_prefix, all_values_dict) - # For each field in each block, create block_num records of each field + # Add some top level records for version of IOC, FPGA, and software + record_factory.create_version_records(fw_vers_dict) + # For each field in each block, create block_num records of each field for block, panda_info in panda_dict.items(): block_info = panda_info.block_info values = panda_info.values From 67612a6bef5a67f754967064d8c0a02813380fc1 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 09:46:07 -0500 Subject: [PATCH 2/8] Update src/pandablocks_ioc/ioc.py Co-authored-by: Eva Lott --- src/pandablocks_ioc/ioc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index f2b8f63e..bf209852 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -179,7 +179,7 @@ def create_softioc( async def get_panda_ver_info( client: AsyncioClient, -) -> tuple[str]: +) -> Dict[EpicsName, str]: """Function that gets version information from the PandA using the IDN command Args: From 9e33125049c6a18fdd7b675f79b5b9cae6a537e6 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 12:59:00 -0500 Subject: [PATCH 3/8] Modify logic for getting versions to use , add version info to SYSTEM block and add PVI info --- src/pandablocks_ioc/_pvi.py | 1 + src/pandablocks_ioc/ioc.py | 75 ++++++++++++++++++++++++++----------- 2 files changed, 54 insertions(+), 22 deletions(-) diff --git a/src/pandablocks_ioc/_pvi.py b/src/pandablocks_ioc/_pvi.py index b19f4e8d..48785a3d 100644 --- a/src/pandablocks_ioc/_pvi.py +++ b/src/pandablocks_ioc/_pvi.py @@ -89,6 +89,7 @@ class PviGroup(Enum): CAPTURE = "Capture" HDF = "HDF" TABLE = "Table" # TODO: May not need this anymore + VERSIONS = "Versions" @dataclass diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index bf209852..4477c90f 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -177,9 +177,9 @@ def create_softioc( asyncio.run_coroutine_threadsafe(client.close(), dispatcher.loop).result() -async def get_panda_ver_info( +async def get_panda_versions( client: AsyncioClient, -) -> Dict[EpicsName, str]: +) -> dict[EpicsName, str]: """Function that gets version information from the PandA using the IDN command Args: @@ -194,22 +194,31 @@ async def get_panda_ver_info( # Currently, IDN reports sw, fpga, and rootfs versions firmware_versions = {"PandA SW": "Unknown", "FPGA": "Unknown", "rootfs": "Unknown"} - prev_key = None - firmware_list = list(firmware_versions.keys()) - for i, key in enumerate(list(firmware_list)): - try: - # Start of string to next key name is version of previous key - if prev_key is not None: - firmware_versions[prev_key] = idn.split(f"{key}: ")[0] - idn = idn.split(f"{key}: ")[1] - prev_key = key - # If we've gone through the list, last key's version is remaining string - if (i + 1) == len(firmware_list): - firmware_versions[key] = idn - except IndexError: - logging.warning(f"Failed to get {key} version information!") + # If the *IDN response contains too many keys, break and leave versions as "Unknown" + if len(idn.split(":")) / 2 > len(list(firmware_versions.keys())): + logging.error(f"Version string {idn} recieved from PandA could not be parsed!") + else: + for firmware_name in firmware_versions: + pattern = re.compile( + rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?:{"|".join( + map( + re.escape, + firmware_versions + ) + )}):|$)' + ) + if match := pattern.search(idn): + firmware_versions[firmware_name] = match.group(1).strip() + logging.info( + f"{firmware_name} Version: {firmware_versions[firmware_name]}" + ) + else: + logging.warning(f"Failed to get {firmware_name} version information!") - return firmware_versions + return { + EpicsName(firmware_name.upper().replace(" ", "_")): version + for firmware_name, version in firmware_versions.items() + } async def introspect_panda( @@ -1868,10 +1877,32 @@ def create_version_records(self, fw_vers_dict: dict[str, str]): fw_vers_dict (dict[str, str]): Dictionary mapping firmwares to versions """ - builder.stringIn("VERSION", initial_value=__version__) - for item in fw_vers_dict: - basename = item.replace(" ", "_").upper() # Normalize name - builder.stringIn(f"{basename}_VERSION", initial_value=fw_vers_dict[item]) + system_block_prefix = "SYSTEM" + + ioc_version_record_name = EpicsName(system_block_prefix + ":IOC_VERSION") + ioc_version_record = builder.stringIn( + ioc_version_record_name, DESC="IOC Version", initial_value=__version__ + ) + add_automatic_pvi_info( + PviGroup.VERSIONS, + ioc_version_record, + ioc_version_record_name, + builder.stringIn, + ) + + for firmware_name, version in fw_vers_dict.items(): + firmware_record_name = EpicsName( + system_block_prefix + f":{firmware_name}_VERSION" + ) + firmware_ver_record = builder.stringIn( + firmware_record_name, DESC=firmware_name, initial_value=version + ) + add_automatic_pvi_info( + PviGroup.VERSIONS, + firmware_ver_record, + firmware_record_name, + builder.stringIn, + ) def initialise(self, dispatcher: asyncio_dispatcher.AsyncioDispatcher) -> None: """Perform any final initialisation code to create the records. No new @@ -1900,7 +1931,7 @@ async def create_records( """Query the PandA and create the relevant records based on the information returned""" - fw_vers_dict = await get_panda_ver_info(client) + fw_vers_dict = await get_panda_versions(client) (panda_dict, all_values_dict) = await introspect_panda(client) # Dictionary containing every record of every type From cc316ce7a57704e7b316047993f7abfcd4ba38ff Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 13:02:43 -0500 Subject: [PATCH 4/8] Revert formatting that broke some things --- src/pandablocks_ioc/ioc.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index 4477c90f..0d6c6855 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -200,12 +200,7 @@ async def get_panda_versions( else: for firmware_name in firmware_versions: pattern = re.compile( - rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?:{"|".join( - map( - re.escape, - firmware_versions - ) - )}):|$)' + rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?:{"|".join(map(re.escape, firmware_versions))}):|$)' # noqa: E501 ) if match := pattern.search(idn): firmware_versions[firmware_name] = match.group(1).strip() From 6e3629041d2dc0436f4ebfa87588921228ce1774 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 13:09:21 -0500 Subject: [PATCH 5/8] Run regenerate test bobfiles given new version changes --- tests/test-bobfiles/SYSTEM.bob | 128 +++++++++++++++++++++++++++++++++ tests/test-bobfiles/index.bob | 43 ++++++++--- 2 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 tests/test-bobfiles/SYSTEM.bob diff --git a/tests/test-bobfiles/SYSTEM.bob b/tests/test-bobfiles/SYSTEM.bob new file mode 100644 index 00000000..f6f58514 --- /dev/null +++ b/tests/test-bobfiles/SYSTEM.bob @@ -0,0 +1,128 @@ + + SYSTEM + 0 + 0 + 506 + 166 + 4 + 4 + + Title + TITLE + SYSTEM + 0 + 0 + 506 + 25 + + + + + + + + + true + 1 + + + VERSIONS + 5 + 30 + 496 + 131 + true + + Label + Ioc Version + 0 + 0 + 250 + 20 + $(text) + + + TextUpdate + TEST_PREFIX:SYSTEM:IOC_VERSION + 255 + 0 + 205 + 20 + + + + + 1 + 6 + + + Label + Panda Sw Version + 0 + 25 + 250 + 20 + $(text) + + + TextUpdate + TEST_PREFIX:SYSTEM:PANDA_SW_VERSION + 255 + 25 + 205 + 20 + + + + + 1 + 6 + + + Label + Fpga Version + 0 + 50 + 250 + 20 + $(text) + + + TextUpdate + TEST_PREFIX:SYSTEM:FPGA_VERSION + 255 + 50 + 205 + 20 + + + + + 1 + 6 + + + Label + Rootfs Version + 0 + 75 + 250 + 20 + $(text) + + + TextUpdate + TEST_PREFIX:SYSTEM:ROOTFS_VERSION + 255 + 75 + 205 + 20 + + + + + 1 + 6 + + + diff --git a/tests/test-bobfiles/index.bob b/tests/test-bobfiles/index.bob index 8172b259..c7a1aab3 100644 --- a/tests/test-bobfiles/index.bob +++ b/tests/test-bobfiles/index.bob @@ -3,7 +3,7 @@ 0 0 488 - 130 + 155 4 4 @@ -27,7 +27,7 @@ Label - PCAP + SYSTEM 23 30 250 @@ -38,7 +38,7 @@ OpenDisplay - PCAP.bob + SYSTEM.bob tab Open Display @@ -52,7 +52,7 @@ Label - DATA + PCAP 23 55 250 @@ -63,7 +63,7 @@ OpenDisplay - DATA.bob + PCAP.bob tab Open Display @@ -77,7 +77,7 @@ Label - SEQ + DATA 23 80 250 @@ -88,7 +88,7 @@ OpenDisplay - SEQ.bob + DATA.bob tab Open Display @@ -102,7 +102,7 @@ Label - PULSE + SEQ 23 105 250 @@ -113,7 +113,7 @@ OpenDisplay - PULSE.bob + SEQ.bob tab Open Display @@ -125,4 +125,29 @@ 20 $(actions) + + Label + PULSE + 23 + 130 + 250 + 20 + $(text) + + + OpenDisplay + + + PULSE.bob + tab + Open Display + + + SubScreen + 278 + 130 + 205 + 20 + $(actions) + From 806d2c8007c1366f528305b0031581d9500b4f52 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 13:33:42 -0500 Subject: [PATCH 6/8] Fixing tests, all passing locally --- tests/fixtures/mocked_panda.py | 21 +++++++++++++++++++++ tests/test_connection.py | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/tests/fixtures/mocked_panda.py b/tests/fixtures/mocked_panda.py index e7165d76..533ed76b 100644 --- a/tests/fixtures/mocked_panda.py +++ b/tests/fixtures/mocked_panda.py @@ -22,6 +22,7 @@ ChangeGroup, Command, Disarm, + Get, GetBlockInfo, GetChanges, GetFieldInfo, @@ -430,6 +431,10 @@ def multiple_seq_responses(table_field_info, table_data_1, table_data_2): GetChanges is polled at 10Hz if a different command isn't made. """ return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key( Put( field="SEQ1.TABLE", @@ -564,6 +569,10 @@ def no_numbered_suffix_to_metadata_responses(table_field_info, table_data_1): doesn't have a suffixed number. """ return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key( Put( field="SEQ.TABLE", @@ -639,6 +648,10 @@ def faulty_multiple_pcap_responses(): ), } return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key(GetFieldInfo(block="PCAP1", extended_metadata=True)): repeat( pcap_info ), @@ -681,6 +694,10 @@ def standard_responses_no_panda_update(table_field_info, table_data_1): Used to test if the softioc can be started. """ return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat( { "TRIG_EDGE": EnumFieldInfo( @@ -737,6 +754,10 @@ def standard_responses(table_field_info, table_data_1, table_data_2): GetChanges is polled at 10Hz if a different command isn't made. """ return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat( { "TRIG_EDGE": EnumFieldInfo( diff --git a/tests/test_connection.py b/tests/test_connection.py index c5598080..67f643be 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -13,6 +13,7 @@ Arm, ChangeGroup, Disarm, + Get, GetBlockInfo, GetChanges, GetFieldInfo, @@ -47,6 +48,10 @@ async def test_no_panda_found_connection_error(): def panda_disconnect_responses(table_field_info, table_data_1, table_data_2): # The responses return nothing, as the panda disconnects after introspection return { + command_to_key(Get(field="*IDN")): repeat( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8 \ + rootfs: PandA 3.1a1-1-g22fdd94" + ), command_to_key(GetFieldInfo(block="PCAP", extended_metadata=True)): repeat( { "TRIG_EDGE": EnumFieldInfo( From 7b6f2e1641921526b3576edc6bd2149f3bbba845 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Thu, 14 Nov 2024 13:36:22 -0500 Subject: [PATCH 7/8] Fix issue with type annotation --- src/pandablocks_ioc/ioc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index 0d6c6855..bb29b9b2 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -1865,7 +1865,7 @@ def create_block_records( return record_dict - def create_version_records(self, fw_vers_dict: dict[str, str]): + def create_version_records(self, fw_vers_dict: dict[EpicsName, str]): """Creates handful of records for tracking versions of IOC/Firmware via EPICS Args: From 5c8b2d49bcb86e7078b05cf6ada95716bd328ed3 Mon Sep 17 00:00:00 2001 From: Jakub Wlodek Date: Fri, 15 Nov 2024 10:23:46 -0500 Subject: [PATCH 8/8] Apply suggestions from review, move Get command outside of function, add paramterized tests --- src/pandablocks_ioc/ioc.py | 43 ++++++++++++++---------- tests/test_ioc.py | 68 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/src/pandablocks_ioc/ioc.py b/src/pandablocks_ioc/ioc.py index bb29b9b2..452ceb80 100644 --- a/src/pandablocks_ioc/ioc.py +++ b/src/pandablocks_ioc/ioc.py @@ -177,32 +177,38 @@ def create_softioc( asyncio.run_coroutine_threadsafe(client.close(), dispatcher.loop).result() -async def get_panda_versions( - client: AsyncioClient, -) -> dict[EpicsName, str]: - """Function that gets version information from the PandA using the IDN command +def get_panda_versions(idn_repsonse: str) -> dict[EpicsName, str]: + """Function that parses version info from the PandA's response to the IDN command + + See: https://pandablocks-server.readthedocs.io/en/latest/commands.html#system-commands Args: - client (AsyncioClient): Client used for commuication with the PandA + idn_response (str): Response from PandA to Get(*IDN) command Returns: - Dict[str, str]: Dictionary mapping firmware name to version + dict[EpicsName, str]: Dictionary mapping firmware record name to version """ - idn = await client.send(Get("*IDN")) - # Currently, IDN reports sw, fpga, and rootfs versions firmware_versions = {"PandA SW": "Unknown", "FPGA": "Unknown", "rootfs": "Unknown"} # If the *IDN response contains too many keys, break and leave versions as "Unknown" - if len(idn.split(":")) / 2 > len(list(firmware_versions.keys())): - logging.error(f"Version string {idn} recieved from PandA could not be parsed!") + # Since spaces are used to deliminate versions and can also be in the keys and + # values, if an additional key is present that we don't explicitly handle, + # our approach of using regex matching will not work. + if sum(name in idn_repsonse for name in firmware_versions) < idn_repsonse.count( + ":" + ): + logging.error( + f"Recieved unexpected version numbers in version string {idn_repsonse}!" + ) else: for firmware_name in firmware_versions: pattern = re.compile( - rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?:{"|".join(map(re.escape, firmware_versions))}):|$)' # noqa: E501 + rf'{re.escape(firmware_name)}:\s*([^:]+?)(?=\s*\b(?: \ + {"|".join(map(re.escape, firmware_versions))}):|$)' ) - if match := pattern.search(idn): + if match := pattern.search(idn_repsonse): firmware_versions[firmware_name] = match.group(1).strip() logging.info( f"{firmware_name} Version: {firmware_versions[firmware_name]}" @@ -1865,11 +1871,11 @@ def create_block_records( return record_dict - def create_version_records(self, fw_vers_dict: dict[EpicsName, str]): + def create_version_records(self, firmware_versions: dict[EpicsName, str]): """Creates handful of records for tracking versions of IOC/Firmware via EPICS Args: - fw_vers_dict (dict[str, str]): Dictionary mapping firmwares to versions + firmware_versions (dict[str, str]): Dictionary mapping firmwares to versions """ system_block_prefix = "SYSTEM" @@ -1885,7 +1891,7 @@ def create_version_records(self, fw_vers_dict: dict[EpicsName, str]): builder.stringIn, ) - for firmware_name, version in fw_vers_dict.items(): + for firmware_name, version in firmware_versions.items(): firmware_record_name = EpicsName( system_block_prefix + f":{firmware_name}_VERSION" ) @@ -1926,7 +1932,10 @@ async def create_records( """Query the PandA and create the relevant records based on the information returned""" - fw_vers_dict = await get_panda_versions(client) + # Get version information from PandA using IDN command + idn_response = await client.send(Get("*IDN")) + fw_vers_dict = get_panda_versions(idn_response) + (panda_dict, all_values_dict) = await introspect_panda(client) # Dictionary containing every record of every type @@ -1934,7 +1943,7 @@ async def create_records( record_factory = IocRecordFactory(client, record_prefix, all_values_dict) - # Add some top level records for version of IOC, FPGA, and software + # Add records for version of IOC, FPGA, and software to SYSTEM block record_factory.create_version_records(fw_vers_dict) # For each field in each block, create block_num records of each field diff --git a/tests/test_ioc.py b/tests/test_ioc.py index 6508c17f..faa9e7b2 100644 --- a/tests/test_ioc.py +++ b/tests/test_ioc.py @@ -39,6 +39,7 @@ StringRecordLabelValidator, _RecordUpdater, _TimeRecordUpdater, + get_panda_versions, update, ) @@ -840,3 +841,70 @@ class MockConnectionStatus: # unreliable number of calls to the set method. record_info.record.set.assert_any_call(True) record_info.record.set.assert_any_call(0) + + +@pytest.mark.parametrize( + "sample_idn_response, expected_output, expected_log_messages", + [ + ( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 " + "07d202f8 rootfs: PandA 3.1a1-1-g22fdd94", + { + EpicsName("PANDA_SW"): "3.0-11-g6422090", + EpicsName("FPGA"): "3.0.0C4 86e5f0a2 07d202f8", + EpicsName("ROOTFS"): "PandA 3.1a1-1-g22fdd94", + }, + [], + ), + ( + "PandA SW: 3.0-11-g6422090 FPGA: 3.0.0C4 86e5f0a2 07d202f8", + { + EpicsName("PANDA_SW"): "3.0-11-g6422090", + EpicsName("FPGA"): "3.0.0C4 86e5f0a2 07d202f8", + EpicsName("ROOTFS"): "Unknown", + }, + ["Failed to get rootfs version information!"], + ), + ( + "PandA SW: 3.0-11-g6422090 rootfs: PandA 3.1a1-1-g22fdd94", + { + EpicsName("PANDA_SW"): "3.0-11-g6422090", + EpicsName("FPGA"): "Unknown", + EpicsName("ROOTFS"): "PandA 3.1a1-1-g22fdd94", + }, + ["Failed to get FPGA version information!"], + ), + ( + "", + { + EpicsName("PANDA_SW"): "Unknown", + EpicsName("FPGA"): "Unknown", + EpicsName("ROOTFS"): "Unknown", + }, + [ + "Failed to get PandA SW version information!", + "Failed to get FPGA version information!", + "Failed to get rootfs version information!", + ], + ), + ( + "FPGA: 3.0.0C4 86e5f0a2 07d202f8 " + "Hello World: 12345 rootfs: PandA 3.1a1-1-g22fdd94", + { + EpicsName("PANDA_SW"): "Unknown", + EpicsName("FPGA"): "Unknown", + EpicsName("ROOTFS"): "Unknown", + }, + [ + "Recieved unexpected version numbers", + ], + ), + ], +) +def test_get_version_information( + sample_idn_response, expected_output, expected_log_messages, caplog +): + parsed_firmware_versions = get_panda_versions(sample_idn_response) + assert parsed_firmware_versions == expected_output + for log_message in expected_log_messages: + assert log_message in caplog.text