From ee3a0836eb81e7350413e8facf9ef22fdd6fb45d Mon Sep 17 00:00:00 2001 From: croy Date: Mon, 3 Feb 2025 17:01:31 -0500 Subject: [PATCH] SystemTableParser to support multi-line Enhancing the algorithm used by the SystemTableParser to support broken lines and multi-line parsing. Change-Id: Iacce397078f88ff4c6360ff907f1dff4f034abf2 --- .../system_host_label_assign_output.py | 10 +- .../system/system_table_parser.py | 93 ++++++++++++++----- unit_tests/parser/system_parser_test.py | 43 ++++++++- 3 files changed, 113 insertions(+), 33 deletions(-) diff --git a/keywords/cloud_platform/system/host/objects/system_host_label_assign_output.py b/keywords/cloud_platform/system/host/objects/system_host_label_assign_output.py index 8357894e..77b5d836 100644 --- a/keywords/cloud_platform/system/host/objects/system_host_label_assign_output.py +++ b/keywords/cloud_platform/system/host/objects/system_host_label_assign_output.py @@ -42,7 +42,6 @@ class SystemHostLabelAssignOutput: # {'Property': 'host_uuid', 'Value': '4feff42d-bc8d-4006-9922-a7fa10a6ee19'} # {'Property': 'label_key', 'Value': 'kube-cpu-mgr-policy'} # {'Property': 'label_value', 'Value': 'static'} - # {'Property': 'Property', 'Value': 'Value'} ###### SEPERATOR ROW ###### # {'Property': 'uuid', 'Value': '0260240f-bcca-41f5-9f32-b3acc030dfb0'} # {'Property': 'host_uuid', 'Value': '4feff42d-bc8d-4006-9922-a7fa10a6ee19'} # {'Property': 'label_key', 'Value': 'kube-topology-mgr-policy'} @@ -51,13 +50,10 @@ class SystemHostLabelAssignOutput: system_host_label_object = SystemHostLabelObject() for value in output_values: - # A middle row with 'Property' means that we are changing entry. - if value['Property'] == 'Property': - self.system_host_labels.append(system_host_label_object) + if value['Property'] == 'uuid': # Every time we hit a uuid, we are encountering a new object. system_host_label_object = SystemHostLabelObject() - - if value['Property'] == 'uuid': system_host_label_object.set_uuid(value['Value']) + self.system_host_labels.append(system_host_label_object) if value['Property'] == 'host_uuid': system_host_label_object.set_host_uuid(value['Value']) @@ -68,8 +64,6 @@ class SystemHostLabelAssignOutput: if value['Property'] == 'label_value': system_host_label_object.set_label_value(value['Value']) - self.system_host_labels.append(system_host_label_object) - def get_all_host_labels(self) -> [SystemHostLabelObject]: """ Gets all the host label objects that were assigned diff --git a/keywords/cloud_platform/system/system_table_parser.py b/keywords/cloud_platform/system/system_table_parser.py index c4a9e462..a14262da 100644 --- a/keywords/cloud_platform/system/system_table_parser.py +++ b/keywords/cloud_platform/system/system_table_parser.py @@ -5,6 +5,21 @@ from framework.logging.automation_logger import get_logger class SystemTableParser: """ Class for System table parsing + + Sample Table: + '+--------------------------------------+---------+-----------+---------------+\n' + '| uuid | name | ptp_insta | parameters |\n' + '| | | nce_name | |\n' + '+--------------------------------------+---------+-----------+---------------+\n' + "| 0000c96e-6dab-48c2-875a-48af194c893c | n4_p2 | ptp4 | ['masterOnly= |\n" + "| | | | 1'] |\n" + '| | | | |\n' + '| 24003e49-f9c4-4794-970e-506fa5c215c0 | n1_if | clock1 | [] |\n' + '| 51e06821-b045-4a6e-854b-6bd829b5c9e2 | ptp1if1 | ptp1 | [] |\n' + "| a689d398-329f-46b4-a99f-23b9a2417c27 | n5_p2 | ptp5 | ['masterOnly= |\n" + "| | | | 1'] |\n" + '| | | | |\n' + '+--------------------------------------+---------+-----------+---------------+\n' """ def __init__(self, system_output): @@ -19,28 +34,60 @@ class SystemTableParser: headers = [] output_values_list = [] - found_headers = False + last_output_value = None + + is_in_headers_block = False + is_in_content_block = False + number_of_columns = -1 + for line in self.system_output: - # output that we care about like headers and actual output have | separators - if line.__contains__('|'): - # find the headers first - if not found_headers: - headers = line.split('|')[1:-1] - found_headers = True - else: - output_values = {} - values = line.split('|')[1:-1] - if len(headers) != len(values): - get_logger().log_error( - f"Number of headers was {len(headers)} " - f"but the number of values was {len(values)}. " - f"Full output was {self.system_output}" - ) - raise KeywordException("Number of headers and values do not match") - index = 0 - for header in headers: - # create dictionary with header and value - output_values[header.strip()] = values[index].strip() - index = index + 1 - output_values_list.append(output_values) + + # We have hit a separator which enters Headers, Content, or Ends the table + if line.__contains__('+'): + + # Find out in which part of the table we are, based on the "+----" separator. + if not is_in_headers_block and not is_in_content_block: # First separator -> Enter Headers + is_in_headers_block = True + is_in_content_block = False + number_of_columns = line.count('+') - 1 + headers = [""] * number_of_columns + continue # This is a separator line, don't try to parse anything. + elif is_in_headers_block and not is_in_content_block: # Second separator -> Go to Content + is_in_headers_block = False + is_in_content_block = True + continue # This is a separator line, don't try to parse anything. + else: # Last separator -> The table is complete + is_in_headers_block = False + is_in_content_block = False + + # Build the list of headers, which could be multi-lined. + if is_in_headers_block: + headers_line = line.split('|')[1:-1] + if len(headers_line) != number_of_columns: + get_logger().log_error(f"Number of headers should be {number_of_columns} based on the number of '+' but the number of values was {len(headers_line)}.") + raise KeywordException("Number of headers and + separator do not match expected value") + + for i in range(number_of_columns): + headers[i] += headers_line[i].strip() + + # Build the list of values, which could be multi-lined. + if is_in_content_block: + values_line = line.split('|')[1:-1] + if len(values_line) != number_of_columns: + get_logger().log_error(f"Number of values should be {number_of_columns} based on the number of '+' but the number of values was {len(values_line)}.") + raise KeywordException("Number of headers and values do not match expected value") + + # If there is a value in the first column, then this is a new entry. + if values_line[0].strip(): + + # Build a dictionary of the Header:Value + last_output_value = {} + for i in range(number_of_columns): + last_output_value[headers[i]] = values_line[i].strip() + output_values_list.append(last_output_value) + + else: # Otherwise, this is the continuation of the previous line. + for i in range(number_of_columns): + last_output_value[headers[i]] += values_line[i].strip() + return output_values_list diff --git a/unit_tests/parser/system_parser_test.py b/unit_tests/parser/system_parser_test.py index 64d03d4d..e6588535 100644 --- a/unit_tests/parser/system_parser_test.py +++ b/unit_tests/parser/system_parser_test.py @@ -164,6 +164,21 @@ system_application_upload = [ "Please use 'system application-list' or 'system application-show hello-kitty' to view the current progress.\n", ] +system_host_if_ptp_remove_wrapped_output = [ + '+--------------------------------------+---------+-----------+---------------+\n', + '| uuid | name | ptp_insta | parameters |\n', + '| | | nce_name | |\n', + '+--------------------------------------+---------+-----------+---------------+\n', + "| 0000c96e-6dab-48c2-875a-48af194c893c | n4_p2 | ptp4 | ['masterOnly= |\n", + "| | | | 1'] |\n", + '| | | | |\n', + '| 24003e49-f9c4-4794-970e-506fa5c215c0 | n1_if | clock1 | [] |\n', + '| 51e06821-b045-4a6e-854b-6bd829b5c9e2 | ptp1if1 | ptp1 | [] |\n', + "| a689d398-329f-46b4-a99f-23b9a2417c27 | n5_p2 | ptp5 | ['masterOnly= |\n", + "| | | | 1'] |\n", + '| | | | |\n', + '+--------------------------------------+---------+-----------+---------------+\n', +] def test_system_parser(): """ @@ -218,7 +233,7 @@ def test_system_parser_error(): SystemTableParser(system_output_error).get_output_values_list() assert False, "There should be an exception when parsing the output." except KeywordException as e: - assert e.args[0] == 'Number of headers and values do not match' + assert e.args[0] == 'Number of headers and + separator do not match expected value' def test_system_host_output(): @@ -253,7 +268,7 @@ def test_system_host_output_error(): SystemHostOutput(system_output_error).get_hosts() assert False, "There should be an exception when we parse the output." except KeywordException as e: - assert e.args[0] == 'Number of headers and values do not match' + assert e.args[0] == 'Number of headers and + separator do not match expected value' def test_system_application_output(): @@ -276,6 +291,30 @@ def test_system_application_output(): assert application.get_status() == 'applied' assert application.get_progress() == 'completed' +def test_system_table_parser_with_wrapped_table_entry(): + """ + Test the system vertical parser with a table that has a column wrapped. + Returns: + + """ + + system_vertical_table_parser = SystemTableParser(system_host_if_ptp_remove_wrapped_output) + list_of_values = system_vertical_table_parser.get_output_values_list() + assert len(list_of_values) == 4 + + first_entry = list_of_values[0] + assert len(first_entry.keys()) == 4 + assert first_entry['uuid'] == '0000c96e-6dab-48c2-875a-48af194c893c' + assert first_entry['name'] == 'n4_p2' + assert first_entry['ptp_instance_name'] == 'ptp4' + assert first_entry['parameters'] == "['masterOnly=1']" + + second_entry = list_of_values[1] + assert len(second_entry.keys()) == 4 + assert second_entry['uuid'] == '24003e49-f9c4-4794-970e-506fa5c215c0' + assert second_entry['name'] == 'n1_if' + assert second_entry['ptp_instance_name'] == 'clock1' + assert second_entry['parameters'] == "[]" def test_system_host_label_assign_output(): """