From f421f1bdc930a944eb5f9b728517f6f59f5b3359 Mon Sep 17 00:00:00 2001 From: twu6 Date: Tue, 9 Jan 2024 14:38:20 -0600 Subject: [PATCH] update CTF v1.8.2 release --- ReadMe.md | 33 ++++-- ctf | 4 +- .../config_required/Test_CTF_CFS_Restart.json | 65 ++++++++++++ .../Test_CTF_Variable_Plugin.json | 12 ++- lib/ctf_utility.py | 31 +++++- lib/plugin_manager.py | 5 + .../readers/ccdd_export_reader.py | 75 ++++++++++++- .../tests/readers/test_ccdd_export_reader.py | 100 +++++++++++++++++- plugins/cfs/cfs_config.py | 38 +++++-- plugins/cfs/pycfs/cfs_controllers.py | 54 +++++++--- plugins/cfs/pycfs/cfs_interface.py | 40 ++++--- plugins/cfs/pycfs/local_cfs_interface.py | 2 +- plugins/cfs/pycfs/output_app_interface.py | 21 ++-- plugins/cfs/pycfs/remote_cfs_interface.py | 1 + .../cfs/tests/pycfs/test_cfs_controllers.py | 74 ++++++++++++- plugins/cfs/tests/pycfs/test_cfs_interface.py | 8 +- .../tests/pycfs/test_output_app_interface.py | 34 +++--- requirements.txt | 29 +++-- run_tests.sh | 1 + unit_tests/lib/test_ctf_utility.py | 27 +++++ unit_tests/lib/test_logger.py | 9 ++ vv_tests/configs/ctf_vv_config.ini | 13 +++ 22 files changed, 569 insertions(+), 107 deletions(-) create mode 100644 example_scripts/config_required/Test_CTF_CFS_Restart.json diff --git a/ReadMe.md b/ReadMe.md index deb75f6..0a31063 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -21,6 +21,32 @@ in the `CTF Documentation` directory of the CTF releases' Assets (https://github ## Release Notes +### v1.8.2 +11/20/2023 + +* CTF Core Changes + + * Add an optional logging configuration `csv_tlm_log`. If True, CTF creates an additional tlm CSV log file. + + * Minor improvements and bug fixes. + +* CTF Plugins Changes + + * Updates to CFS Plugin + * Display full data attributes of nested structures in tlm logs. + + * Resolve user-defined variables for all test instructions. + + * Fix a regression with shutdown and restart of CFS targets in the same test script. + + * Change CFS shutdown behavior to only shut down targets automatically if the target was started by `StartCfs`. + +### v1.8.1 +09/15/2023 + +* CTF Plugins Changes + * Fix a regression in the CFS Plugin that caused CTF to not communicate with CFS targets which were not started via `StartCfs`. + ### v1.8 09/27/2023 * CTF Core Changes @@ -49,12 +75,7 @@ in the `CTF Documentation` directory of the CTF releases' Assets (https://github * Fix CCSDS v1 message header issue. * Fix a regression issue of CCDD JSON reader not reusing known data types referenced by name. - - * Updates to Gateway Plugin - * Improve `SendCfsCommandWithAggregatedPayload` instruction to allow different targets in payload, and its syntax. - - * Fix inner command header attribute assignment issue of test instruction. - + * CTF Tool and Scripts Changes * Consolidate unit tests for open source release and internal release. diff --git a/ctf b/ctf index c82a788..3993f96 100755 --- a/ctf +++ b/ctf @@ -36,8 +36,8 @@ from lib.ctf_utility import expand_path from lib.logger import logger as log, set_logger_options_from_config, change_log_file from lib.exceptions import CtfTestError -CTF_VERSION = "v1.8" -CTF_RELEASE_DATE = "Sep 27 2023" +CTF_VERSION = "v1.8.2" +CTF_RELEASE_DATE = "Nov 20 2023" def main(): diff --git a/example_scripts/config_required/Test_CTF_CFS_Restart.json b/example_scripts/config_required/Test_CTF_CFS_Restart.json new file mode 100644 index 0000000..043b6a1 --- /dev/null +++ b/example_scripts/config_required/Test_CTF_CFS_Restart.json @@ -0,0 +1,65 @@ +{ + "test_script_number": "CTF-CFS-Restart-Test", + "test_script_name": "Test_CTF_CFS_Restart.json", + "owner": "CTF", + "description": "CTF Example Script showing use of a CFS target after shutdown and restart", + "requirements": { + "MyRequirement": "N/A" + }, + "ctf_options": { + "verify_timeout": 4 + }, + "test_setup": "", + "import": {}, + "tests": [ + { + "test_number": "CFS-Restart-Test-1", + "description": "", + "instructions": [ + { + "instruction": "ShutdownCfs", + "data": { + "target": "tgt1" + } + }, + { + "instruction": "StartCfs", + "data": { + "target": "tgt1" + }, + "wait": 3 + }, + { + "instruction": "EnableCfsOutput", + "data": { + "target": "tgt1" + }, + "wait": 90 + }, + { + "instruction": "SendCfsCommand", + "data": { + "target": "tgt1", + "mid": "TO_CMD_MID", + "cc": "TO_RESET_CC", + "args": {} + } + }, + { + "instruction": "CheckTlmValue", + "data": { + "target": "tgt1", + "mid": "TO_HK_TLM_MID", + "args": [ + { + "compare": "==", + "variable": "usCmdCnt", + "value": 0 + } + ] + } + } + ] + } + ] +} diff --git a/functional_tests/plugin_tests/Test_CTF_Variable_Plugin.json b/functional_tests/plugin_tests/Test_CTF_Variable_Plugin.json index e7ef979..f020087 100644 --- a/functional_tests/plugin_tests/Test_CTF_Variable_Plugin.json +++ b/functional_tests/plugin_tests/Test_CTF_Variable_Plugin.json @@ -20,7 +20,7 @@ "data": { "variable_name": "my_var", "operator": "=", - "value": 13, + "value": "0xD", "variable_type": "int" } }, @@ -57,6 +57,14 @@ "variable_type": "int" } }, + { + "instruction": "CheckUserVariable", + "data": { + "variable_name": "my_var", + "operator": "==", + "value": 9 + } + }, { "instruction": "SetUserVariable", "data": { @@ -103,7 +111,7 @@ "data": { "variable_name": "my_var", "operator": "==", - "value": 244 + "value": "0xF4" } }, { diff --git a/lib/ctf_utility.py b/lib/ctf_utility.py index e1f5a08..33d9d6b 100644 --- a/lib/ctf_utility.py +++ b/lib/ctf_utility.py @@ -100,6 +100,14 @@ def set_variable(variable_name, op_code, value, variable_type=None): if variable_type and variable_type in type_map: user_passed_type = type_map[variable_type] + # cast values from hex to int conversion + if variable_type == 'int' and isinstance(value, str): + try: + value = int(value, 0) + except ValueError as exception: + log.error('Could not cast {} to int type, trigger exception {}'.format(value, exception)) + return False + if op_code == "=": if variable_type: if variable_type in type_map: @@ -129,9 +137,6 @@ def set_variable(variable_name, op_code, value, variable_type=None): if user_passed_type and not isinstance(value, user_passed_type): log.info("Converting value {} to type {}".format(value, user_passed_type.__name__)) - # add a special case for int hex conversion - if isinstance(value, str) and variable_type == "int" and ("x" in value or "X" in value): - value = int(value, 16) try: value = user_passed_type(value) except ValueError: @@ -200,6 +205,26 @@ def resolve_variable(variable): return variable +def resolve_dic_variable(var_obj: dict): + """ + Recursively resolve the user defined variables in dictionary object. + The function resolve_variable only resolve str type variables. + """ + if not isinstance(var_obj, dict): + return var_obj + new_object = dict() + for key, value in var_obj.items(): + resolved_key = resolve_variable(key) + if isinstance(value, dict): + resolved_value = resolve_dic_variable(value) + elif isinstance(value, list): + resolved_value = [resolve_dic_variable(v) for v in value] + else: + resolved_value = resolve_variable(value) + new_object[resolved_key] = resolved_value + return new_object + + INDEX_PATTERN = r'\[(.*?)\]' diff --git a/lib/plugin_manager.py b/lib/plugin_manager.py index 79d2582..573dc64 100644 --- a/lib/plugin_manager.py +++ b/lib/plugin_manager.py @@ -52,6 +52,7 @@ from inspect import signature from lib.ctf_global import Global +from lib.ctf_utility import resolve_dic_variable from lib.exceptions import CtfTestError from lib.logger import logger as log @@ -131,6 +132,10 @@ def process_command(self, **kwargs): (required + optional) """ result = False + + # resolve variables in kwargs, so that individual instruction does not need to process the argument + kwargs = resolve_dic_variable(kwargs) + instruction = kwargs["instruction"] if "data" in kwargs.keys(): data = kwargs["data"] diff --git a/plugins/ccsds_plugin/readers/ccdd_export_reader.py b/plugins/ccsds_plugin/readers/ccdd_export_reader.py index a03c534..20eaf46 100644 --- a/plugins/ccsds_plugin/readers/ccdd_export_reader.py +++ b/plugins/ccsds_plugin/readers/ccdd_export_reader.py @@ -23,11 +23,21 @@ import fnmatch import json import ctypes +from pprint import pformat from lib.logger import logger as log from lib.exceptions import CtfTestError +from lib.ctf_global import Global from plugins.ccsds_plugin.ccsds_interface import CCSDSInterface +try: + TLM_FORMATTER = Global.config.get("logging", "tlm_formatter", fallback=None) + PPRINT_DEPTH = int(Global.config.get("logging", "pprint_depth", fallback="7")) +except (AttributeError, ValueError) as exception: + log.error("Exception from getting INI logging config") + TLM_FORMATTER = "compact" + PPRINT_DEPTH = 7 + # Helper Functions def ctypes_name(name): @@ -53,6 +63,56 @@ def dynamic_init(self, *args, **kwargs): self.__dict__.update(kwargs) +def build_obj_from_ctype(ctype_object): + # pylint: disable=protected-access + """ + Build a python dictionary object from a ctypes structure object with mapped attributes. + + @note - Utility function used by __str__ for formatted output. + + @param ctype_object: The ctypes structure object instance being represented + """ + shadow_obj = dict() + key = ctype_object.__class__.__name__ + if isinstance(ctype_object, ctypes.Structure): + val = dict() + for field in ctype_object._fields_: + val[field[0]] = build_obj_from_ctype(getattr(ctype_object, field[0])) + shadow_obj[key] = val + elif isinstance(ctype_object, ctypes.Array): + lst = [build_obj_from_ctype(field) for field in ctype_object] + shadow_obj[key] = lst + else: + return ctype_object + + return shadow_obj + + +def build_str_from_ctype(ctype_object): + # pylint: disable=protected-access + """ + Build a string from a ctype object that prints name-value pairs of each field. + + @note - Utility function used by __str__ for formatted output. + + @param ctype_object: The ctypes structure object instance being represented + """ + + max_shown_array = 1000 + string = "" + name = ctype_object.__class__.__name__ + if hasattr(ctype_object, '_fields_'): + string = "{}: {{{}}}".format(name, ", ".join(["{}: {}".format( + field[0], build_str_from_ctype(getattr(ctype_object, field[0]))) for field in ctype_object._fields_])) + elif hasattr(ctype_object, '_length_'): + string = "[{}{}]".format(", ".join([build_str_from_ctype(e) for e in ctype_object[:max_shown_array]]), + f"... ({len(ctype_object) - max_shown_array} more)" + if len(ctype_object) > max_shown_array else "") + else: + string = str(ctype_object) + return string + + def to_string(self): """ A generic implementation of __str__ for a dynamic type that prints name-value pairs for each of its fields. @@ -61,9 +121,12 @@ def to_string(self): @param self: The object instance being represented """ - string = "{}: {{{}}}".format(self.__class__.__name__, - ", ".join(["{}: {}".format(field[0], getattr(self, field[0])) - for field in self._fields_])) # pylint: disable=protected-access + string = "" + if TLM_FORMATTER == "pprint": + dict_obj = build_obj_from_ctype(self) + string = pformat(dict_obj, sort_dicts=False, depth=PPRINT_DEPTH, width=400) + else: + string = build_str_from_ctype(self) return string @@ -458,8 +521,10 @@ def process_types(self, json_list): mid['mid_name'], self.mids[mid['mid_name']])) if int(mid['mid_value'], 0) in self.mids.values(): log.error("Found duplicate MID value: {}".format(mid['mid_value'])) - log.debug("Update mids dict with key:{} value:{} ".format(mid['mid_name'], - int(mid['mid_value'], 0))) + log.debug("Update mids dict with key:{} value:{} ({})".format(mid['mid_name'], + int(mid['mid_value'], 0), + hex(int(mid['mid_value'],0))) + ) self.mids.update({mid['mid_name']: int(mid['mid_value'], 0)}) else: log.error("Invalid type definition in {}".format(self.current_file_name)) diff --git a/plugins/ccsds_plugin/tests/readers/test_ccdd_export_reader.py b/plugins/ccsds_plugin/tests/readers/test_ccdd_export_reader.py index fbcd989..cb4099c 100644 --- a/plugins/ccsds_plugin/tests/readers/test_ccdd_export_reader.py +++ b/plugins/ccsds_plugin/tests/readers/test_ccdd_export_reader.py @@ -16,11 +16,14 @@ import ctypes from unittest.mock import patch import pytest +from importlib import reload import lib +import plugins +from lib.ctf_global import Global from lib.exceptions import CtfTestError -from plugins.ccsds_plugin.readers.ccdd_export_reader import CCDDExportReader, ctypes_name, \ - dynamic_init, create_type_class, to_string, _compare_field, _compare_ctypes +from plugins.ccsds_plugin.readers.ccdd_export_reader import CCDDExportReader, ctypes_name, dynamic_init,\ + create_type_class, to_string, _compare_field, _compare_ctypes, build_obj_from_ctype, build_str_from_ctype from plugins.cfs.cfs_config import CfsConfig @@ -40,6 +43,83 @@ def test_func_ctypes_name(): assert ctypes_name('byte') == 'c_byte' +def test_build_obj_from_ctype(): + """ + Test function: build_obj_from_ctype + Converts a Python type name to an equivalent ctypes type name by prepending 'c_' and stripping '_t' + """ + + class Test_Struct_1(ctypes.BigEndianStructure): + _pack_ = 1 + _fields_ = [("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("float", ctypes.c_float), + ("double", ctypes.c_double), + ("array", ctypes.c_uint8 * 10) + ] + struct1 = Test_Struct_1() + struct1.byte = 9; struct1.word = 987; struct1.float = 1.23; struct1.double = 6.789 + struct1.array[1] = 5; struct1.array[2] = 6 + dict_obj = build_obj_from_ctype(struct1) + + class Test_Struct_2(ctypes.BigEndianStructure): + _pack_ = 1 + _fields_ = [("struct1", Test_Struct_1), + ("int", ctypes.c_uint32), + ("double", ctypes.c_double), + ("struct1_array", Test_Struct_1 * 2) + ] + struct2 = Test_Struct_2() + struct2.struct1.byte = 11; struct2.struct1.word = 456; struct2.struct1.float = 1234.567; + struct2.struct1_array[1].word = 76 + dict_obj2 = build_obj_from_ctype(struct2) + assert dict_obj["Test_Struct_1"]["byte"] == 9 + assert dict_obj["Test_Struct_1"]["word"] == 987 + assert abs(dict_obj["Test_Struct_1"]["float"] - 1.23) < 0.0001 + assert abs(dict_obj["Test_Struct_1"]["double"] - 6.789) < 0.0001 + assert dict_obj["Test_Struct_1"]["array"]["c_ubyte_Array_10"][1] == 5 + assert dict_obj["Test_Struct_1"]["array"]["c_ubyte_Array_10"][2] == 6 + assert dict_obj2["Test_Struct_2"]["struct1"]["Test_Struct_1"]["byte"] == 11 + assert dict_obj2["Test_Struct_2"]["struct1"]["Test_Struct_1"]["word"] == 456 + assert abs(dict_obj2["Test_Struct_2"]["struct1"]["Test_Struct_1"]["float"] - 1234.567) < 0.0001 + assert dict_obj2["Test_Struct_2"]["struct1_array"]["Test_Struct_1_Array_2"][1]["Test_Struct_1"]["word"] == 76 + + +def test_build_str_from_ctype(): + """ + Test function: build_obj_from_ctype + Converts a Python type name to an equivalent ctypes type name by prepending 'c_' and stripping '_t' + """ + + class Test_Struct_1(ctypes.BigEndianStructure): + _pack_ = 1 + _fields_ = [("byte", ctypes.c_uint8), + ("word", ctypes.c_uint16), + ("double", ctypes.c_double), + ("array", ctypes.c_uint8 * 10) + ] + struct1 = Test_Struct_1() + struct1.byte = 9; struct1.word = 987; struct1.double = 6.789 + struct1.array[1] = 5; struct1.array[2] = 6 + dict_str = build_str_from_ctype(struct1) + assert dict_str == "Test_Struct_1: {byte: 9, word: 987, double: 6.789, " \ + "array: [0, 5, 6, 0, 0, 0, 0, 0, 0, 0]}" + + class Test_Struct_2(ctypes.BigEndianStructure): + _pack_ = 1 + _fields_ = [("struct1", Test_Struct_1), + ("int", ctypes.c_uint32), + ("struct1_array", Test_Struct_1 * 2) + ] + struct2 = Test_Struct_2() + struct2.struct1.byte = 11; struct2.struct1.word = 456; struct2.struct1_array[1].word = 76 + dict_str = build_str_from_ctype(struct2) + assert dict_str == "Test_Struct_2: {struct1: Test_Struct_1: {byte: 11, word: 456, double: 0.0, array: " \ + "[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, int: 0, struct1_array: " \ + "[Test_Struct_1: {byte: 0, word: 0, double: 0.0, array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}, " \ + "Test_Struct_1: {byte: 0, word: 76, double: 0.0, array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]}]}" + + def test_func_create_type_class(utils): """ Test function: create_type_class @@ -211,7 +291,7 @@ def test_ccdd_export_reader_process_command_exception(ccdd_export_reader, utils) json_dict = { "cmd_mid_name": "INVALID_CMD_MID", "cmd_codes": - [{"cc_name": "INVALID", "cc_value": "0", "cc_data_type": "INVALID", "cc_parameters": []}] + [{"cc_name": "INVALID", "cc_value": "0", "cc_data_type": "INVALID", "cc_parameters": []}] } utils.clear_log() with pytest.raises(CtfTestError): @@ -303,7 +383,7 @@ def test_ccdd_export_reader_build_data_type_and_field_multiply(ccdd_export_reade fields = [] ccdd_export_reader.type_dict = {'int32': ctypes.c_int32, 'int64': ctypes.c_int64, 'uint8': ctypes.c_ubyte, 'TRUE': 1} - assert ccdd_export_reader._build_data_type_and_field(param, fields) is ctypes.c_ubyte * 1 + assert ccdd_export_reader._build_data_type_and_field(param, fields) is ctypes.c_ubyte * 1 def test_ccdd_export_reader_build_data_type_and_field_type_in_subtype(ccdd_export_reader): @@ -632,7 +712,6 @@ def test_ccdd_export_reader_process_custom_types(ccdd_export_reader): assert hasattr(ccdd_export_reader.type_dict["my_outer_type"]().inner, "myArg") - def test_ccdd_export_reader_process_custom_types_errors(ccdd_export_reader): # invalid name key json_dict = { @@ -728,3 +807,14 @@ def test_ccdd_export_reader_process_ccsds_get_ccsds_messages_from_dir(ccdd_expor mid_map, macro_map = ccdd_export_reader.get_ccsds_messages_from_dir(directory) assert len(mid_map) > 0 assert len(macro_map) > 0 + + +def test_ctypes_create_to_str(): + Global.config.set('logging', 'tlm_formatter', 'pprint') + # reload module to set tlm_formatter to 'pprint' + reload(plugins.ccsds_plugin.readers.ccdd_export_reader) + type_created = create_type_class("arraystruct", ctypes.Structure, [("bytearray", ctypes.c_uint8 * 2)]) + # convert dynamic type obj to string + obj_type = type_created() + assert str(obj_type) == "{'arraystruct': {'bytearray': {'c_ubyte_Array_2': [0, 0]}}}" + Global.config.set('logging', 'tlm_formatter', 'None') diff --git a/plugins/cfs/cfs_config.py b/plugins/cfs/cfs_config.py index 8022889..f630906 100644 --- a/plugins/cfs/cfs_config.py +++ b/plugins/cfs/cfs_config.py @@ -80,6 +80,8 @@ def __init__(self, name): # The following variable is set by the CCSDS Reader Implementation self.ccsds_header_info_included = None self.telemetry_debug = None + self.csv_tlm_log = None + self.send_keepalive_msg = None self.crc = None try: @@ -102,36 +104,46 @@ def configure(self, name): log.debug(traceback.format_exc()) raise CtfTestError("Error in configure") from exception - def load_field(self, section, field_name, config_getter, validate_function=None): + def load_optional_field(self, section, field_name, config_getter, default_value=None, validate_function=None): """ - Interpret field attribute of loaded CFS target config section. + Interpret field attribute of loaded CFS target config section. If not found, return default_value. @param section: loaded Json CFS target section. @param field_name: the field name for loaded attribute. @param config_getter: the function to get an option value for a given section. + @param default_value: the default value for loaded attribute, if not found. @param validate_function: the function to validate the field attribute (Optional). @return Any: field attribute with matching name """ - value = config_getter(section, field_name, fallback=None) if value is None and section != "cfs": log.info("Config Value {}:{} does not exist or is not the right type. " "Attempting to load from base section [cfs].".format(section, field_name)) value = config_getter("cfs", field_name, fallback=None) if value is None: - log.error("Config Value cfs:{} does not exist or is not the right type. " - "Needed to configure CFS.".format(field_name)) - self.validation.add_error("field {}".format(field_name)) - return None - if validate_function is not None: + log.info("Config Value {}:{} does not exist or is not the right type. " + "Use default value: {}".format(section, field_name, default_value )) + value = default_value + if validate_function is not None and value is not None: try: value = validate_function(value) except (TypeError, OSError, socket.error, IOError): + self.validation.add_error("field {}".format(field_name)) value = None + return value + def load_field(self, section, field_name, config_getter, validate_function=None): + """ + Interpret field attribute of loaded CFS target config section. If not found, the test instruction fails. + @param section: loaded Json CFS target section. + @param field_name: the field name for loaded attribute. + @param config_getter: the function to get an option value for a given section. + @param validate_function: the function to validate the field attribute (Optional). + @return Any: field attribute with matching name + """ + value = self.load_optional_field(section, field_name, config_getter, None, validate_function) if value is None: log.error("Invalid Config Value at {}:{}.".format(section, field_name)) - return None - + self.validation.add_error("field {}".format(field_name)) return value def load_config_data(self, section_name): @@ -223,6 +235,12 @@ def load_config_data(self, section_name): log.warning("No CFS configuration defined for {}".format(section_name)) self.validation.add_error("section {}".format(section_name)) + self.csv_tlm_log = self.load_optional_field("logging", "csv_tlm_log", Global.config.getboolean,False, + self.validation.validate_boolean) + + self.send_keepalive_msg = self.load_optional_field(section_name, "send_keepalive_msg", Global.config.getboolean, + False, self.validation.validate_boolean) + # The following variable is set by the CCSDS Reader Implementation self.ccsds_header_info_included = self.load_field("ccsds", "CCSDS_header_info_included", Global.config.getboolean, self.validation.validate_boolean) diff --git a/plugins/cfs/pycfs/cfs_controllers.py b/plugins/cfs/pycfs/cfs_controllers.py index ad41fbc..95b67eb 100644 --- a/plugins/cfs/pycfs/cfs_controllers.py +++ b/plugins/cfs/pycfs/cfs_controllers.py @@ -111,7 +111,9 @@ def initialize(self): log.debug("Initializing CfsController") if not self.process_ccsds_files(): return False + return self._init_cfs_interface() + def _init_cfs_interface(self): log.info("Starting Local CFS Interface to {}:{} for target {}" .format(self.config.cfs_target_ip, self.config.cmd_udp_port, self.config.name)) command = CommandInterface(self.ccsds, port=self.config.cmd_udp_port, ip=self.config.cfs_target_ip, @@ -123,7 +125,7 @@ def initialize(self): if not result: log.error("Failed to initialize LocalCfsInterface") else: - log.info("CfsController Initialized for target {}".format(self.config.name)) + log.info("LocalCfsInterface Initialized for target {}".format(self.config.name)) return result @@ -141,6 +143,12 @@ def start_cfs(self, run_args): it calls CfsController instance's start_cfs function. """ log.info("Starting CFS on {}".format(self.config.name)) + if not self.cfs: + log.debug("No CFS interface to Linux target {}".format(self.config.name)) + if not self._init_cfs_interface(): + log.error("Unable to start CFS!") + return False + result = {} try: result = self.cfs.start_cfs(run_args) @@ -450,7 +458,7 @@ def resolve_macros(self, arg): raise CtfParameterError("Unknown macro '{}' in arg {}. Use format #MACRO#".format(macro, arg), arg) return arg - # noinspection PyProtectedMember + # noinspection PyProtectedMember,PyUnresolvedReferences def resolve_simple_type(self, arg, arg_type): """ Implementation of helper function resolve_simple_type. @@ -467,7 +475,10 @@ def resolve_simple_type(self, arg, arg_type): raise CtfParameterError("Invalid value for bool: {}".format(arg), arg) elif arg_type in [ctypes.c_char, ctypes.c_char_p, ctypes.c_wchar, ctypes.c_wchar_p]: arg = str(arg).encode() - elif arg_type in [ctypes.c_float, ctypes.c_double, ctypes.c_longdouble]: + # ctypes.c_float and ctypes.c_double are aliased to a hidden type depending on target architecture + # ctypes.c_longdouble is not well-supported and does not correspond to any type used in CCDD + elif arg_type in [ctypes.c_float, ctypes.c_float.__ctype_be__, ctypes.c_float.__ctype_le__, + ctypes.c_double, ctypes.c_double.__ctype_be__, ctypes.c_double.__ctype_le__]: arg = float(arg) elif hasattr(arg_type, '_length_') and arg_type._type_ is not ctypes.c_char: # assume this is a primitive array try: @@ -730,18 +741,21 @@ def shutdown_cfs(self): self.cfs = None status = False - # check whether cFS instance exists and if so, try to kill it + # check whether cFS process exists and if so, try to kill it log.info("Killing Linux process...") - if not self.cfs_pid: - log.error("CFS pid not found! Process may not be killed.") - elif os.system("ps -p {}".format(self.cfs_pid)) != 0: - log.error("CFS process {} not found! It may have already terminated.".format(self.config.cfs_run_cmd)) + if self.cfs_pid: + if os.system("ps -p {}".format(self.cfs_pid)) != 0: + log.error("Process {} not found! It may have already terminated.".format(self.cfs_pid)) + else: + status = (os.system("kill -9 {}".format(self.cfs_pid)) == 0) + if not status: + log.error("Failed to kill process {}. CFS and/or xterm may have already exited or still be running!" + .format(self.cfs_pid)) else: - status = (os.system("kill -9 {}".format(self.cfs_pid)) == 0) - if not status: - log.error("Failed to kill process {}. CFS and/or xterm may have already exited or still be running!" - .format(self.cfs_pid)) + log.warning("CFS pid is not known. Will attempt to kill process by name '{}'" + .format(self.config.cfs_run_cmd)) + # try to kill outer command (xterm, sh) run by CTF if os.system('pkill -fe -9 "{}"'.format(self.config.cfs_run_cmd)) != 0: status = False log.error("Failed to kill any {}. CFS may have already exited or still be running!" @@ -756,10 +770,13 @@ def shutdown(self): """ log.info("Shutting down controller for {}".format(self.config.name)) if self.cfs: - try: - self.shutdown_cfs() - except CtfTestError: - log.error("Error: Shutting down controller for {}".format(self.config.name)) + if self.cfs.started_by_ctf: + try: + self.shutdown_cfs() + except CtfTestError: + log.error("Error: Shutting down controller for {}".format(self.config.name)) + else: + log.warning("CFS target {} was not started by CTF and will NOT be stopped.".format(self.config.name)) self.cfs = None else: log.info("CFS was already shut down") @@ -862,6 +879,9 @@ def initialize(self): if not self.process_ccsds_files(): return False + return self._init_cfs_interface() + + def _init_cfs_interface(self): log.info("Starting Remote CFS Interface to {}:{} for target {}" .format(self.config.cfs_target_ip, self.config.cmd_udp_port, self.config.name)) self.execution = SshController(SshConfig()) @@ -881,7 +901,7 @@ def initialize(self): log.warning("Not starting CFS executable... Expecting \"StartCfs\" in test script...") if result: - log.info("RemoteCfsController Initialized for target {}".format(self.config.name)) + log.info("RemoteCfsInterface Initialized for target {}".format(self.config.name)) return result def archive_cfs_files(self, source_path): diff --git a/plugins/cfs/pycfs/cfs_interface.py b/plugins/cfs/pycfs/cfs_interface.py index b420ddd..7ada204 100644 --- a/plugins/cfs/pycfs/cfs_interface.py +++ b/plugins/cfs/pycfs/cfs_interface.py @@ -93,6 +93,7 @@ def __init__(self, config, telemetry, command, mid_map, ccsds): .format(self.config.evs_short_event_mid_name)) self.init_passed = False + self.started_by_ctf = False self.command = command self.telemetry = telemetry @@ -112,15 +113,14 @@ def __init__(self, config, telemetry, command, mid_map, ccsds): tlm_app_choice = getattr(output_app_interface, self.config.tlm_app_choice) log.debug("Imported CFS Output Interface: {}".format(tlm_app_choice)) - self.output_manager = tlm_app_choice(self.config.ctf_ip, - self.config.tlm_udp_port, + self.output_manager = tlm_app_choice(self.config, self.command, - self.config.ccsds_ver, self.mid_payload_map, - self.config.name) + ) self.cfs_std_out_path = None self.evs_log_file = None self.tlm_log_file = None + self.tlm_csv_file = None # This flag is used to indicate the tlm is starting to come # in from the CFS application being tested self.tlm_has_been_received = False @@ -186,13 +186,20 @@ def __create_tlm_log_file(self): tlm_log_file_path = os.path.join(Global.current_script_log_dir, self.config.name + "_tlm_msgs.log") self.tlm_log_file = open(tlm_log_file_path, "a+") self.tlm_log_file.write("Time: MID, Data\n") + log.debug("Create tlm log file {}".format(self.tlm_log_file)) # update build-in variable for ctf tlm folder ctf_utility.set_variable("_CTF_TLM_DIR", "=", os.path.abspath(Global.current_script_log_dir), "string") + + if self.tlm_csv_file is None and self.config.csv_tlm_log and self.config.telemetry_debug: + tlm_log_csv_path = os.path.join(Global.current_script_log_dir, self.config.name + "_tlm_msgs.csv") + self.tlm_csv_file = open(tlm_log_csv_path, "a+") + self.tlm_csv_file.write("MID, Payload Length, Message (in Hex)\n") + except IOError: log.error("Failed to create tlm log file {}") log.debug(traceback.format_exc()) - def write_tlm_log(self, payload, buf, header): + def write_tlm_log(self, payload, buf: bytearray, header): """ Write payload and mid to telemetry log file. if log file does not exist, create one. """ @@ -210,15 +217,20 @@ def write_tlm_log(self, payload, buf, header): datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3], hex(mid), sequence_count, timestamp_seconds, timestamp_subseconds, str(payload).replace("\n", "\n\t"))) + if self.config.telemetry_debug: self.tlm_log_file.write(" For MID {} Payload length: {} hex values: 0x{}\n".format(hex(mid), len(buf), buf.hex())) + if self.config.csv_tlm_log: + self.tlm_csv_file.write("{}, {}, {}\n".format(hex(mid), len(buf), buf.hex())) + except (IOError, ValueError): log.error("Failed to write telemetry packet received for {}".format(hex(mid))) log.debug(traceback.format_exc()) def write_tlm_error_log(self, mid: str, description: str, buf: bytearray): + """ Write telemetry error messages to log file. if log file does not exist, create one. """ @@ -229,9 +241,10 @@ def write_tlm_error_log(self, mid: str, description: str, buf: bytearray): self.tlm_log_file.write("{} - {}: mid:{} \n\t{}\n". format(Global.get_time_manager().exec_time, datetime.datetime.now().strftime("%H:%M:%S.%f")[:-3], mid, description)) - if self.config.telemetry_debug: self.tlm_log_file.write(" For MID {} buf hex values: 0x{}\n".format(mid, buf.hex())) + if self.config.csv_tlm_log: + self.tlm_csv_file.write("{}, {}, {}\n".format(mid, len(buf), buf.hex())) except (IOError, ValueError): log.error("Failed to write telemetry packet received for {}".format(mid)) @@ -334,7 +347,7 @@ def parse_command_packet(self, buffer): self.on_packet_received(mid, header, payload) return mid - def parse_telemetry_packet(self, buffer): + def parse_telemetry_packet(self, buffer: bytearray): """ Parse telemetry packets from received buffer. """ @@ -375,8 +388,8 @@ def log_unknown_packet_mid(self, mid): """ If this is the first time receiving a packet with the given mid, log the message. """ - msg = "Received Message with MID = {}. This MID is not in the CCSDS MID Map. Ignoring..." \ - .format(hex(mid)) + msg = "Target {} received message with MID = {}. This MID is not in the CCSDS MID Map. Ignoring..." \ + .format(self.config.name, hex(mid)) if mid not in self.has_received_mid: log.warning(msg) self.has_received_mid[mid] = True @@ -388,7 +401,7 @@ def log_invalid_packet(self, mid): If this is the first time receiving a packet with the given mid, log the packet. """ if not self.has_received_mid[mid]: - log.error("Cannot retrieve payload from packet with MID {}.".format(hex(mid))) + log.error("Target:{} cannot retrieve payload from packet with MID {}.".format(self.config.name, hex(mid))) self.has_received_mid[mid] = True log.debug(traceback.format_exc()) @@ -398,8 +411,8 @@ def on_packet_received(self, mid: int, header: any, payload: any) -> None: """ exec_time = Global.get_time_manager().exec_time if not self.has_received_mid[mid]: - log.info("Receiving first packet for Data Type: {} with MID: {} at time: {}" - .format(type(payload).__name__, hex(mid), exec_time)) + log.info("Target:{} receiving first packet for Data Type: {} with MID: {} at time: {}" + .format(self.config.name, type(payload).__name__, hex(mid), exec_time)) # Update the array so that the message is not printed again self.has_received_mid[mid] = True @@ -627,6 +640,7 @@ def check_tlm_value(self, mid, args=None, discard_old_packets=True): return check_tlm_result # Traverse packets backwards validating each packet for the selected MID + log.debug("Check tlmvalue for MID {} in {} messages".format(hex(mid), len(self.received_mid_packets_dic[mid]))) for i in range(len(self.received_mid_packets_dic[mid]) - 1, -1, -1): # Get current packet for the selected MID payload = self.received_mid_packets_dic[mid][i].payload @@ -739,7 +753,7 @@ def check_tlm_packet(self, payload, args, log_result=True): break if isinstance(actual, bytes): - log.info("Bytes object {} is decoded to {}".format(actual, actual.decode())) + log.debug("Bytes object {} is decoded to {}".format(actual, actual.decode())) actual = actual.decode() initial_result = self.check_value(actual, expected_value, arg["compare"], mask, mask_value) diff --git a/plugins/cfs/pycfs/local_cfs_interface.py b/plugins/cfs/pycfs/local_cfs_interface.py index 0a75057..3649387 100644 --- a/plugins/cfs/pycfs/local_cfs_interface.py +++ b/plugins/cfs/pycfs/local_cfs_interface.py @@ -172,7 +172,7 @@ def start_cfs(self, run_args): cfs_process = Popen(start_string, cwd=self.config.cfs_run_dir, shell=True, universal_newlines=True) return_values["pid"] = cfs_process.pid - + self.started_by_ctf = True except Exception as exception: log.error("Error attempting to execute command: {}".format(start_string)) log.debug(exception) diff --git a/plugins/cfs/pycfs/output_app_interface.py b/plugins/cfs/pycfs/output_app_interface.py index d05cc53..934390b 100644 --- a/plugins/cfs/pycfs/output_app_interface.py +++ b/plugins/cfs/pycfs/output_app_interface.py @@ -17,6 +17,7 @@ """ from lib.ctf_global import Global from lib.logger import logger as log +from plugins.cfs.cfs_config import CfsConfig TO_ENABLE_OUTPUT = "TO_ENABLE_OUTPUT_CC" TO_ENABLE_OUTPUT_CC = 2 @@ -29,18 +30,18 @@ class OutputManager: within this class, define the methods that all of the output applications must implement """ - def __init__(self, local_ip, local_port, command_interface, ccsds_ver, command_mids=None, name=None): + def __init__(self, config: CfsConfig, command_interface, command_mids=None): """ Constructor implementation for OutputManager class. It sets up the local_ip, local_port, command_interface, ccsds version, command_args, command_mids. """ - self.local_ip = local_ip - self.local_port = local_port + self.local_ip = config.ctf_ip + self.local_port = config.tlm_udp_port self.command_interface = command_interface - self.ccsds_ver = ccsds_ver + self.ccsds_ver = config.ccsds_ver self.command_args = None self.command_mids = command_mids - self.name = name + self.name = config.name def enable_output(self): """ @@ -69,17 +70,15 @@ class ToApi(OutputManager): to the CFS test framework. """ - def __init__(self, local_ip="", local_port=0, command_interface=None, - ccsds_ver=0, mid_map=None, name=None): + def __init__(self, config, command_interface, mid_map): """ Constructor of the ToApi class. - @param local_ip: The IP address we want packets to be forwarded to. Default: 127.0.0.1 - @param local_port: The port we want packets to be forwarded to. Default: 40096 + @param config: target cfs config @param command_interface: An instance of the CommandInterface class (used to send commands to UDP) - @param ccsds_ver: CCSDS header version (1 or 2) + @param mid_map: command_mid dictionary """ - OutputManager.__init__(self, local_ip, local_port, command_interface, ccsds_ver, mid_map, name) + OutputManager.__init__(self, config, command_interface, mid_map) for mid, value in mid_map.items(): if isinstance(value, dict) and TO_ENABLE_OUTPUT in value: self.command_args = value[TO_ENABLE_OUTPUT]["ARG_CLASS"] diff --git a/plugins/cfs/pycfs/remote_cfs_interface.py b/plugins/cfs/pycfs/remote_cfs_interface.py index 2b5d6fd..d1240bd 100644 --- a/plugins/cfs/pycfs/remote_cfs_interface.py +++ b/plugins/cfs/pycfs/remote_cfs_interface.py @@ -74,6 +74,7 @@ def start_cfs(self, run_args): result = self.execution_controller.run_command_persistent(start_string, cwd=self.config.cfs_run_dir) return_values['pid'] = self.execution_controller.get_last_pid() + self.started_by_ctf = True if result and return_values['pid'] is not None: Global.time_manager.wait(1) diff --git a/plugins/cfs/tests/pycfs/test_cfs_controllers.py b/plugins/cfs/tests/pycfs/test_cfs_controllers.py index 98fbf4e..9a5d3e3 100644 --- a/plugins/cfs/tests/pycfs/test_cfs_controllers.py +++ b/plugins/cfs/tests/pycfs/test_cfs_controllers.py @@ -50,6 +50,7 @@ def _cfs_controller_instance_inited(): config.cfs_run_cmd = 'tail -f /dev/null' controller = CfsController(config) controller.initialize() + controller.cfs.started_by_ctf = True controller.cfs_pid = 42 return controller @@ -184,6 +185,37 @@ def test_cfs_controller_start_cfs(cfs_controller_inited): patch('plugins.cfs.pycfs.local_cfs_interface.LocalCfsInterface.enable_output'): mock_start_cfs.return_value = {'result': True, 'pid': -1} cfs_controller_inited.start_cfs('') + assert cfs_controller_inited.cfs_pid == -1 + + +def test_cfs_controller_start_cfs_restart(cfs_controller_inited): + """ + Test CfsController class start_cfs method: create a new CFS interface + Implementation of CFS plugin instructions start_cfs. When CFS plugin instructions (start_cfs) is executed, + it calls CfsController instance's start_cfs function. + """ + with patch("plugins.cfs.pycfs.cfs_controllers.LocalCfsInterface") as mock_cfs: + mock_cfs.return_value.init_passed = True + mock_cfs.return_value.start_cfs.return_value = {'result': True, 'pid': -1} + cfs_controller_inited.shutdown_cfs() + assert cfs_controller_inited.cfs is None + assert cfs_controller_inited.start_cfs('') is True + assert cfs_controller_inited.cfs + + +def test_cfs_controller_start_cfs_restart_error(cfs_controller_inited): + """ + Test CfsController class start_cfs method: fail to create a new CFS interface + Implementation of CFS plugin instructions start_cfs. When CFS plugin instructions (start_cfs) is executed, + it calls CfsController instance's start_cfs function. + """ + with patch("plugins.cfs.pycfs.cfs_controllers.LocalCfsInterface") as mock_cfs: + mock_cfs.return_value.init_passed = False + cfs_controller_inited.shutdown_cfs() + assert cfs_controller_inited.cfs is None + assert cfs_controller_inited.start_cfs('') is False + assert cfs_controller_inited.cfs + cfs_controller_inited.cfs.start_cfs.assert_not_called() def test_cfs_controller_enable_cfs_output(cfs_controller): @@ -563,6 +595,10 @@ def test_cfs_controller_resolve_simple_type(cfs_controller_inited): assert cfs_controller_inited.resolve_simple_type(1.23, arg_type) == 1.23 assert cfs_controller_inited.resolve_simple_type('1.23', arg_type) == 1.23 assert cfs_controller_inited.resolve_simple_type(0, arg_type) == 0.0 + arg_type = ctypes.c_float.__ctype_be__ + assert cfs_controller_inited.resolve_simple_type(1.23, arg_type) == 1.23 + assert cfs_controller_inited.resolve_simple_type('1.23', arg_type) == 1.23 + assert cfs_controller_inited.resolve_simple_type(0, arg_type) == 0.0 # bool type arg_type = ctypes.c_bool @@ -904,11 +940,12 @@ def test_cfs_controller_shutdown_cfs_fail_pid(cfs_controller_inited, utils): """ # case: no cfs pid with patch('os.system') as mock_system: - mock_system.return_value = 0 cfs_controller_inited.cfs_pid = None assert not cfs_controller_inited.shutdown_cfs() assert not cfs_controller_inited.cfs + assert utils.has_log_level("WARNING") assert utils.has_log_level("ERROR") + mock_system.assert_called_once_with('pkill -fe -9 "tail -f /dev/null"') def test_cfs_controller_shutdown_cfs_fail_ps(cfs_controller_inited, utils): @@ -918,6 +955,8 @@ def test_cfs_controller_shutdown_cfs_fail_ps(cfs_controller_inited, utils): assert not cfs_controller_inited.shutdown_cfs() assert not cfs_controller_inited.cfs assert utils.has_log_level("ERROR") + mock_system.assert_any_call("ps -p 42") + mock_system.assert_called_with('pkill -fe -9 "tail -f /dev/null"') def test_cfs_controller_shutdown_cfs_fail_kill(cfs_controller_inited, utils): @@ -927,16 +966,43 @@ def test_cfs_controller_shutdown_cfs_fail_kill(cfs_controller_inited, utils): assert not cfs_controller_inited.shutdown_cfs() assert not cfs_controller_inited.cfs assert utils.has_log_level("ERROR") + mock_system.assert_any_call('ps -p 42') + mock_system.assert_any_call('kill -9 42') + mock_system.assert_called_with('pkill -fe -9 "tail -f /dev/null"') -def test_cfs_controller_shutdown(cfs_controller_inited): +def test_cfs_controller_shutdown(cfs_controller_inited, utils): """ Test CfsController class shutdown method: This function will shut down the CFS application being tested even if the JSON test file does not include the shutdown test command """ - assert cfs_controller_inited.shutdown() is None - assert cfs_controller_inited.cfs is None + with patch('plugins.cfs.pycfs.cfs_controllers.CfsController.shutdown_cfs') as mock_shutdown_cfs: + # case: nominal + assert cfs_controller_inited.shutdown() is None + assert cfs_controller_inited.cfs is None + assert not utils.has_log_level("ERROR") + mock_shutdown_cfs.assert_called_once() + mock_shutdown_cfs.reset_mock() + # case: cfs already shut down + assert cfs_controller_inited.shutdown() is None + assert cfs_controller_inited.cfs is None + assert not utils.has_log_level("ERROR") + mock_shutdown_cfs.assert_not_called() + + +def test_cfs_controller_shutdown_not_started(cfs_controller_inited, utils): + """ + Test CfsController class shutdown method: + This function will not shut down the CFS application being tested if it was not started by CTF + """ + with patch('plugins.cfs.pycfs.cfs_controllers.CfsController.shutdown_cfs') as mock_shutdown_cfs: + # case: nominal + cfs_controller_inited.cfs.started_by_ctf = False + assert cfs_controller_inited.shutdown() is None + assert cfs_controller_inited.cfs is None + assert utils.has_log_level("WARNING") + mock_shutdown_cfs.assert_not_called() def test_cfs_controller_shutdown_exception(cfs_controller_inited, utils): diff --git a/plugins/cfs/tests/pycfs/test_cfs_interface.py b/plugins/cfs/tests/pycfs/test_cfs_interface.py index 1b0a204..1a90183 100644 --- a/plugins/cfs/tests/pycfs/test_cfs_interface.py +++ b/plugins/cfs/tests/pycfs/test_cfs_interface.py @@ -124,13 +124,13 @@ def test_cfs_interface_write_tlm_log(cfs, utils): header = cfs.ccsds.CcsdsTelemetry() with patch('builtins.open', new_callable=mock_open()) as mock_file: cfs.config.telemetry_debug = True + cfs.config.csv_tlm_log = True cfs.write_tlm_log('payload1', bytearray('payload1', 'utf-8'), header) assert cfs.tlm_log_file is mock_file.return_value - mock_file.assert_called_once_with('./cfs_tlm_msgs.log', 'a+') - assert mock_file.return_value.write.call_count == 3 + assert mock_file.return_value.write.call_count == 5 mock_file.return_value.write.reset_mock() cfs.write_tlm_log('payload2', bytearray('payload2', 'utf-8'), header) - assert mock_file.return_value.write.call_count == 2 + assert mock_file.return_value.write.call_count == 3 mock_file.return_value.write.reset_mock() mock_file.return_value.write.side_effect = IOError('mock error') cfs.write_tlm_log('payload3', bytearray('payload3', 'utf-8'), header) @@ -142,6 +142,7 @@ def test_cfs_interface_write_tlm_error_log_io_error(cfs, utils): assert not utils.has_log_level('ERROR') with patch('builtins.open', new_callable=mock_open()) as mock_file: cfs.config.telemetry_debug = True + cfs.config.csv_tlm_log = True mock_file.return_value.write.side_effect = IOError('mock error') cfs.write_tlm_error_log(hex(100), 'Undefined mid' , bytearray('payload1', 'utf-8')) assert utils.has_log_level('ERROR') @@ -152,6 +153,7 @@ def test_cfs_interface_write_tlm_error_log(cfs, utils): assert not utils.has_log_level('ERROR') with patch('builtins.open', new_callable=mock_open()) as mock_file: cfs.config.telemetry_debug = True + cfs.config.csv_tlm_log = True cfs.write_tlm_error_log(hex(100), 'Undefined mid' , bytearray('payload1', 'utf-8')) assert cfs.tlm_log_file is mock_file.return_value mock_file.return_value.write.reset_mock() diff --git a/plugins/cfs/tests/pycfs/test_output_app_interface.py b/plugins/cfs/tests/pycfs/test_output_app_interface.py index 013fb82..e20d90c 100644 --- a/plugins/cfs/tests/pycfs/test_output_app_interface.py +++ b/plugins/cfs/tests/pycfs/test_output_app_interface.py @@ -18,6 +18,7 @@ from lib.ctf_global import Global from lib.logger import set_logger_options_from_config +from plugins.cfs.cfs_config import CfsConfig from plugins.cfs.pycfs.output_app_interface import OutputManager, ToApi @@ -28,11 +29,13 @@ def init_global(): @pytest.fixture(name='outmgr') def output_manager(): - return OutputManager('local_ip', 'local_port', 'command_interface', 'ccsds_ver') + cfs_config = CfsConfig("cfs") + return OutputManager(cfs_config, 'command_interface', None) @pytest.fixture(name='toapi') def to_api(): + cfs_config = CfsConfig("cfs") mid_map = { 0xA00B: { "TO_ENABLE_OUTPUT_CC": { @@ -41,14 +44,14 @@ def to_api(): } } } - return ToApi('local_ip', 'local_port', 'command_interface', 'ccsds_ver', mid_map, "name") + return ToApi(cfs_config, 'command_interface', mid_map) def test_output_manager_init(outmgr): - assert outmgr.local_ip == 'local_ip' - assert outmgr.local_port == 'local_port' + assert outmgr.local_ip == '127.0.0.1' + assert outmgr.local_port == 5011 assert outmgr.command_interface == 'command_interface' - assert outmgr.ccsds_ver == 'ccsds_ver' + assert outmgr.ccsds_ver == 2 assert outmgr.command_args is None assert outmgr.command_mids is None @@ -69,14 +72,14 @@ def test_out_manager_on_time_interval(outmgr): def test_to_api_init(toapi): - assert toapi.local_ip == 'local_ip' - assert toapi.local_port == 'local_port' + assert toapi.local_ip == '127.0.0.1' + assert toapi.local_port == 5011 assert toapi.command_interface == 'command_interface' - assert toapi.ccsds_ver == 'ccsds_ver' + assert toapi.ccsds_ver == 2 assert toapi.cmd_cc == 2 assert toapi.mid == 0xA00B assert toapi.command_args - assert toapi.name == 'name' + assert toapi.name == 'cfs' def test_to_api_init_None(utils): @@ -88,12 +91,13 @@ def test_to_api_init_None(utils): } } } - toapi = ToApi('local_ip', 'local_port', 'command_interface', 'ccsds_ver', mid_map, "name") + cfs_config = CfsConfig("cfs") + toapi = ToApi(cfs_config, 'command_interface', mid_map) - assert toapi.local_ip == 'local_ip' - assert toapi.local_port == 'local_port' + assert toapi.local_ip == '127.0.0.1' + assert toapi.local_port == 5011 assert toapi.command_interface == 'command_interface' - assert toapi.ccsds_ver == 'ccsds_ver' + assert toapi.ccsds_ver == 2 utils.has_log_level('WARNING') @@ -106,9 +110,9 @@ def test_to_api_enable_output(toapi): assert toapi.enable_output() pm.find_plugin_for_command_and_execute.assert_called_with({ "data": { - "target": "name", + "target": "cfs", "cc": "TO_ENABLE_OUTPUT_CC", - "args": {"cDestIp": "local_ip", "usDestPort": "local_port"}, + "args": {"cDestIp": "127.0.0.1", "usDestPort": 5011}, "mid": 0xA00B }, "instruction": "SendCfsCommand" diff --git a/requirements.txt b/requirements.txt index 19d3f70..780ae82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,25 +1,34 @@ +astroid==2.5 attrs==19.3.0 bcrypt==3.1.7 cffi==1.13.2 +configobj==5.0.8 +coverage==5.3.1 cryptography==2.8 +demjson==2.2.4 fabric==2.5.0 -ftputil~=4.0.0 +ftputil==4.0.0 importlib-metadata==1.5.0 -invoke~=1.4.1 +iniconfig==2.0.0 +invoke==1.4.1 +isort==5.13.2 jsonschema==3.2.0 +lazy-object-proxy==1.10.0 +mccabe==0.6.1 +mock==4.0.3 +packaging==23.2 paramiko==2.6.0 +pluggy==1.3.0 psutil==5.7.0 +py==1.11.0 pycparser==2.19 pyelftools==0.26 +pylint==2.6.0 PyNaCl==1.3.0 pyrsistent==0.15.7 +pytest==6.2.5 +pytest-cov==2.10.1 six==1.13.0 +toml==0.10.2 +wrapt==1.12.1 zipp==3.1.0 -mock~=4.0.2 -setuptools~=47.3.0 -coverage==5.3.1 -configobj==5.0.8 -pytest~=6.2.1 -pytest-cov==2.10.1 -pylint==2.6.0 -demjson==2.2.4 \ No newline at end of file diff --git a/run_tests.sh b/run_tests.sh index d6aec54..751bda0 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -49,6 +49,7 @@ elif [ "$1" == "utc" ]; then pytest -v ./unit_tests/ \ --workspace=open_source \ -W ignore::pytest.PytestCollectionWarning \ + -W ignore::DeprecationWarning:invoke.loader \ --cov-config=.ctf_coveragerc \ --cov=plugins --cov=lib \ --cov-report=html | tee $OUT_SUBDIR/ctf_ut_results.log diff --git a/unit_tests/lib/test_ctf_utility.py b/unit_tests/lib/test_ctf_utility.py index 6b3f4d4..8ec788c 100644 --- a/unit_tests/lib/test_ctf_utility.py +++ b/unit_tests/lib/test_ctf_utility.py @@ -19,6 +19,7 @@ from lib import ctf_utility from lib.ctf_global import Global +from lib.ctf_utility import resolve_dic_variable from lib.exceptions import CtfParameterError @@ -71,6 +72,17 @@ def test_ctf_utility_resolve_variable_exception(utils): assert utils.has_log_level('ERROR') +def test_ctf_utility_resolve_dic_variable(): + assert ctf_utility.set_variable('var_1', '=', 100) + dict_obj = {1: '$var_1$', '$var_1$': 78} + assert resolve_dic_variable(dict_obj) == {1: 100, 100: 78} + dict_obj = [1, '$var_1$', 3] + assert resolve_dic_variable(dict_obj) == [1, '$var_1$', 3] + dict_obj = {1: '$var_1$', 2: [100, {1: 'a$var_1$', '$var_1$': 78}, {4: 4}]} + assert resolve_dic_variable(dict_obj) == {1: 100, 2: [100, {1: 'a100', 100: 78}, {4: 4}]} + Global.variable_store.clear() + + def test_ctf_utility_set_variable_pass(): assert ctf_utility.set_variable('var_1', '=', 100) assert ctf_utility.set_variable('var_2', '=', 100) @@ -103,6 +115,14 @@ def test_ctf_utility_set_variable_specify_type(): assert ctf_utility.set_variable('var_1', '+', "10", "float") assert ctf_utility.get_variable('var_1') == 11.0 assert isinstance(ctf_utility.get_variable('var_1'), float) + assert ctf_utility.set_variable('var_1', '=', "0xF3", "int") + assert ctf_utility.get_variable('var_1') == 243 + assert ctf_utility.set_variable('var_1', '=', "0Xa1", "int") + assert ctf_utility.get_variable('var_1') == 161 + assert ctf_utility.set_variable('var_1', '=', "12", "int") + assert ctf_utility.get_variable('var_1') == 12 + assert ctf_utility.set_variable('var_1', '+', "0xA", "int") + assert isinstance(ctf_utility.get_variable('var_1'), int) Global.variable_store.clear() @@ -140,6 +160,13 @@ def test_ctf_utility_set_variable_exception(utils): utils.clear_log() assert not ctf_utility.set_variable('var_1', '=', 's12', 'int') assert utils.has_log_level('ERROR') + utils.clear_log() + + assert not ctf_utility.set_variable('var_1', '=', 's12', 'float') + assert utils.has_log_level('ERROR') + + assert ctf_utility.set_variable('var_1', '=', '1.0', 'float') + assert not ctf_utility.set_variable('var_1', '+', 's1.0', 'float') Global.variable_store.clear() diff --git a/unit_tests/lib/test_logger.py b/unit_tests/lib/test_logger.py index 717894d..c2307b8 100644 --- a/unit_tests/lib/test_logger.py +++ b/unit_tests/lib/test_logger.py @@ -54,3 +54,12 @@ def test_logger_change_log_file(): default_handler = logger.logger.handlers[0] assert logger.logger.handlers[0].baseFilename == os.path.abspath('./temp_log') logger.logger.handlers[0] = default_handler + + +def test_colorlog_import(): + from importlib import reload + import lib + os.system('pip install colorlog') + reload(lib.logger) + logger.init_logger(Global.config) + os.system('pip uninstall colorlog -y') diff --git a/vv_tests/configs/ctf_vv_config.ini b/vv_tests/configs/ctf_vv_config.ini index b2be4f8..07d18d0 100644 --- a/vv_tests/configs/ctf_vv_config.ini +++ b/vv_tests/configs/ctf_vv_config.ini @@ -68,6 +68,19 @@ json_results = True # DEBUG: show all logs! log_level = DEBUG +# Optional setting, use Python pprint module to “pretty-print” telemetry log data structures +# https://docs.python.org/3/library/pprint.html +# if not set, use original compact format +tlm_formatter = pprint + +# The number of nesting levels which may be printed for pprint +# if the data structure being printed is too deep, the next contained level is replaced by .... +pprint_depth = 7 + +# Optional setting, will create an additional csv tlm log file if True, +# only applicable when telemetry_debug config is enabled +# csv_tlm_log = False + ################################# # ccsds options #################################