diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8895afe26..3d64f2199 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -53,6 +53,9 @@ parameters: - name: ycabled root_dir: sonic-ycabled python3: true + - name: sensormond + root_dir: sonic-sensormond + python3: true - name: artifactBranch type: string default: 'refs/heads/master' diff --git a/sonic-sensormond/pytest.ini b/sonic-sensormond/pytest.ini new file mode 100644 index 000000000..d90ee9ed9 --- /dev/null +++ b/sonic-sensormond/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv diff --git a/sonic-sensormond/scripts/sensormond b/sonic-sensormond/scripts/sensormond new file mode 100755 index 000000000..da8bc1bda --- /dev/null +++ b/sonic-sensormond/scripts/sensormond @@ -0,0 +1,529 @@ +#!/usr/bin/python3 + +''' + sensormond + Sensor monitor daemon for SONiC +''' + +import signal +import sys +import threading +import time + +import sonic_platform +from sonic_py_common import daemon_base, logger +from swsscommon import swsscommon + + +SYSLOG_IDENTIFIER = 'sensormond' +NOT_AVAILABLE = 'N/A' +CHASSIS_INFO_KEY = 'chassis 1' +INVALID_SLOT = -1 + +PHYSICAL_ENTITY_INFO_TABLE = 'PHYSICAL_ENTITY_INFO' + +# Exit with non-zero exit code by default so supervisord will restart Sensormon. +SENSORMON_ERROR_EXIT = 1 +exit_code = SENSORMON_ERROR_EXIT + +# Utility functions + +def try_get(callback, default=NOT_AVAILABLE): + ''' + Handy function to invoke the callback and catch NotImplementedError + :param callback: Callback to be invoked + :param default: Default return value if exception occur + :return: Default return value if exception occur else return value of the callback + ''' + try: + ret = callback() + if ret is None: + ret = default + except NotImplementedError: + ret = default + + return ret + +def update_entity_info(table, parent_name, key, device, device_index): + fvs = swsscommon.FieldValuePairs( + [('position_in_parent', str(try_get(device.get_position_in_parent, device_index))), + ('parent_name', parent_name)]) + table.set(key, fvs) + +class SensorStatus(logger.Logger): + + def __init__(self): + super(SensorStatus, self).__init__(SYSLOG_IDENTIFIER) + + self.value = None + self.over_threshold = False + self.under_threshold = False + + def set_value(self, name, value): + ''' + Record sensor changes. + :param name: Name of the sensor. + :param value: New value. + ''' + if value == NOT_AVAILABLE: + if self.value is not None: + self.log_warning('Value of {} became unavailable'.format(name)) + self.value = None + return + + self.value = value + + def set_over_threshold(self, value, threshold): + ''' + Set over threshold status + :param value: value + :param threshold: High threshold + :return: True if over threshold status changed else False + ''' + if value == NOT_AVAILABLE or threshold == NOT_AVAILABLE: + old_status = self.over_threshold + self.over_threshold = False + return old_status != self.over_threshold + + status = value > threshold + if status == self.over_threshold: + return False + + self.over_threshold = status + return True + + def set_under_threshold(self, value, threshold): + ''' + Set under value status + :param value: value + :param threshold: Low threshold + :return: True if under threshold status changed else False + ''' + if value == NOT_AVAILABLE or threshold == NOT_AVAILABLE: + old_status = self.under_threshold + self.under_threshold = False + return old_status != self.under_threshold + + status = value < threshold + if status == self.under_threshold: + return False + + self.under_threshold = status + return True + + +# +# SensorUpdater ====================================================================== +# +class SensorUpdater(logger.Logger): + + def __init__(self, table_name, chassis): + ''' + Initializer of SensorUpdater + :param table_name: Name of sensor table + :param chassis: Object representing a platform chassis + ''' + super(SensorUpdater, self).__init__(SYSLOG_IDENTIFIER) + + self.chassis = chassis + state_db = daemon_base.db_connect("STATE_DB") + self.table = swsscommon.Table(state_db, table_name) + self.phy_entity_table = swsscommon.Table(state_db, PHYSICAL_ENTITY_INFO_TABLE) + self.chassis_table = None + + self.is_chassis_system = chassis.is_modular_chassis() + if self.is_chassis_system: + my_slot = try_get(chassis.get_my_slot, INVALID_SLOT) + if my_slot != INVALID_SLOT: + try: + # Modular chassis may not have table CHASSIS_STATE_DB. + slot_table_name = table_name + '_' + str(my_slot) + chassis_state_db = daemon_base.db_connect("CHASSIS_STATE_DB") + self.chassis_table = swsscommon.Table(chassis_state_db, slot_table_name) + except Exception as e: + self.chassis_table = None + + def __del__(self): + if self.table: + table_keys = self.table.getKeys() + for tk in table_keys: + self.table._del(tk) + if self.is_chassis_system and self.chassis_table is not None: + self.chassis_table._del(tk) + if self.phy_entity_table: + phy_entity_keys = self.phy_entity_table.getKeys() + for pek in phy_entity_keys: + self.phy_entity_table._del(pek) + + def _log_on_status_changed(self, normal_status, normal_log, abnormal_log): + ''' + Log when any status changed + :param normal_status: Expected status. + :param normal_log: Log string for expected status. + :param abnormal_log: Log string for unexpected status + :return: + ''' + if normal_status: + self.log_notice(normal_log) + else: + self.log_warning(abnormal_log) + +# +# VoltageUpdater ====================================================================== +# +class VoltageUpdater(SensorUpdater): + # Voltage information table name in database + VOLTAGE_INFO_TABLE_NAME = 'VOLTAGE_INFO' + + def __init__(self, chassis): + ''' + Initializer of VoltageUpdater + :param chassis: Object representing a platform chassis + ''' + super(VoltageUpdater, self).__init__(self.VOLTAGE_INFO_TABLE_NAME, chassis) + + self.voltage_status_dict = {} + + if self.is_chassis_system: + self.module_voltage_sensors = set() + + def update(self): + ''' + Update all voltage information to database + :return: + ''' + self.log_debug("Start voltage update") + for index, voltage_sensor in enumerate(self.chassis.get_all_voltage_sensors()): + + self._refresh_voltage_status(CHASSIS_INFO_KEY, voltage_sensor, index) + + if self.is_chassis_system: + available_voltage_sensors = set() + for module_index, module in enumerate(self.chassis.get_all_modules()): + module_name = try_get(module.get_name, 'Module {}'.format(module_index + 1)) + + for voltage_sensor_index, voltage_sensor in enumerate(module.get_all_voltage_sensors()): + available_voltage_sensors.add((voltage_sensor, module_name, voltage_sensor_index)) + self._refresh_voltage_status(module_name, voltage_sensor, voltage_sensor_index) + + voltage_sensors_to_remove = self.module_voltage_sensors - available_voltage_sensors + self.module_voltage_sensors = available_voltage_sensors + for voltage_sensor, parent_name, voltage_sensor_index in voltage_sensors_to_remove: + self._remove_voltage_sensor_from_db(voltage_sensor, parent_name, voltage_sensor_index) + + self.log_debug("End Voltage updating") + + def _refresh_voltage_status(self, parent_name, voltage_sensor, voltage_sensor_index): + ''' + Get voltage status by platform API and write to database + :param parent_name: Name of parent device of the voltage_sensor object + :param voltage_sensor: Object representing a platform voltage voltage_sensor + :param voltage_sensor_index: Index of the voltage_sensor object in platform chassis + :return: + ''' + try: + name = try_get(voltage_sensor.get_name, '{} voltage_sensor {}'.format(parent_name, voltage_sensor_index + 1)) + + update_entity_info(self.phy_entity_table, parent_name, name, voltage_sensor, voltage_sensor_index + 1) + + if name not in self.voltage_status_dict: + self.voltage_status_dict[name] = SensorStatus() + + voltage_status = self.voltage_status_dict[name] + + high_threshold = NOT_AVAILABLE + low_threshold = NOT_AVAILABLE + high_critical_threshold = NOT_AVAILABLE + low_critical_threshold = NOT_AVAILABLE + maximum_voltage = NOT_AVAILABLE + minimum_voltage = NOT_AVAILABLE + unit = NOT_AVAILABLE + voltage = try_get(voltage_sensor.get_value) + is_replaceable = try_get(voltage_sensor.is_replaceable, False) + if voltage != NOT_AVAILABLE: + voltage_status.set_value(name, voltage) + unit = try_get(voltage_sensor.get_unit) + minimum_voltage = try_get(voltage_sensor.get_minimum_recorded) + maximum_voltage = try_get(voltage_sensor.get_maximum_recorded) + high_threshold = try_get(voltage_sensor.get_high_threshold) + low_threshold = try_get(voltage_sensor.get_low_threshold) + high_critical_threshold = try_get(voltage_sensor.get_high_critical_threshold) + low_critical_threshold = try_get(voltage_sensor.get_low_critical_threshold) + + warning = False + if voltage != NOT_AVAILABLE and voltage_status.set_over_threshold(voltage, high_threshold): + self._log_on_status_changed(not voltage_status.over_threshold, + 'High voltage warning cleared: {} voltage restored to {}{}, high threshold {}{}'. + format(name, voltage, unit, high_threshold, unit), + 'High voltage warning: {} current voltage {}{}, high threshold {}{}'. + format(name, voltage, unit, high_threshold, unit) + ) + warning = warning | voltage_status.over_threshold + + if voltage != NOT_AVAILABLE and voltage_status.set_under_threshold(voltage, low_threshold): + self._log_on_status_changed(not voltage_status.under_threshold, + 'Low voltage warning cleared: {} voltage restored to {}{}, low threshold {}{}'. + format(name, voltage, unit, low_threshold, unit), + 'Low voltage warning: {} current voltage {}{}, low threshold {}{}'. + format(name, voltage, unit, low_threshold, unit) + ) + warning = warning | voltage_status.under_threshold + + fvs = swsscommon.FieldValuePairs( + [('voltage', str(voltage)), + ('unit', unit), + ('minimum_voltage', str(minimum_voltage)), + ('maximum_voltage', str(maximum_voltage)), + ('high_threshold', str(high_threshold)), + ('low_threshold', str(low_threshold)), + ('warning_status', str(warning)), + ('critical_high_threshold', str(high_critical_threshold)), + ('critical_low_threshold', str(low_critical_threshold)), + ('is_replaceable', str(is_replaceable)), + ('timestamp', time.strftime('%Y%m%d %H:%M:%S')) + ]) + + self.table.set(name, fvs) + if self.is_chassis_system and self.chassis_table is not None: + self.chassis_table.set(name, fvs) + except Exception as e: + self.log_warning('Failed to update voltage_sensor status for {} - {}'.format(name, repr(e))) + + def _remove_voltage_sensor_from_db(self, voltage_sensor, parent_name, voltage_sensor_index): + name = try_get(voltage_sensor.get_name, '{} voltage_sensor {}'.format(parent_name, voltage_sensor_index + 1)) + self.table._del(name) + + if self.chassis_table is not None: + self.chassis_table._del(name) + +# +# CurrentUpdater ====================================================================== +# +class CurrentUpdater(SensorUpdater): + # Current information table name in database + CURRENT_INFO_TABLE_NAME = 'CURRENT_INFO' + + def __init__(self, chassis): + ''' + Initializer of CurrentUpdater + :param chassis: Object representing a platform chassis + ''' + super(CurrentUpdater, self).__init__(self.CURRENT_INFO_TABLE_NAME, chassis) + + self.current_status_dict = {} + if self.is_chassis_system: + self.module_current_sensors = set() + + def update(self): + ''' + Update all current information to database + :return: + ''' + self.log_debug("Start current updating") + for index, current_sensor in enumerate(self.chassis.get_all_current_sensors()): + + self._refresh_current_status(CHASSIS_INFO_KEY, current_sensor, index) + + if self.is_chassis_system: + available_current_sensors = set() + for module_index, module in enumerate(self.chassis.get_all_modules()): + module_name = try_get(module.get_name, 'Module {}'.format(module_index + 1)) + + for current_sensor_index, current_sensor in enumerate(module.get_all_current_sensors()): + available_current_sensors.add((current_sensor, module_name, current_sensor_index)) + self._refresh_current_status(module_name, current_sensor, current_sensor_index) + + current_sensors_to_remove = self.module_current_sensors - available_current_sensors + self.module_current_sensors = available_current_sensors + for current_sensor, parent_name, current_sensor_index in current_sensors_to_remove: + self._remove_current_sensor_from_db(current_sensor, parent_name, current_sensor_index) + + self.log_debug("End Current updating") + + def _refresh_current_status(self, parent_name, current_sensor, current_sensor_index): + ''' + Get current status by platform API and write to database + :param parent_name: Name of parent device of the current_sensor object + :param current_sensor: Object representing a platform current current_sensor + :param current_sensor_index: Index of the current_sensor object in platform chassis + :return: + ''' + try: + name = try_get(current_sensor.get_name, '{} current_sensor {}'.format(parent_name, current_sensor_index + 1)) + + update_entity_info(self.phy_entity_table, parent_name, name, current_sensor, current_sensor_index + 1) + + if name not in self.current_status_dict: + self.current_status_dict[name] = SensorStatus() + + current_status = self.current_status_dict[name] + + unit = NOT_AVAILABLE + high_threshold = NOT_AVAILABLE + low_threshold = NOT_AVAILABLE + high_critical_threshold = NOT_AVAILABLE + low_critical_threshold = NOT_AVAILABLE + maximum_current = NOT_AVAILABLE + minimum_current = NOT_AVAILABLE + current = try_get(current_sensor.get_value) + is_replaceable = try_get(current_sensor.is_replaceable, False) + if current != NOT_AVAILABLE: + current_status.set_value(name, current) + unit = try_get(current_sensor.get_unit) + minimum_current = try_get(current_sensor.get_minimum_recorded) + maximum_current = try_get(current_sensor.get_maximum_recorded) + high_threshold = try_get(current_sensor.get_high_threshold) + low_threshold = try_get(current_sensor.get_low_threshold) + high_critical_threshold = try_get(current_sensor.get_high_critical_threshold) + low_critical_threshold = try_get(current_sensor.get_low_critical_threshold) + + warning = False + if current != NOT_AVAILABLE and current_status.set_over_threshold(current, high_threshold): + self._log_on_status_changed(not current_status.over_threshold, + 'High Current warning cleared: {} current restored to {}{}, high threshold {}{}'. + format(name, current, unit, high_threshold, unit), + 'High Current warning: {} current Current {}{}, high threshold {}{}'. + format(name, current, unit, high_threshold, unit) + ) + warning = warning | current_status.over_threshold + + if current != NOT_AVAILABLE and current_status.set_under_threshold(current, low_threshold): + self._log_on_status_changed(not current_status.under_threshold, + 'Low current warning cleared: {} current restored to {}{}, low threshold {}{}'. + format(name, current, unit, low_threshold, unit), + 'Low current warning: {} current current {}{}, low threshold {}{}'. + format(name, current, unit, low_threshold, unit) + ) + warning = warning | current_status.under_threshold + + fvs = swsscommon.FieldValuePairs( + [('current', str(current)), + ('unit', unit), + ('minimum_current', str(minimum_current)), + ('maximum_current', str(maximum_current)), + ('high_threshold', str(high_threshold)), + ('low_threshold', str(low_threshold)), + ('warning_status', str(warning)), + ('critical_high_threshold', str(high_critical_threshold)), + ('critical_low_threshold', str(low_critical_threshold)), + ('is_replaceable', str(is_replaceable)), + ('timestamp', time.strftime('%Y%m%d %H:%M:%S')) + ]) + + self.table.set(name, fvs) + if self.is_chassis_system and self.chassis_table is not None: + self.chassis_table.set(name, fvs) + except Exception as e: + self.log_warning('Failed to update current_sensor status for {} - {}'.format(name, repr(e))) + + def _remove_current_sensor_from_db(self, current_sensor, parent_name, current_sensor_index): + name = try_get(current_sensor.get_name, '{} current_sensor {}'.format(parent_name, current_sensor_index + 1)) + self.table._del(name) + + if self.chassis_table is not None: + self.chassis_table._del(name) + +# +# Daemon ======================================================================= +# +class SensorMonitorDaemon(daemon_base.DaemonBase): + + # Initial update interval + INITIAL_INTERVAL = 5 + # Periodic Update interval + UPDATE_INTERVAL = 60 + # Update time threshold. If update time exceeds this threshold, log warning msg. + UPDATE_ELAPSED_THRESHOLD = 30 + + def __init__(self): + ''' + Initializer of SensorMonitorDaemon + ''' + super(SensorMonitorDaemon, self).__init__(SYSLOG_IDENTIFIER) + + # Set minimum logging level to INFO + self.set_min_log_priority_info() + + self.stop_event = threading.Event() + + self.wait_time = self.INITIAL_INTERVAL + + self.interval = self.UPDATE_INTERVAL + + try: + self.chassis = sonic_platform.platform.Platform().get_chassis() + except Exception as e: + self.log_error("Failed to get chassis info, err: {}".format(repr(e))) + + self.voltage_updater = VoltageUpdater(self.chassis) + + self.current_updater = CurrentUpdater(self.chassis) + + + # Override signal handler from DaemonBase + def signal_handler(self, sig, frame): + ''' + Signal handler + :param sig: Signal number + :param frame: not used + :return: + ''' + FATAL_SIGNALS = [signal.SIGINT, signal.SIGTERM] + NONFATAL_SIGNALS = [signal.SIGHUP] + + global exit_code + + if sig in FATAL_SIGNALS: + self.log_info("Caught signal '{}' - exiting...".format(signal.Signals(sig).name)) + exit_code = 128 + sig # Make sure we exit with a non-zero code so that supervisor will try to restart us + self.stop_event.set() + elif sig in NONFATAL_SIGNALS: + self.log_info("Caught signal '{}' - ignoring...".format(signal.Signals(sig).name)) + else: + self.log_warning("Caught unhandled signal '{}' - ignoring...".format(signal.Signals(sig).name)) + + # Main daemon logic + def run(self): + ''' + Run main logical of this daemon + :return: + ''' + if self.stop_event.wait(self.wait_time): + # We received a fatal signal + return False + + begin = time.time() + + self.voltage_updater.update() + self.current_updater.update() + + elapsed = time.time() - begin + if elapsed < self.interval: + self.wait_time = self.interval - elapsed + else: + self.wait_time = self.INITIAL_INTERVAL + + if elapsed > self.UPDATE_ELAPSED_THRESHOLD: + self.logger.log_warning('Sensors update took a long time : ' + '{} seconds'.format(elapsed)) + + return True + +# +# Main ========================================================================= +# +def main(): + sensor_control = SensorMonitorDaemon() + + sensor_control.log_info("Starting up...") + + while sensor_control.run(): + pass + + sensor_control.log_info("Shutting down with exit code {}".format(exit_code)) + + return exit_code + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sonic-sensormond/setup.cfg b/sonic-sensormond/setup.cfg new file mode 100644 index 000000000..b7e478982 --- /dev/null +++ b/sonic-sensormond/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/sonic-sensormond/setup.py b/sonic-sensormond/setup.py new file mode 100644 index 000000000..1aa5e3c08 --- /dev/null +++ b/sonic-sensormond/setup.py @@ -0,0 +1,43 @@ +from setuptools import setup + +setup( + name='sonic-sensormond', + version='1.0', + description='Sensor Monitor daemon for SONiC', + license='Apache 2.0', + author='SONiC Team', + author_email='linuxnetdev@microsoft.com', + url='https://github.com/Azure/sonic-platform-daemons', + maintainer='Mridul Bajpai', + maintainer_email='mridul@cisco.com', + packages=[ + 'tests' + ], + scripts=[ + 'scripts/sensormond', + ], + setup_requires=[ + 'pytest-runner', + 'wheel' + ], + tests_require=[ + 'mock>=2.0.0; python_version < "3.3"', + 'pytest', + 'pytest-cov', + 'sonic-platform-common' + ], + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Information Technology', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: Apache Software License', + 'Natural Language :: English', + 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.7', + 'Topic :: System :: Hardware', + ], + keywords='sonic SONiC SENSORMONITOR sensormonitor SENSORMON sensormon sensormond', + test_suite='setup.get_test_suite' +) diff --git a/sonic-sensormond/tests/__init__.py b/sonic-sensormond/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/sonic-sensormond/tests/mock_platform.py b/sonic-sensormond/tests/mock_platform.py new file mode 100644 index 000000000..146c1aff9 --- /dev/null +++ b/sonic-sensormond/tests/mock_platform.py @@ -0,0 +1,300 @@ +from sonic_platform_base import chassis_base +from sonic_platform_base import module_base + +class MockVoltageSensor(): + def __init__(self, index=None): + self._name = 'Voltage sensor {}'.format(index) if index != None else None + self._presence = True + self._model = 'Voltage sensor model' + self._serial = 'Voltage sensor serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = False + + self._value = 2 + self._minimum_value = 1 + self._maximum_value = 5 + self._high_threshold = 3 + self._low_threshold = 1 + self._high_critical_threshold = 4 + self._low_critical_threshold = 0 + + def get_value(self): + return self._value + + def get_unit(self): + return "mV" + + def get_minimum_recorded(self): + return self._minimum_value + + def get_maximum_recorded(self): + return self._maximum_value + + def get_high_threshold(self): + return self._high_threshold + + def get_low_threshold(self): + return self._low_threshold + + def get_high_critical_threshold(self): + return self._high_critical_threshold + + def get_low_critical_threshold(self): + return self._low_critical_threshold + + def make_over_threshold(self): + self._high_threshold = 2 + self._value = 3 + self._low_threshold = 1 + + def make_under_threshold(self): + self._high_threshold = 3 + self._value = 1 + self._low_threshold = 2 + + def make_normal_value(self): + self._high_threshold = 3 + self._value = 2 + self._low_threshold = 1 + + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name + + def get_presence(self): + return self._presence + + def set_presence(self, presence): + self._presence = presence + + def get_model(self): + return self._model + + def get_serial(self): + return self._serial + + def get_status(self): + return self._status + + def set_status(self, status): + self._status = status + + def get_position_in_parent(self): + return self._position_in_parent + + def is_replaceable(self): + return self._replaceable + +class MockCurrentSensor(): + def __init__(self, index=None): + self._name = 'Current sensor {}'.format(index) if index != None else None + self._presence = True + self._model = 'Current sensor model' + self._serial = 'Current sensor serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = False + + self._value = 2 + self._minimum_value = 1 + self._maximum_value = 5 + self._high_threshold = 3 + self._low_threshold = 1 + self._high_critical_threshold = 4 + self._low_critical_threshold = 0 + + def get_value(self): + return self._value + + def get_unit(self): + return "mA" + + def get_minimum_recorded(self): + return self._minimum_value + + def get_maximum_recorded(self): + return self._maximum_value + + def get_high_threshold(self): + return self._high_threshold + + def get_low_threshold(self): + return self._low_threshold + + def get_high_critical_threshold(self): + return self._high_critical_threshold + + def get_low_critical_threshold(self): + return self._low_critical_threshold + + def make_over_threshold(self): + self._high_threshold = 2 + self._value = 3 + self._low_threshold = 1 + + def make_under_threshold(self): + self._high_threshold = 3 + self._value = 1 + self._low_threshold = 2 + + def make_normal_value(self): + self._high_threshold = 3 + self._value = 2 + self._low_threshold = 1 + + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name + + def get_presence(self): + return self._presence + + def set_presence(self, presence): + self._presence = presence + + def get_model(self): + return self._model + + def get_serial(self): + return self._serial + + def get_status(self): + return self._status + + def set_status(self, status): + self._status = status + + def get_position_in_parent(self): + return self._position_in_parent + + def is_replaceable(self): + return self._replaceable + +class MockErrorVoltageSensor(MockVoltageSensor): + def get_value(self): + raise Exception('Failed to get voltage') + +class MockErrorCurrentSensor(MockCurrentSensor): + def get_value(self): + raise Exception('Failed to get current') + +class MockChassis(chassis_base.ChassisBase): + def __init__(self): + super(MockChassis, self).__init__() + self._name = None + self._presence = True + self._model = 'Chassis Model' + self._serial = 'Chassis Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = False + self._current_sensor_list = [] + self._voltage_sensor_list = [] + + self._is_chassis_system = False + self._my_slot = module_base.ModuleBase.MODULE_INVALID_SLOT + + def get_num_voltage_sensors(self): + return len(self._voltage_sensor_list) + + def get_num_current_sensors(self): + return len(self._current_sensor_list) + + def get_all_voltage_sensors(self): + return self._voltage_sensor_list + + def get_all_current_sensors(self): + return self._current_sensor_list + + def make_over_threshold_voltage_sensor(self): + voltage_sensor = MockVoltageSensor() + voltage_sensor.make_over_threshold() + self._voltage_sensor_list.append(voltage_sensor) + + def make_under_threshold_voltage_sensor(self): + voltage_sensor = MockVoltageSensor() + voltage_sensor.make_under_threshold() + self._voltage_sensor_list.append(voltage_sensor) + + def make_error_voltage_sensor(self): + voltage_sensor = MockErrorVoltageSensor() + self._voltage_sensor_list.append(voltage_sensor) + + def make_module_voltage_sensor(self): + module = MockModule() + self._module_list.append(module) + module._voltage_sensor_list.append(MockVoltageSensor()) + + def make_over_threshold_current_sensor(self): + current_sensor = MockCurrentSensor() + current_sensor.make_over_threshold() + self._current_sensor_list.append(current_sensor) + + def make_under_threshold_current_sensor(self): + current_sensor = MockCurrentSensor() + current_sensor.make_under_threshold() + self._current_sensor_list.append(current_sensor) + + def make_error_current_sensor(self): + current_sensor = MockErrorCurrentSensor() + self._current_sensor_list.append(current_sensor) + + def make_module_current_sensor(self): + module = MockModule() + self._module_list.append(module) + module._current_sensor_list.append(MockCurrentSensor()) + + def is_modular_chassis(self): + return self._is_chassis_system + + def set_modular_chassis(self, is_true): + self._is_chassis_system = is_true + + def set_my_slot(self, my_slot): + self._my_slot = my_slot + + def get_my_slot(self): + return self._my_slot + + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name + + def get_presence(self): + return self._presence + + def set_presence(self, presence): + self._presence = presence + + def get_model(self): + return self._model + + def get_serial(self): + return self._serial + + def get_status(self): + return self._status + + def set_status(self, status): + self._status = status + + def get_position_in_parent(self): + return self._position_in_parent + + def is_replaceable(self): + return self._replaceable + + +class MockModule(module_base.ModuleBase): + def __init__(self): + super(MockModule, self).__init__() + self._current_sensor_list = [] + self._voltage_sensor_list = [] + + def get_all_voltage_sensors(self): + return self._voltage_sensor_list + + def get_all_current_sensors(self): + return self._current_sensor_list + diff --git a/sonic-sensormond/tests/mock_swsscommon.py b/sonic-sensormond/tests/mock_swsscommon.py new file mode 100644 index 000000000..be9b0544c --- /dev/null +++ b/sonic-sensormond/tests/mock_swsscommon.py @@ -0,0 +1,60 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + +from swsssdk import ConfigDBConnector, SonicDBConfig, SonicV2Connector + +STATE_DB = '' + + +class Table: + def __init__(self, db, table_name): + self.table_name = table_name + self.mock_dict = {} + self.mock_keys = [] + + def _del(self, key): + del self.mock_dict[key] + pass + + def set(self, key, fvs): + self.mock_dict[key] = fvs.fv_dict + pass + + def get(self, key): + if key in self.mock_dict: + return self.mock_dict[key] + return None + + def get_size(self): + return (len(self.mock_dict)) + + def getKeys(self): + return self.mock_keys + + +class FieldValuePairs: + fv_dict = {} + + def __init__(self, tuple_list): + if isinstance(tuple_list, list) and isinstance(tuple_list[0], tuple): + self.fv_dict = dict(tuple_list) + + def __setitem__(self, key, kv_tuple): + self.fv_dict[kv_tuple[0]] = kv_tuple[1] + + def __getitem__(self, key): + return self.fv_dict[key] + + def __eq__(self, other): + if not isinstance(other, FieldValuePairs): + # don't attempt to compare against unrelated types + return NotImplemented + + return self.fv_dict == other.fv_dict + + def __repr__(self): + return repr(self.fv_dict) + + def __str__(self): + return repr(self.fv_dict) diff --git a/sonic-sensormond/tests/mocked_libs/sonic_platform/__init__.py b/sonic-sensormond/tests/mocked_libs/sonic_platform/__init__.py new file mode 100644 index 000000000..e491d5b52 --- /dev/null +++ b/sonic-sensormond/tests/mocked_libs/sonic_platform/__init__.py @@ -0,0 +1,6 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +from . import chassis +from . import platform diff --git a/sonic-sensormond/tests/mocked_libs/sonic_platform/chassis.py b/sonic-sensormond/tests/mocked_libs/sonic_platform/chassis.py new file mode 100644 index 000000000..ed76efbf7 --- /dev/null +++ b/sonic-sensormond/tests/mocked_libs/sonic_platform/chassis.py @@ -0,0 +1,16 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +import sys +from unittest import mock +from sonic_platform_base.chassis_base import ChassisBase + + +class Chassis(ChassisBase): + def __init__(self): + ChassisBase.__init__(self) + self._eeprom = mock.MagicMock() + + def get_eeprom(self): + return self._eeprom diff --git a/sonic-sensormond/tests/mocked_libs/sonic_platform/platform.py b/sonic-sensormond/tests/mocked_libs/sonic_platform/platform.py new file mode 100644 index 000000000..e1e7735f3 --- /dev/null +++ b/sonic-sensormond/tests/mocked_libs/sonic_platform/platform.py @@ -0,0 +1,11 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +from sonic_platform_base.platform_base import PlatformBase +from sonic_platform.chassis import Chassis + +class Platform(PlatformBase): + def __init__(self): + PlatformBase.__init__(self) + self._chassis = Chassis() diff --git a/sonic-sensormond/tests/test_sensormond.py b/sonic-sensormond/tests/test_sensormond.py new file mode 100644 index 000000000..7a6f34886 --- /dev/null +++ b/sonic-sensormond/tests/test_sensormond.py @@ -0,0 +1,496 @@ +import os +import sys +import multiprocessing +from imp import load_source +from unittest import mock +import pytest +from sonic_py_common import daemon_base +from swsscommon import swsscommon + +# Setup load paths for mocked modules + +tests_path = os.path.dirname(os.path.abspath(__file__)) +mocked_libs_path = os.path.join(tests_path, 'mocked_libs') +sys.path.insert(0, mocked_libs_path) +modules_path = os.path.dirname(tests_path) +scripts_path = os.path.join(modules_path, 'scripts') +sys.path.insert(0, modules_path) + +# Import mocked modules + +from .mock_swsscommon import Table, FieldValuePairs +from .mock_platform import MockChassis, MockVoltageSensor, MockCurrentSensor + +# Load file under test +load_source('sensormond', os.path.join(scripts_path, 'sensormond')) +import sensormond + +daemon_base.db_connect = mock.MagicMock() +swsscommon.Table = Table +swsscommon.FieldValuePairs = FieldValuePairs + +VOLTAGE_INFO_TABLE_NAME = 'VOLTAGE_INFO' +CURRENT_INFO_TABLE_NAME = 'CURRENT_INFO' + +@pytest.fixture(scope='function', autouse=True) +def configure_mocks(): + sensormond.SensorStatus.log_notice = mock.MagicMock() + sensormond.SensorStatus.log_warning = mock.MagicMock() + sensormond.VoltageUpdater.log_notice = mock.MagicMock() + sensormond.VoltageUpdater.log_warning = mock.MagicMock() + sensormond.CurrentUpdater.log_notice = mock.MagicMock() + sensormond.CurrentUpdater.log_warning = mock.MagicMock() + + yield + + sensormond.SensorStatus.log_notice.reset() + sensormond.SensorStatus.log_warning.reset() + sensormond.VoltageUpdater.log_notice.reset() + sensormond.VoltageUpdater.log_warning.reset() + sensormond.CurrentUpdater.log_notice.reset() + sensormond.CurrentUpdater.log_warning.reset() + +def test_sensor_status_set_over_threshold(): + sensor_status = sensormond.SensorStatus() + ret = sensor_status.set_over_threshold(sensormond.NOT_AVAILABLE, sensormond.NOT_AVAILABLE) + assert not ret + + ret = sensor_status.set_over_threshold(sensormond.NOT_AVAILABLE, 0) + assert not ret + + ret = sensor_status.set_over_threshold(0, sensormond.NOT_AVAILABLE) + assert not ret + + ret = sensor_status.set_over_threshold(2, 1) + assert ret + assert sensor_status.over_threshold + + ret = sensor_status.set_over_threshold(1, 2) + assert ret + assert not sensor_status.over_threshold + + +def test_sensor_status_set_under_threshold(): + sensor_status = sensormond.SensorStatus() + ret = sensor_status.set_under_threshold(sensormond.NOT_AVAILABLE, sensormond.NOT_AVAILABLE) + assert not ret + + ret = sensor_status.set_under_threshold(sensormond.NOT_AVAILABLE, 0) + assert not ret + + ret = sensor_status.set_under_threshold(0, sensormond.NOT_AVAILABLE) + assert not ret + + ret = sensor_status.set_under_threshold(1, 2) + assert ret + assert sensor_status.under_threshold + + ret = sensor_status.set_under_threshold(2, 1) + assert ret + assert not sensor_status.under_threshold + + +def test_sensor_status_set_not_available(): + SENSOR_NAME = 'Chassis 1 Sensor 1' + sensor_status = sensormond.SensorStatus() + sensor_status.value = 20.0 + + sensor_status.set_value(SENSOR_NAME, sensormond.NOT_AVAILABLE) + assert sensor_status.value is None + assert sensor_status.log_warning.call_count == 1 + sensor_status.log_warning.assert_called_with('Value of {} became unavailable'.format(SENSOR_NAME)) + +class TestVoltageUpdater(object): + """ + Test cases to cover functionality in VoltageUpdater class + """ + def test_deinit(self): + chassis = MockChassis() + voltage_updater = sensormond.VoltageUpdater(chassis) + voltage_updater.voltage_status_dict = {'key1': 'value1', 'key2': 'value2'} + voltage_updater.table = Table("STATE_DB", "xtable") + voltage_updater.table._del = mock.MagicMock() + voltage_updater.table.getKeys = mock.MagicMock(return_value=['key1','key2']) + voltage_updater.phy_entity_table = Table("STATE_DB", "ytable") + voltage_updater.phy_entity_table._del = mock.MagicMock() + voltage_updater.phy_entity_table.getKeys = mock.MagicMock(return_value=['key1','key2']) + voltage_updater.chassis_table = Table("STATE_DB", "ctable") + voltage_updater.chassis_table._del = mock.MagicMock() + voltage_updater.is_chassis_system = True + + voltage_updater.__del__() + assert voltage_updater.table.getKeys.call_count == 1 + assert voltage_updater.table._del.call_count == 2 + expected_calls = [mock.call('key1'), mock.call('key2')] + voltage_updater.table._del.assert_has_calls(expected_calls, any_order=True) + + def test_over_voltage(self): + chassis = MockChassis() + chassis.make_over_threshold_voltage_sensor() + voltage_updater = sensormond.VoltageUpdater(chassis) + voltage_updater.update() + voltage_sensor_list = chassis.get_all_voltage_sensors() + assert voltage_updater.log_warning.call_count == 1 + voltage_updater.log_warning.assert_called_with('High voltage warning: chassis 1 voltage_sensor 1 current voltage 3mV, high threshold 2mV') + + voltage_sensor_list[0].make_normal_value() + voltage_updater.update() + assert voltage_updater.log_notice.call_count == 1 + voltage_updater.log_notice.assert_called_with('High voltage warning cleared: chassis 1 voltage_sensor 1 voltage restored to 2mV, high threshold 3mV') + + def test_under_voltage(self): + chassis = MockChassis() + chassis.make_under_threshold_voltage_sensor() + voltage_updater = sensormond.VoltageUpdater(chassis) + voltage_updater.update() + voltage_sensor_list = chassis.get_all_voltage_sensors() + assert voltage_updater.log_warning.call_count == 1 + voltage_updater.log_warning.assert_called_with('Low voltage warning: chassis 1 voltage_sensor 1 current voltage 1mV, low threshold 2mV') + + voltage_sensor_list[0].make_normal_value() + voltage_updater.update() + assert voltage_updater.log_notice.call_count == 1 + voltage_updater.log_notice.assert_called_with('Low voltage warning cleared: chassis 1 voltage_sensor 1 voltage restored to 2mV, low threshold 1mV') + + def test_update_voltage_sensor_with_exception(self): + chassis = MockChassis() + chassis.make_error_voltage_sensor() + voltage_sensor = MockVoltageSensor() + voltage_sensor.make_over_threshold() + chassis.get_all_voltage_sensors().append(voltage_sensor) + + voltage_updater = sensormond.VoltageUpdater(chassis) + voltage_updater.update() + assert voltage_updater.log_warning.call_count == 2 + + if sys.version_info.major == 3: + expected_calls = [ + mock.call("Failed to update voltage_sensor status for chassis 1 voltage_sensor 1 - Exception('Failed to get voltage')"), + mock.call('High voltage warning: chassis 1 voltage_sensor 2 current voltage 3mV, high threshold 2mV') + ] + else: + expected_calls = [ + mock.call("Failed to update voltage_sensor status for chassis 1 voltage_sensor 1 - Exception('Failed to get voltage',)"), + mock.call('High voltage warning: chassis 1 voltage_sensor 2 current voltage 3mV, high threshold 2mV') + ] + assert voltage_updater.log_warning.mock_calls == expected_calls + + def test_update_module_voltage_sensors(self): + chassis = MockChassis() + chassis.make_module_voltage_sensor() + chassis.set_modular_chassis(True) + voltage_updater = sensormond.VoltageUpdater(chassis) + voltage_updater.update() + assert len(voltage_updater.module_voltage_sensors) == 1 + + chassis._module_list = [] + voltage_updater.update() + assert len(voltage_updater.module_voltage_sensors) == 0 + + +class TestCurrentUpdater(object): + """ + Test cases to cover functionality in CurrentUpdater class + """ + def test_deinit(self): + chassis = MockChassis() + current_updater = sensormond.CurrentUpdater(chassis) + current_updater.current_status_dict = {'key1': 'value1', 'key2': 'value2'} + current_updater.table = Table("STATE_DB", "xtable") + current_updater.table._del = mock.MagicMock() + current_updater.table.getKeys = mock.MagicMock(return_value=['key1','key2']) + current_updater.phy_entity_table = Table("STATE_DB", "ytable") + current_updater.phy_entity_table._del = mock.MagicMock() + current_updater.phy_entity_table.getKeys = mock.MagicMock(return_value=['key1','key2']) + current_updater.chassis_table = Table("STATE_DB", "ctable") + current_updater.chassis_table._del = mock.MagicMock() + current_updater.is_chassis_system = True + + current_updater.__del__() + assert current_updater.table.getKeys.call_count == 1 + assert current_updater.table._del.call_count == 2 + expected_calls = [mock.call('key1'), mock.call('key2')] + current_updater.table._del.assert_has_calls(expected_calls, any_order=True) + + def test_over_current(self): + chassis = MockChassis() + chassis.make_over_threshold_current_sensor() + current_updater = sensormond.CurrentUpdater(chassis) + current_updater.update() + current_sensor_list = chassis.get_all_current_sensors() + assert current_updater.log_warning.call_count == 1 + current_updater.log_warning.assert_called_with('High Current warning: chassis 1 current_sensor 1 current Current 3mA, high threshold 2mA') + + current_sensor_list[0].make_normal_value() + current_updater.update() + assert current_updater.log_notice.call_count == 1 + current_updater.log_notice.assert_called_with('High Current warning cleared: chassis 1 current_sensor 1 current restored to 2mA, high threshold 3mA') + + def test_under_current(self): + chassis = MockChassis() + chassis.make_under_threshold_current_sensor() + current_updater = sensormond.CurrentUpdater(chassis) + current_updater.update() + current_sensor_list = chassis.get_all_current_sensors() + assert current_updater.log_warning.call_count == 1 + current_updater.log_warning.assert_called_with('Low current warning: chassis 1 current_sensor 1 current current 1mA, low threshold 2mA') + + current_sensor_list[0].make_normal_value() + current_updater.update() + assert current_updater.log_notice.call_count == 1 + current_updater.log_notice.assert_called_with('Low current warning cleared: chassis 1 current_sensor 1 current restored to 2mA, low threshold 1mA') + + def test_update_current_sensor_with_exception(self): + chassis = MockChassis() + chassis.make_error_current_sensor() + current_sensor = MockCurrentSensor() + current_sensor.make_over_threshold() + chassis.get_all_current_sensors().append(current_sensor) + + current_updater = sensormond.CurrentUpdater(chassis) + current_updater.update() + assert current_updater.log_warning.call_count == 2 + + if sys.version_info.major == 3: + expected_calls = [ + mock.call("Failed to update current_sensor status for chassis 1 current_sensor 1 - Exception('Failed to get current')"), + mock.call('High Current warning: chassis 1 current_sensor 2 current Current 3mA, high threshold 2mA') + ] + else: + expected_calls = [ + mock.call("Failed to update current_sensor status for chassis 1 current_sensor 1 - Exception('Failed to get current',)"), + mock.call('High Current warning: chassis 1 current_sensor 2 current Current 3mA, high threshold 2mA') + ] + assert current_updater.log_warning.mock_calls == expected_calls + + def test_update_module_current_sensors(self): + chassis = MockChassis() + chassis.make_module_current_sensor() + chassis.set_modular_chassis(True) + current_updater = sensormond.CurrentUpdater(chassis) + current_updater.update() + assert len(current_updater.module_current_sensors) == 1 + + chassis._module_list = [] + current_updater.update() + assert len(current_updater.module_current_sensors) == 0 + +# Modular chassis-related tests + + +def test_updater_voltage_sensor_check_modular_chassis(): + chassis = MockChassis() + assert chassis.is_modular_chassis() == False + + voltage_updater = sensormond.VoltageUpdater(chassis) + assert voltage_updater.chassis_table == None + + chassis.set_modular_chassis(True) + chassis.set_my_slot(-1) + voltage_updater = sensormond.VoltageUpdater(chassis) + assert voltage_updater.chassis_table == None + + my_slot = 1 + chassis.set_my_slot(my_slot) + voltage_updater = sensormond.VoltageUpdater(chassis) + assert voltage_updater.chassis_table != None + assert voltage_updater.chassis_table.table_name == '{}_{}'.format(VOLTAGE_INFO_TABLE_NAME, str(my_slot)) + + +def test_updater_voltage_sensor_check_chassis_table(): + chassis = MockChassis() + + voltage_sensor1 = MockVoltageSensor() + chassis.get_all_voltage_sensors().append(voltage_sensor1) + + chassis.set_modular_chassis(True) + chassis.set_my_slot(1) + voltage_updater = sensormond.VoltageUpdater(chassis) + + voltage_updater.update() + assert voltage_updater.chassis_table.get_size() == chassis.get_num_voltage_sensors() + + voltage_sensor2 = MockVoltageSensor() + chassis.get_all_voltage_sensors().append(voltage_sensor2) + voltage_updater.update() + assert voltage_updater.chassis_table.get_size() == chassis.get_num_voltage_sensors() + +def test_updater_voltage_sensor_check_min_max(): + chassis = MockChassis() + + voltage_sensor = MockVoltageSensor(1) + chassis.get_all_voltage_sensors().append(voltage_sensor) + + chassis.set_modular_chassis(True) + chassis.set_my_slot(1) + voltage_updater = sensormond.VoltageUpdater(chassis) + + voltage_updater.update() + slot_dict = voltage_updater.chassis_table.get(voltage_sensor.get_name()) + assert slot_dict['minimum_voltage'] == str(voltage_sensor.get_minimum_recorded()) + assert slot_dict['maximum_voltage'] == str(voltage_sensor.get_maximum_recorded()) + + +def test_updater_current_sensor_check_modular_chassis(): + chassis = MockChassis() + assert chassis.is_modular_chassis() == False + + current_updater = sensormond.CurrentUpdater(chassis) + assert current_updater.chassis_table == None + + chassis.set_modular_chassis(True) + chassis.set_my_slot(-1) + current_updater = sensormond.CurrentUpdater(chassis) + assert current_updater.chassis_table == None + + my_slot = 1 + chassis.set_my_slot(my_slot) + current_updater = sensormond.CurrentUpdater(chassis) + assert current_updater.chassis_table != None + assert current_updater.chassis_table.table_name == '{}_{}'.format(CURRENT_INFO_TABLE_NAME, str(my_slot)) + + +def test_updater_current_sensor_check_chassis_table(): + chassis = MockChassis() + + current_sensor1 = MockCurrentSensor() + chassis.get_all_current_sensors().append(current_sensor1) + + chassis.set_modular_chassis(True) + chassis.set_my_slot(1) + current_updater = sensormond.CurrentUpdater(chassis) + + current_updater.update() + assert current_updater.chassis_table.get_size() == chassis.get_num_current_sensors() + + current_sensor2 = MockCurrentSensor() + chassis.get_all_current_sensors().append(current_sensor2) + current_updater.update() + assert current_updater.chassis_table.get_size() == chassis.get_num_current_sensors() + + +def test_updater_current_sensor_check_min_max(): + chassis = MockChassis() + + current_sensor = MockCurrentSensor(1) + chassis.get_all_current_sensors().append(current_sensor) + + chassis.set_modular_chassis(True) + chassis.set_my_slot(1) + current_updater = sensormond.CurrentUpdater(chassis) + + current_updater.update() + slot_dict = current_updater.chassis_table.get(current_sensor.get_name()) + assert slot_dict['minimum_current'] == str(current_sensor.get_minimum_recorded()) + assert slot_dict['maximum_current'] == str(current_sensor.get_maximum_recorded()) + +def test_signal_handler(): + # Test SIGHUP + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.set = mock.MagicMock() + daemon_sensormond.log_info = mock.MagicMock() + daemon_sensormond.log_warning = mock.MagicMock() + daemon_sensormond.signal_handler(sensormond.signal.SIGHUP, None) + assert daemon_sensormond.log_info.call_count == 1 + daemon_sensormond.log_info.assert_called_with("Caught signal 'SIGHUP' - ignoring...") + assert daemon_sensormond.log_warning.call_count == 0 + assert daemon_sensormond.stop_event.set.call_count == 0 + assert sensormond.exit_code == 1 + + # Test SIGINT + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.set = mock.MagicMock() + daemon_sensormond.log_info = mock.MagicMock() + daemon_sensormond.log_warning = mock.MagicMock() + test_signal = sensormond.signal.SIGINT + daemon_sensormond.signal_handler(test_signal, None) + assert daemon_sensormond.log_info.call_count == 1 + daemon_sensormond.log_info.assert_called_with("Caught signal 'SIGINT' - exiting...") + assert daemon_sensormond.log_warning.call_count == 0 + assert daemon_sensormond.stop_event.set.call_count == 1 + assert sensormond.exit_code == (128 + test_signal) + + # Test SIGTERM + sensormond.exit_code = 1 + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.set = mock.MagicMock() + daemon_sensormond.log_info = mock.MagicMock() + daemon_sensormond.log_warning = mock.MagicMock() + test_signal = sensormond.signal.SIGTERM + daemon_sensormond.signal_handler(test_signal, None) + assert daemon_sensormond.log_info.call_count == 1 + daemon_sensormond.log_info.assert_called_with("Caught signal 'SIGTERM' - exiting...") + assert daemon_sensormond.log_warning.call_count == 0 + assert daemon_sensormond.stop_event.set.call_count == 1 + assert sensormond.exit_code == (128 + test_signal) + + # Test an unhandled signal + sensormond.exit_code = 1 + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.set = mock.MagicMock() + daemon_sensormond.log_info = mock.MagicMock() + daemon_sensormond.log_warning = mock.MagicMock() + daemon_sensormond.signal_handler(sensormond.signal.SIGUSR1, None) + assert daemon_sensormond.log_warning.call_count == 1 + daemon_sensormond.log_warning.assert_called_with("Caught unhandled signal 'SIGUSR1' - ignoring...") + assert daemon_sensormond.log_info.call_count == 0 + assert daemon_sensormond.stop_event.set.call_count == 0 + assert sensormond.exit_code == 1 + + +def test_daemon_run(): + + import sonic_platform.platform + class MyPlatform(): + def get_chassis(self): + return MockChassis() + sonic_platform.platform.Platform = MyPlatform + + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.wait = mock.MagicMock(return_value=True) + ret = daemon_sensormond.run() + assert ret is False + + daemon_sensormond = sensormond.SensorMonitorDaemon() + daemon_sensormond.stop_event.wait = mock.MagicMock(return_value=False) + ret = daemon_sensormond.run() + assert ret is True + + +def test_try_get(): + def good_callback(): + return 'good result' + + def unimplemented_callback(): + raise NotImplementedError + + ret = sensormond.try_get(good_callback) + assert ret == 'good result' + + ret = sensormond.try_get(unimplemented_callback) + assert ret == sensormond.NOT_AVAILABLE + + ret = sensormond.try_get(unimplemented_callback, 'my default') + assert ret == 'my default' + + +def test_update_entity_info(): + mock_table = mock.MagicMock() + mock_voltage_sensor = MockVoltageSensor() + expected_fvp = sensormond.swsscommon.FieldValuePairs( + [('position_in_parent', '1'), + ('parent_name', 'Parent Name') + ]) + + sensormond.update_entity_info(mock_table, 'Parent Name', 'Key Name', mock_voltage_sensor, 1) + assert mock_table.set.call_count == 1 + mock_table.set.assert_called_with('Key Name', expected_fvp) + + +@mock.patch('sensormond.SensorMonitorDaemon.run') +def test_main(mock_run): + mock_run.return_value = False + + ret = sensormond.main() + assert mock_run.call_count == 1 + assert ret != 0