From 8509f43ccdc964c843db6981a10f3305db8574a3 Mon Sep 17 00:00:00 2001 From: Joe LeVeque Date: Thu, 18 Mar 2021 10:10:20 -0700 Subject: [PATCH] [thermalctld] Refactor to allow for greater unit test coverage; Add more unit tests (#157) - Refactor thermalctld to reduce the amount of code in infinite loops, thus allowing us better unit test coverage - Refactor mock_platform.py such that it inherits from sonic-platform-common in order to ensure it is aligned with the current API definitions (this introduces a test-time dependency on the sonic-platform-common package) - Increase pytest verbosity to prevent truncation of error messages - Miscellaneous cleanup: - Fixes to grammar - Remove unnecessary punctuation from log messages - Increase overall unit test unit test coverage from 73% to 93% --- azure-pipelines.yml | 4 + sonic-thermalctld/pytest.ini | 2 +- sonic-thermalctld/scripts/thermalctld | 326 +++---- sonic-thermalctld/setup.py | 3 +- sonic-thermalctld/tests/mock_platform.py | 478 ++++++---- sonic-thermalctld/tests/mock_swsscommon.py | 4 + .../mocked_libs/sonic_platform/__init__.py | 6 + .../mocked_libs/sonic_platform/chassis.py | 25 + .../mocked_libs/sonic_platform/platform.py | 11 + .../tests/mocked_libs/swsscommon/__init__.py | 5 + .../mocked_libs/swsscommon/swsscommon.py | 54 ++ sonic-thermalctld/tests/test_thermalctld.py | 813 ++++++++++++------ 12 files changed, 1189 insertions(+), 542 deletions(-) create mode 100644 sonic-thermalctld/tests/mocked_libs/sonic_platform/__init__.py create mode 100644 sonic-thermalctld/tests/mocked_libs/sonic_platform/chassis.py create mode 100644 sonic-thermalctld/tests/mocked_libs/sonic_platform/platform.py create mode 100644 sonic-thermalctld/tests/mocked_libs/swsscommon/__init__.py create mode 100644 sonic-thermalctld/tests/mocked_libs/swsscommon/swsscommon.py diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 05bb0b21d163..cd7d1f3cdff3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -81,8 +81,12 @@ jobs: set -xe sudo pip2 install swsssdk-2.0.1-py2-none-any.whl sudo pip2 install sonic_py_common-1.0-py2-none-any.whl + sudo pip2 install sonic_config_engine-1.0-py2-none-any.whl + sudo pip2 install sonic_platform_common-1.0-py2-none-any.whl sudo pip3 install swsssdk-2.0.1-py3-none-any.whl sudo pip3 install sonic_py_common-1.0-py3-none-any.whl + sudo pip3 install sonic_config_engine-1.0-py3-none-any.whl + sudo pip3 install sonic_platform_common-1.0-py3-none-any.whl workingDirectory: $(Pipeline.Workspace)/target/python-wheels/ displayName: 'Install Python dependencies' diff --git a/sonic-thermalctld/pytest.ini b/sonic-thermalctld/pytest.ini index aa4fe636e352..d90ee9ed9e12 100644 --- a/sonic-thermalctld/pytest.ini +++ b/sonic-thermalctld/pytest.ini @@ -1,2 +1,2 @@ [pytest] -addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -v +addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv diff --git a/sonic-thermalctld/scripts/thermalctld b/sonic-thermalctld/scripts/thermalctld index edadb90085d9..1da7d432845e 100644 --- a/sonic-thermalctld/scripts/thermalctld +++ b/sonic-thermalctld/scripts/thermalctld @@ -1,28 +1,26 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 """ thermalctld Thermal control daemon for SONiC """ -try: - import os - import signal - import threading - import time - from datetime import datetime +import signal +import sys +import threading +import time +from datetime import datetime - from sonic_py_common import daemon_base, logger - from sonic_py_common.task_base import ProcessTaskBase +import sonic_platform +from sonic_py_common import daemon_base, logger +from sonic_py_common.task_base import ProcessTaskBase +from swsscommon import swsscommon - # If unit testing is occurring, mock swsscommon - if os.getenv("THERMALCTLD_UNIT_TESTING") == "1": - from tests import mock_swsscommon as swsscommon - else: - from swsscommon import swsscommon -except ImportError as e: - raise ImportError(repr(e) + " - required module not found") +# TODO: Once we no longer support Python 2, we can eliminate this and get the +# name using the 'name' field (e.g., `signal.SIGINT.name`) starting with Python 3.5 +SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n) + for n in dir(signal) if n.startswith('SIG') and '_' not in n) SYSLOG_IDENTIFIER = 'thermalctld' NOT_AVAILABLE = 'N/A' @@ -30,10 +28,15 @@ CHASSIS_INFO_KEY = 'chassis 1' PHYSICAL_ENTITY_INFO_TABLE = 'PHYSICAL_ENTITY_INFO' INVALID_SLOT = -1 -# utility functions +ERR_UNKNOWN = 1 +ERR_INIT_FAILED = 2 -# try get information from platform API and return a default value if caught NotImplementedError +# Thermal control daemon is designed to never exit, it must always +# return non-zero exit code when exiting and so that supervisord will +# restart it automatically. +exit_code = ERR_UNKNOWN +# utility functions def try_get(callback, default=NOT_AVAILABLE): """ @@ -60,14 +63,14 @@ def update_entity_info(table, parent_name, key, device, device_index): class FanStatus(logger.Logger): - absence_fan_count = 0 - fault_fan_count = 0 + absent_fan_count = 0 + faulty_fan_count = 0 - def __init__(self, log_identifier, fan=None, is_psu_fan=False): + def __init__(self, fan=None, is_psu_fan=False): """ - Constructor of FanStatus + Initializer of FanStatus """ - super(FanStatus, self).__init__(log_identifier) + super(FanStatus, self).__init__(SYSLOG_IDENTIFIER) self.fan = fan self.is_psu_fan = is_psu_fan @@ -79,12 +82,12 @@ class FanStatus(logger.Logger): @classmethod def get_bad_fan_count(cls): - return cls.absence_fan_count + cls.fault_fan_count + return cls.absent_fan_count + cls.faulty_fan_count @classmethod def reset_fan_counter(cls): - cls.absence_fan_count = 0 - cls.fault_fan_count = 0 + cls.absent_fan_count = 0 + cls.faulty_fan_count = 0 def set_presence(self, presence): """ @@ -93,7 +96,7 @@ class FanStatus(logger.Logger): :return: True if status changed else False """ if not presence and not self.is_psu_fan: - FanStatus.absence_fan_count += 1 + FanStatus.absent_fan_count += 1 if presence == self.presence: return False @@ -108,7 +111,7 @@ class FanStatus(logger.Logger): :return: True if status changed else False """ if not status: - FanStatus.fault_fan_count += 1 + FanStatus.faulty_fan_count += 1 if status == self.status: return False @@ -123,7 +126,7 @@ class FanStatus(logger.Logger): return False if current_status is True: - self.log_warning('Fan speed or target_speed or tolerance become unavailable, ' + self.log_warning('Fan speed or target_speed or tolerance became unavailable, ' 'speed={}, target_speed={}, tolerance={}'.format(speed, target_speed, tolerance)) return False return True @@ -173,11 +176,11 @@ class FanStatus(logger.Logger): Indicate the Fan works as expect :return: True if Fan works normal else False """ - return self.presence and \ - self.status and \ - not self.under_speed and \ - not self.over_speed and \ - not self.invalid_direction + return (self.presence and + self.status and + not self.under_speed and + not self.over_speed and + not self.invalid_direction) # @@ -188,12 +191,12 @@ class FanUpdater(logger.Logger): FAN_INFO_TABLE_NAME = 'FAN_INFO' FAN_DRAWER_INFO_TABLE_NAME = 'FAN_DRAWER_INFO' - def __init__(self, log_identifier, chassis): + def __init__(self, chassis): """ - Constructor for FanUpdater + Initializer for FanUpdater :param chassis: Object representing a platform chassis """ - super(FanUpdater, self).__init__(log_identifier) + super(FanUpdater, self).__init__(SYSLOG_IDENTIFIER) self.chassis = chassis self.fan_status_dict = {} @@ -204,7 +207,7 @@ class FanUpdater(logger.Logger): def deinit(self): """ - Destructor of FanUpdater + Deinitializer of FanUpdater :return: """ for name in self.fan_status_dict.keys(): @@ -238,24 +241,24 @@ class FanUpdater(logger.Logger): try: self._refresh_fan_status(drawer, drawer_index, fan, fan_index) except Exception as e: - self.log_warning('Failed to update FAN status - {}'.format(repr(e))) + self.log_warning('Failed to update fan status - {}'.format(repr(e))) for psu_index, psu in enumerate(self.chassis.get_all_psus()): for fan_index, fan in enumerate(psu.get_all_fans()): try: self._refresh_fan_status(psu, psu_index, fan, fan_index, True) except Exception as e: - self.log_warning('Failed to update PSU FAN status - {}'.format(repr(e))) + self.log_warning('Failed to update PSU fan status - {}'.format(repr(e))) self._update_led_color() bad_fan_count = FanStatus.get_bad_fan_count() if bad_fan_count > 0 and old_bad_fan_count != bad_fan_count: - self.log_warning("Insufficient number of working fans warning: {} fans are not working.".format( - bad_fan_count + self.log_warning("Insufficient number of working fans warning: {} fan{} not working".format( + bad_fan_count, " is" if bad_fan_count == 1 else "s are" )) elif old_bad_fan_count > 0 and bad_fan_count == 0: - self.log_notice("Insufficient number of working fans warning cleared: all fans are back to normal.") + self.log_notice("Insufficient number of working fans warning cleared: all fans are back to normal") self.log_debug("End fan updating") @@ -291,10 +294,10 @@ class FanUpdater(logger.Logger): parent_name = 'PSU {}'.format(parent_index + 1) else: parent_name = drawer_name if drawer_name != NOT_AVAILABLE else CHASSIS_INFO_KEY - fan_name = try_get(fan.get_name, '{} FAN {}'.format(parent_name, fan_index + 1)) + fan_name = try_get(fan.get_name, '{} fan {}'.format(parent_name, fan_index + 1)) update_entity_info(self.phy_entity_table, parent_name, fan_name, fan, fan_index + 1) if fan_name not in self.fan_status_dict: - self.fan_status_dict[fan_name] = FanStatus(SYSLOG_IDENTIFIER, fan, is_psu_fan) + self.fan_status_dict[fan_name] = FanStatus(fan, is_psu_fan) fan_status = self.fan_status_dict[fan_name] @@ -316,7 +319,7 @@ class FanUpdater(logger.Logger): if fan_status.set_presence(presence): set_led = True self._log_on_status_changed(fan_status.presence, - 'Fan removed warning cleared: {} was inserted.'.format(fan_name), + 'Fan removed warning cleared: {} was inserted'.format(fan_name), 'Fan removed warning: {} was removed from ' 'the system, potential overheat hazard'.format(fan_name) ) @@ -324,23 +327,23 @@ class FanUpdater(logger.Logger): if presence and fan_status.set_fault_status(fan_fault_status): set_led = True self._log_on_status_changed(fan_status.status, - 'Fan fault warning cleared: {} is back to normal.'.format(fan_name), - 'Fan fault warning: {} is broken.'.format(fan_name) + 'Fan fault warning cleared: {} is back to normal'.format(fan_name), + 'Fan fault warning: {} is broken'.format(fan_name) ) if presence and fan_status.set_under_speed(speed, speed_target, speed_tolerance): set_led = True self._log_on_status_changed(not fan_status.under_speed, - 'Fan low speed warning cleared: {} speed is back to normal.'.format(fan_name), - 'Fan low speed warning: {} current speed={}, target speed={}, tolerance={}.'. + 'Fan low speed warning cleared: {} speed is back to normal'.format(fan_name), + 'Fan low speed warning: {} current speed={}, target speed={}, tolerance={}'. format(fan_name, speed, speed_target, speed_tolerance) ) if presence and fan_status.set_over_speed(speed, speed_target, speed_tolerance): set_led = True self._log_on_status_changed(not fan_status.over_speed, - 'Fan high speed warning cleared: {} speed is back to normal.'.format(fan_name), - 'Fan high speed warning: {} target speed={}, current speed={}, tolerance={}.'. + 'Fan high speed warning cleared: {} speed is back to normal'.format(fan_name), + 'Fan high speed warning: {} target speed={}, current speed={}, tolerance={}'. format(fan_name, speed_target, speed, speed_tolerance) ) @@ -389,7 +392,7 @@ class FanUpdater(logger.Logger): fan.set_status_led(fan.STATUS_LED_COLOR_RED) fan_drawer.set_status_led(fan.STATUS_LED_COLOR_RED) except NotImplementedError as e: - self.log_warning('Failed to set led to fan, set_status_led not implemented') + self.log_warning('Failed to set status LED for fan {}, set_status_led not implemented'.format(fan_name)) def _update_led_color(self): for fan_name, fan_status in self.fan_status_dict.items(): @@ -398,7 +401,7 @@ class FanUpdater(logger.Logger): ('led_status', str(try_get(fan_status.fan.get_status_led))) ]) except Exception as e: - self.log_warning('Failed to get led status for fan - {}'.format(repr(e))) + self.log_warning('Failed to get status LED state for fan {} - {}'.format(fan_name, e)) fvs = swsscommon.FieldValuePairs([ ('led_status', NOT_AVAILABLE) ]) @@ -413,7 +416,7 @@ class FanUpdater(logger.Logger): ('led_status', str(try_get(drawer.get_status_led))) ]) except Exception as e: - self.log_warning('Failed to get led status for fan drawer') + self.log_warning('Failed to get status LED state for fan drawer') fvs = swsscommon.FieldValuePairs([ ('led_status', NOT_AVAILABLE) ]) @@ -423,8 +426,8 @@ class FanUpdater(logger.Logger): class TemperatureStatus(logger.Logger): TEMPERATURE_DIFF_THRESHOLD = 10 - def __init__(self, log_identifier): - super(TemperatureStatus, self).__init__(log_identifier) + def __init__(self): + super(TemperatureStatus, self).__init__(SYSLOG_IDENTIFIER) self.temperature = None self.over_temperature = False @@ -439,7 +442,7 @@ class TemperatureStatus(logger.Logger): """ if temperature == NOT_AVAILABLE: if self.temperature is not None: - self.log_warning('Temperature of {} become unavailable'.format(name)) + self.log_warning('Temperature of {} became unavailable'.format(name)) self.temperature = None return @@ -456,7 +459,7 @@ class TemperatureStatus(logger.Logger): def _check_temperature_value_available(self, temperature, threshold, current_status): if temperature == NOT_AVAILABLE or threshold == NOT_AVAILABLE: if current_status is True: - self.log_warning('Thermal temperature or threshold become unavailable, ' + self.log_warning('Thermal temperature or threshold became unavailable, ' 'temperature={}, threshold={}'.format(temperature, threshold)) return False return True @@ -507,12 +510,12 @@ class TemperatureUpdater(logger.Logger): # Temperature information table name in database TEMPER_INFO_TABLE_NAME = 'TEMPERATURE_INFO' - def __init__(self, log_identifier, chassis): + def __init__(self, chassis): """ - Constructor of TemperatureUpdater + Initializer of TemperatureUpdater :param chassis: Object representing a platform chassis """ - super(TemperatureUpdater, self).__init__(log_identifier) + super(TemperatureUpdater, self).__init__(SYSLOG_IDENTIFIER) self.chassis = chassis self.temperature_status_dict = {} @@ -531,7 +534,7 @@ class TemperatureUpdater(logger.Logger): def deinit(self): """ - Destructor of TemperatureUpdater + Deinitializer of TemperatureUpdater :return: """ for name in self.temperature_status_dict.keys(): @@ -570,7 +573,7 @@ class TemperatureUpdater(logger.Logger): try: self._refresh_temperature_status(parent_name, thermal, thermal_index) except Exception as e: - self.log_warning('Failed to update thermal status - {}'.format(e)) + self.log_warning('Failed to update thermal status - {}'.format(repr(e))) for sfp_index, sfp in enumerate(self.chassis.get_all_sfps()): parent_name = 'SFP {}'.format(sfp_index + 1) @@ -578,7 +581,7 @@ class TemperatureUpdater(logger.Logger): try: self._refresh_temperature_status(parent_name, thermal, thermal_index) except Exception as e: - self.log_warning('Failed to update thermal status - {}'.format(e)) + self.log_warning('Failed to update thermal status - {}'.format(repr(e))) self.log_debug("End temperature updating") @@ -600,7 +603,7 @@ class TemperatureUpdater(logger.Logger): update_entity_info(self.phy_entity_table, parent_name, name, thermal, thermal_index + 1) if name not in self.temperature_status_dict: - self.temperature_status_dict[name] = TemperatureStatus(SYSLOG_IDENTIFIER) + self.temperature_status_dict[name] = TemperatureStatus() temperature_status = self.temperature_status_dict[name] @@ -624,7 +627,7 @@ class TemperatureUpdater(logger.Logger): warning = False if temperature != NOT_AVAILABLE and temperature_status.set_over_temperature(temperature, high_threshold): self._log_on_status_changed(not temperature_status.over_temperature, - 'High temperature warning cleared: {} temperature restore to {}C, high threshold {}C.'. + 'High temperature warning cleared: {} temperature restored to {}C, high threshold {}C'. format(name, temperature, high_threshold), 'High temperature warning: {} current temperature {}C, high threshold {}C'. format(name, temperature, high_threshold) @@ -633,7 +636,7 @@ class TemperatureUpdater(logger.Logger): if temperature != NOT_AVAILABLE and temperature_status.set_under_temperature(temperature, low_threshold): self._log_on_status_changed(not temperature_status.under_temperature, - 'Low temperature warning cleared: {} temperature restore to {}C, low threshold {}C.'. + 'Low temperature warning cleared: {} temperature restored to {}C, low threshold {}C'. format(name, temperature, low_threshold), 'Low temperature warning: {} current temperature {}C, low threshold {}C'. format(name, temperature, low_threshold) @@ -661,22 +664,44 @@ class TemperatureUpdater(logger.Logger): class ThermalMonitor(ProcessTaskBase): # Initial update interval INITIAL_INTERVAL = 5 + # Update interval value UPDATE_INTERVAL = 60 + # Update elapse threshold. If update used time is larger than the value, generate a warning log. - UPDATE_ELAPSE_THRESHOLD = 30 + UPDATE_ELAPSED_THRESHOLD = 30 def __init__(self, chassis): """ - Constructor for ThermalMonitor + Initializer for ThermalMonitor :param chassis: Object representing a platform chassis """ - ProcessTaskBase.__init__(self) + super(ThermalMonitor, self).__init__() + + self.wait_time = self.INITIAL_INTERVAL # TODO: Refactor to eliminate the need for this Logger instance self.logger = logger.Logger(SYSLOG_IDENTIFIER) - self.fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - self.temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + + # Set minimum logging level to INFO + self.logger.set_min_log_priority_info() + + self.fan_updater = FanUpdater(chassis) + self.temperature_updater = TemperatureUpdater(chassis) + + def main(self): + begin = time.time() + self.fan_updater.update() + self.temperature_updater.update() + elapsed = time.time() - begin + if elapsed < self.UPDATE_INTERVAL: + self.wait_time = self.UPDATE_INTERVAL - elapsed + else: + self.wait_time = self.INITIAL_INTERVAL + + if elapsed > self.UPDATE_ELAPSED_THRESHOLD: + self.logger.log_warning('Update fan and temperature status took {} seconds, ' + 'there might be performance risk'.format(elapsed)) def task_worker(self): """ @@ -686,20 +711,8 @@ class ThermalMonitor(ProcessTaskBase): self.logger.log_info("Start thermal monitoring loop") # Start loop to update fan, temperature info in DB periodically - wait_time = ThermalMonitor.INITIAL_INTERVAL - while not self.task_stopping_event.wait(wait_time): - begin = time.time() - self.fan_updater.update() - self.temperature_updater.update() - elapse = time.time() - begin - if elapse < ThermalMonitor.UPDATE_INTERVAL: - wait_time = ThermalMonitor.UPDATE_INTERVAL - elapse - else: - wait_time = ThermalMonitor.INITIAL_INTERVAL - - if elapse > ThermalMonitor.UPDATE_ELAPSE_THRESHOLD: - self.logger.log_warning('Update fan and temperature status takes {} seconds, ' - 'there might be performance risk'.format(elapse)) + while not self.task_stopping_event.wait(self.wait_time): + self.main() self.fan_updater.deinit() self.temperature_updater.deinit() @@ -718,19 +731,50 @@ class ThermalControlDaemon(daemon_base.DaemonBase): POLICY_FILE = '/usr/share/sonic/platform/thermal_policy.json' - def __init__(self, log_identifier): + def __init__(self): """ - Constructor of ThermalControlDaemon + Initializer of ThermalControlDaemon """ - super(ThermalControlDaemon, self).__init__(log_identifier) + super(ThermalControlDaemon, self).__init__(SYSLOG_IDENTIFIER) + + # Set minimum logging level to INFO + self.set_min_log_priority_info() + self.stop_event = threading.Event() - # Thermal control daemon is designed to never exit, it must always - # return non zero exit code when exiting and so that supervisord will - # restart it automatically. - self.exit_code = 1 + self.wait_time = self.INTERVAL + + chassis = sonic_platform.platform.Platform().get_chassis() - # Signal handler + self.thermal_monitor = ThermalMonitor(chassis) + self.thermal_monitor.task_run() + + self.thermal_manager = None + try: + self.thermal_manager = chassis.get_thermal_manager() + if self.thermal_manager: + self.thermal_manager.initialize() + self.thermal_manager.load(ThermalControlDaemon.POLICY_FILE) + self.thermal_manager.init_thermal_algorithm(chassis) + except NotImplementedError: + self.log_warning('Thermal manager is not supported on this platform') + except Exception as e: + self.log_error('Caught exception while initializing thermal manager - {}'.format(repr(e))) + sys.exit(ERR_INIT_FAILED) + + def deinit(self): + """ + Deinitializer of ThermalControlDaemon + """ + try: + if self.thermal_manager: + self.thermal_manager.deinitialize() + except Exception as e: + self.log_error('Caught exception while destroying thermal manager - {}'.format(repr(e))) + + self.thermal_monitor.task_stop() + + # Override signal handler from DaemonBase def signal_handler(self, sig, frame): """ Signal handler @@ -738,77 +782,67 @@ class ThermalControlDaemon(daemon_base.DaemonBase): :param frame: not used :return: """ - if sig == signal.SIGHUP: - self.log_info("Caught SIGHUP - ignoring...") - elif sig == signal.SIGINT or sig == signal.SIGTERM: - self.log_info("Caught signal {} - exiting...".format(sig)) - self.exit_code = sig + 128 + 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(SIGNALS_TO_NAMES_DICT[sig])) + 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(SIGNALS_TO_NAMES_DICT[sig])) else: - self.log_warning("Caught unhandled signal '" + sig + "'") + self.log_warning("Caught unhandled signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig])) + # Main daemon logic def run(self): """ Run main logical of this daemon :return: """ - self.log_info("Starting up...") - - import sonic_platform.platform - chassis = sonic_platform.platform.Platform().get_chassis() - - thermal_monitor = ThermalMonitor(chassis) - thermal_monitor.task_run() + if self.stop_event.wait(self.wait_time): + # We received a fatal signal + return False - thermal_manager = None + begin = time.time() try: - thermal_manager = chassis.get_thermal_manager() - if thermal_manager: - thermal_manager.initialize() - thermal_manager.load(ThermalControlDaemon.POLICY_FILE) - thermal_manager.init_thermal_algorithm(chassis) - except NotImplementedError: - self.log_warning('Thermal manager is not supported on this platform.') + if self.thermal_manager: + self.thermal_manager.run_policy(chassis) except Exception as e: - self.log_error('Caught exception while initializing thermal manager - {}'.format(repr(e))) - - wait_time = ThermalControlDaemon.INTERVAL - while not self.stop_event.wait(wait_time): - begin = time.time() - try: - if thermal_manager: - thermal_manager.run_policy(chassis) - except Exception as e: - self.log_error('Caught exception while running thermal policy - {}'.format(repr(e))) - elapsed = time.time() - begin - if elapsed < ThermalControlDaemon.INTERVAL: - wait_time = ThermalControlDaemon.INTERVAL - elapsed - else: - wait_time = ThermalControlDaemon.FAST_START_INTERVAL - - if elapsed > ThermalControlDaemon.RUN_POLICY_WARN_THRESHOLD_SECS: - self.log_warning('Thermal policy execution takes {} seconds, ' - 'there might be performance risk'.format(elapsed)) + self.log_error('Caught exception while running thermal policy - {}'.format(repr(e))) - try: - if thermal_manager: - thermal_manager.deinitialize() - except Exception as e: - self.log_error('Caught exception while destroy thermal manager - {}'.format(repr(e))) + elapsed = time.time() - begin + if elapsed < self.INTERVAL: + self.wait_time = self.INTERVAL - elapsed + else: + self.wait_time = self.FAST_START_INTERVAL - thermal_monitor.task_stop() + if elapsed > self.RUN_POLICY_WARN_THRESHOLD_SECS: + self.log_warning('Thermal policy execution took {} seconds, ' + 'there might be performance risk'.format(elapsed)) - self.log_info("Shutdown with exit code {}...".format(self.exit_code)) - exit(self.exit_code) + return True # # Main ========================================================================= # def main(): - thermal_control = ThermalControlDaemon(SYSLOG_IDENTIFIER) - thermal_control.run() + thermal_control = ThermalControlDaemon() + + thermal_control.log_info("Starting up...") + + while thermal_control.run(): + pass + + thermal_control.log_info("Shutting down with exit code {}...".format(exit_code)) + + thermal_control.deinit() + + return exit_code if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/sonic-thermalctld/setup.py b/sonic-thermalctld/setup.py index 6955ecdd8286..5e9bc083fea1 100644 --- a/sonic-thermalctld/setup.py +++ b/sonic-thermalctld/setup.py @@ -23,7 +23,8 @@ tests_require=[ 'mock>=2.0.0; python_version < "3.3"', 'pytest', - 'pytest-cov' + 'pytest-cov', + 'sonic-platform-common' ], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/sonic-thermalctld/tests/mock_platform.py b/sonic-thermalctld/tests/mock_platform.py index 168de9754b0f..117dcafcffbb 100644 --- a/sonic-thermalctld/tests/mock_platform.py +++ b/sonic-thermalctld/tests/mock_platform.py @@ -1,262 +1,432 @@ -class MockDevice: +from sonic_platform_base import chassis_base +from sonic_platform_base import fan_base +from sonic_platform_base import fan_drawer_base +from sonic_platform_base import module_base +from sonic_platform_base import psu_base +from sonic_platform_base import sfp_base +from sonic_platform_base import thermal_base +from sonic_platform_base.sonic_thermal_control import thermal_manager_base + + +class MockFan(fan_base.FanBase): def __init__(self): - self.name = None - self.presence = True - self.model = 'FAN Model' - self.serial = 'Fan Serial' + super(MockFan, self).__init__() + self._name = None + self._presence = True + self._model = 'Fan Model' + self._serial = 'Fan Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = True + + self._speed = 20 + self._speed_tolerance = 20 + self._target_speed = 20 + self._direction = self.FAN_DIRECTION_INTAKE + self._status_led = self.STATUS_LED_COLOR_RED + + def get_speed(self): + return self._speed + + def get_speed_tolerance(self): + return self._speed_tolerance + + def get_target_speed(self): + return self._target_speed + + def get_direction(self): + return self._direction + + def get_status_led(self): + return self._status_led + + def set_status_led(self, value): + self._status_led = value + + def make_under_speed(self): + self._speed = 1 + self._target_speed = 2 + self._speed_tolerance = 0 + def make_over_speed(self): + self._speed = 2 + self._target_speed = 1 + self._speed_tolerance = 0 + + def make_normal_speed(self): + self._speed = 1 + self._target_speed = 1 + self._speed_tolerance = 0 + + # Methods inherited from DeviceBase class and related setters def get_name(self): - return self.name + return self._name def get_presence(self): - return self.presence + return self._presence + + def set_presence(self, presence): + self._presence = presence def get_model(self): - return self.model + return self._model def get_serial(self): - return self.serial - - def get_position_in_parent(self): - return 1 - - def is_replaceable(self): - return True + return self._serial def get_status(self): - return True + return self._status + def set_status(self, status): + self._status = status -class MockFan(MockDevice): - STATUS_LED_COLOR_RED = 'red' - STATUS_LED_COLOR_GREEN = 'green' + def get_position_in_parent(self): + return self._position_in_parent - def __init__(self): - MockDevice.__init__(self) - self.speed = 20 - self.speed_tolerance = 20 - self.target_speed = 20 - self.status = True - self.direction = 'intake' - self.led_status = 'red' + def is_replaceable(self): + return self._replaceable + +class MockErrorFan(MockFan): def get_speed(self): - return self.speed + raise Exception('Failed to get speed') - def get_speed_tolerance(self): - return self.speed_tolerance - def get_target_speed(self): - return self.target_speed +class MockFanDrawer(fan_drawer_base.FanDrawerBase): + def __init__(self, index): + super(MockFanDrawer, self).__init__() + self._name = 'FanDrawer {}'.format(index) + self._presence = True + self._model = 'Fan Drawer Model' + self._serial = 'Fan Drawer Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = True - def get_status(self): - return self.status + self._status_led = self.STATUS_LED_COLOR_RED - def get_direction(self): - return self.direction + def get_all_fans(self): + return self._fan_list def get_status_led(self): - return self.led_status + return self._status_led def set_status_led(self, value): - self.led_status = value + self._status_led = value - def make_under_speed(self): - self.speed = 1 - self.target_speed = 2 - self.speed_tolerance = 0 + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name - def make_over_speed(self): - self.speed = 2 - self.target_speed = 1 - self.speed_tolerance = 0 + def get_presence(self): + return self._presence - def make_normal_speed(self): - self.speed = 1 - self.target_speed = 1 - self.speed_tolerance = 0 + def set_presence(self, presence): + self._presence = presence + def get_model(self): + return self._model -class MockErrorFan(MockFan): - def get_speed(self): - raise Exception('Fail to get speed') + 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 MockPsu(MockDevice): +class MockPsu(psu_base.PsuBase): def __init__(self): - MockDevice.__init__(self) - self.fan_list = [] + super(MockPsu, self).__init__() + self._name = None + self._presence = True + self._model = 'PSU Model' + self._serial = 'PSU Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = True def get_all_fans(self): - return self.fan_list + return self._fan_list + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name -class MockFanDrawer(MockDevice): - def __init__(self, index): - MockDevice.__init__(self) - self.name = 'FanDrawer {}'.format(index) - self.fan_list = [] - self.led_status = 'red' + def get_presence(self): + return self._presence - def get_name(self): - return self.name + def set_presence(self, presence): + self._presence = presence - def get_all_fans(self): - return self.fan_list + def get_model(self): + return self._model - def get_status_led(self): - return self.led_status + def get_serial(self): + return self._serial - def set_status_led(self, value): - self.led_status = value + 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 MockThermal(MockDevice): - def __init__(self, index=None): - MockDevice.__init__(self) - self.name = None - self.name = 'Thermal {}'.format(index) if index != None else None - self.temperature = 2 - self.minimum_temperature = 1 - self.maximum_temperature = 5 - self.high_threshold = 3 - self.low_threshold = 1 - self.high_critical_threshold = 4 - self.low_critical_threshold = 0 +class MockSfp(sfp_base.SfpBase): + def __init__(self): + super(MockSfp, self).__init__() + self._name = None + self._presence = True + self._model = 'SFP Model' + self._serial = 'SFP Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = True + + # Methods inherited from DeviceBase class and related setters def get_name(self): - return self.name + 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 MockThermal(thermal_base.ThermalBase): + def __init__(self, index=None): + super(MockThermal, self).__init__() + self._name = 'Thermal {}'.format(index) if index != None else None + self._presence = True + self._model = 'Thermal Model' + self._serial = 'Thermal Serial' + self._status = True + self._position_in_parent = 1 + self._replaceable = False + + self._temperature = 2 + self._minimum_temperature = 1 + self._maximum_temperature = 5 + self._high_threshold = 3 + self._low_threshold = 1 + self._high_critical_threshold = 4 + self._low_critical_threshold = 0 def get_temperature(self): - return self.temperature + return self._temperature def get_minimum_recorded(self): - return self.minimum_temperature + return self._minimum_temperature def get_maximum_recorded(self): - return self.maximum_temperature + return self._maximum_temperature def get_high_threshold(self): - return self.high_threshold + return self._high_threshold def get_low_threshold(self): - return self.low_threshold + return self._low_threshold def get_high_critical_threshold(self): - return self.high_critical_threshold + return self._high_critical_threshold def get_low_critical_threshold(self): - return self.low_critical_threshold + return self._low_critical_threshold def make_over_temper(self): - self.high_threshold = 2 - self.temperature = 3 - self.low_threshold = 1 + self._high_threshold = 2 + self._temperature = 3 + self._low_threshold = 1 def make_under_temper(self): - self.high_threshold = 3 - self.temperature = 1 - self.low_threshold = 2 + self._high_threshold = 3 + self._temperature = 1 + self._low_threshold = 2 def make_normal_temper(self): - self.high_threshold = 3 - self.temperature = 2 - self.low_threshold = 1 + self._high_threshold = 3 + self._temperature = 2 + self._low_threshold = 1 + # Methods inherited from DeviceBase class and related setters + def get_name(self): + return self._name -class MockErrorThermal(MockThermal): - def get_temperature(self): - raise Exception('Fail to get temperature') + def get_presence(self): + return self._presence + def set_presence(self, presence): + self._presence = presence -class MockChassis: - def __init__(self): - self.fan_list = [] - self.psu_list = [] - self.thermal_list = [] - self.fan_drawer_list = [] - self.sfp_list = [] - self.is_chassis_system = False + def get_model(self): + return self._model - def get_all_fans(self): - return self.fan_list + def get_serial(self): + return self._serial + + def get_status(self): + return self._status - def get_all_psus(self): - return self.psu_list + def set_status(self, status): + self._status = status - def get_all_thermals(self): - return self.thermal_list + def get_position_in_parent(self): + return self._position_in_parent - def get_all_fan_drawers(self): - return self.fan_drawer_list + def is_replaceable(self): + return self._replaceable - def get_all_sfps(self): - return self.sfp_list - def get_num_thermals(self): - return len(self.thermal_list) +class MockErrorThermal(MockThermal): + def get_temperature(self): + raise Exception('Failed to get temperature') - def make_absence_fan(self): + +class MockThermalManager(thermal_manager_base.ThermalManagerBase): + def __init__(self): + super(MockThermalManager, self).__init__() + + +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._is_chassis_system = False + self._my_slot = module_base.ModuleBase.MODULE_INVALID_SLOT + self._thermal_manager = MockThermalManager() + + def make_absent_fan(self): fan = MockFan() - fan.presence = False - fan_drawer = MockFanDrawer(len(self.fan_drawer_list)) - fan_drawer.fan_list.append(fan) - self.fan_list.append(fan) - self.fan_drawer_list.append(fan_drawer) + fan.set_presence(False) + fan_drawer = MockFanDrawer(len(self._fan_drawer_list)) + fan_drawer._fan_list.append(fan) + self._fan_list.append(fan) + self._fan_drawer_list.append(fan_drawer) - def make_fault_fan(self): + def make_faulty_fan(self): fan = MockFan() - fan.status = False - fan_drawer = MockFanDrawer(len(self.fan_drawer_list)) - fan_drawer.fan_list.append(fan) - self.fan_list.append(fan) - self.fan_drawer_list.append(fan_drawer) + fan.set_status(False) + fan_drawer = MockFanDrawer(len(self._fan_drawer_list)) + fan_drawer._fan_list.append(fan) + self._fan_list.append(fan) + self._fan_drawer_list.append(fan_drawer) def make_under_speed_fan(self): fan = MockFan() fan.make_under_speed() - fan_drawer = MockFanDrawer(len(self.fan_drawer_list)) - fan_drawer.fan_list.append(fan) - self.fan_list.append(fan) - self.fan_drawer_list.append(fan_drawer) + fan_drawer = MockFanDrawer(len(self._fan_drawer_list)) + fan_drawer._fan_list.append(fan) + self._fan_list.append(fan) + self._fan_drawer_list.append(fan_drawer) def make_over_speed_fan(self): fan = MockFan() fan.make_over_speed() - fan_drawer = MockFanDrawer(len(self.fan_drawer_list)) - fan_drawer.fan_list.append(fan) - self.fan_list.append(fan) - self.fan_drawer_list.append(fan_drawer) + fan_drawer = MockFanDrawer(len(self._fan_drawer_list)) + fan_drawer._fan_list.append(fan) + self._fan_list.append(fan) + self._fan_drawer_list.append(fan_drawer) def make_error_fan(self): fan = MockErrorFan() - fan_drawer = MockFanDrawer(len(self.fan_drawer_list)) - fan_drawer.fan_list.append(fan) - self.fan_list.append(fan) - self.fan_drawer_list.append(fan_drawer) + fan_drawer = MockFanDrawer(len(self._fan_drawer_list)) + fan_drawer._fan_list.append(fan) + self._fan_list.append(fan) + self._fan_drawer_list.append(fan_drawer) def make_over_temper_thermal(self): thermal = MockThermal() thermal.make_over_temper() - self.thermal_list.append(thermal) + self._thermal_list.append(thermal) def make_under_temper_thermal(self): thermal = MockThermal() thermal.make_under_temper() - self.thermal_list.append(thermal) + self._thermal_list.append(thermal) def make_error_thermal(self): thermal = MockErrorThermal() - self.thermal_list.append(thermal) + self._thermal_list.append(thermal) def is_modular_chassis(self): - return self.is_chassis_system + return self._is_chassis_system def set_modular_chassis(self, is_true): - self.is_chassis_system = is_true + self._is_chassis_system = is_true def set_my_slot(self, my_slot): - self.my_slot = my_slot + self._my_slot = my_slot def get_my_slot(self): - return self.my_slot + return self._my_slot + + def get_thermal_manager(self): + return self._thermal_manager + + # 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 diff --git a/sonic-thermalctld/tests/mock_swsscommon.py b/sonic-thermalctld/tests/mock_swsscommon.py index c46c8a70a64d..ade0d354181a 100644 --- a/sonic-thermalctld/tests/mock_swsscommon.py +++ b/sonic-thermalctld/tests/mock_swsscommon.py @@ -1,3 +1,7 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + STATE_DB = '' CHASSIS_STATE_DB = '' diff --git a/sonic-thermalctld/tests/mocked_libs/sonic_platform/__init__.py b/sonic-thermalctld/tests/mocked_libs/sonic_platform/__init__.py new file mode 100644 index 000000000000..e491d5b52166 --- /dev/null +++ b/sonic-thermalctld/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-thermalctld/tests/mocked_libs/sonic_platform/chassis.py b/sonic-thermalctld/tests/mocked_libs/sonic_platform/chassis.py new file mode 100644 index 000000000000..49a939987783 --- /dev/null +++ b/sonic-thermalctld/tests/mocked_libs/sonic_platform/chassis.py @@ -0,0 +1,25 @@ +""" + Mock implementation of sonic_platform package for unit testing +""" + +# TODO: Clean this up once we no longer need to support Python 2 +import sys +if sys.version_info.major == 3: + from unittest import mock +else: + import mock + +from sonic_platform_base.chassis_base import ChassisBase + + +class Chassis(ChassisBase): + def __init__(self): + ChassisBase.__init__(self) + self._eeprom = mock.MagicMock() + self._thermal_manager = mock.MagicMock() + + def get_eeprom(self): + return self._eeprom + + def get_thermal_manager(self): + return self._thermal_manager diff --git a/sonic-thermalctld/tests/mocked_libs/sonic_platform/platform.py b/sonic-thermalctld/tests/mocked_libs/sonic_platform/platform.py new file mode 100644 index 000000000000..e1e7735f38f1 --- /dev/null +++ b/sonic-thermalctld/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-thermalctld/tests/mocked_libs/swsscommon/__init__.py b/sonic-thermalctld/tests/mocked_libs/swsscommon/__init__.py new file mode 100644 index 000000000000..012af621e5f0 --- /dev/null +++ b/sonic-thermalctld/tests/mocked_libs/swsscommon/__init__.py @@ -0,0 +1,5 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + +from . import swsscommon diff --git a/sonic-thermalctld/tests/mocked_libs/swsscommon/swsscommon.py b/sonic-thermalctld/tests/mocked_libs/swsscommon/swsscommon.py new file mode 100644 index 000000000000..6947a8601819 --- /dev/null +++ b/sonic-thermalctld/tests/mocked_libs/swsscommon/swsscommon.py @@ -0,0 +1,54 @@ +''' + Mock implementation of swsscommon package for unit testing +''' + +STATE_DB = '' + + +class Table: + def __init__(self, db, table_name): + self.table_name = table_name + self.mock_dict = {} + + 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)) + + +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-thermalctld/tests/test_thermalctld.py b/sonic-thermalctld/tests/test_thermalctld.py index 3f203ab870fb..d2f647384f13 100644 --- a/sonic-thermalctld/tests/test_thermalctld.py +++ b/sonic-thermalctld/tests/test_thermalctld.py @@ -1,236 +1,392 @@ import os import sys -from imp import load_source +from imp import load_source # TODO: Replace with importlib once we no longer need to support Python 2 # TODO: Clean this up once we no longer need to support Python 2 if sys.version_info.major == 3: - from unittest.mock import Mock, MagicMock, patch + from unittest import mock else: - from mock import Mock, MagicMock, patch + import mock + +import pytest from sonic_py_common import daemon_base -from .mock_platform import MockChassis, MockFan, MockThermal +from .mock_platform import MockChassis, MockFan, MockPsu, MockSfp, MockThermal + +daemon_base.db_connect = mock.MagicMock() -SYSLOG_IDENTIFIER = 'thermalctld_test' -NOT_AVAILABLE = 'N/A' +tests_path = os.path.dirname(os.path.abspath(__file__)) -daemon_base.db_connect = MagicMock() +# Add mocked_libs path so that the file under test can load mocked modules from there +mocked_libs_path = os.path.join(tests_path, 'mocked_libs') +sys.path.insert(0, mocked_libs_path) -test_path = os.path.dirname(os.path.abspath(__file__)) -modules_path = os.path.dirname(test_path) -scripts_path = os.path.join(modules_path, "scripts") +# Add path to the file under test so that we can load it +modules_path = os.path.dirname(tests_path) +scripts_path = os.path.join(modules_path, 'scripts') sys.path.insert(0, modules_path) -os.environ["THERMALCTLD_UNIT_TESTING"] = "1" -load_source('thermalctld', scripts_path + '/thermalctld') -from thermalctld import * +load_source('thermalctld', os.path.join(scripts_path, 'thermalctld')) +import thermalctld TEMPER_INFO_TABLE_NAME = 'TEMPERATURE_INFO' -def setup_function(): - FanStatus.log_notice = MagicMock() - FanStatus.log_warning = MagicMock() - FanUpdater.log_notice = MagicMock() - FanUpdater.log_warning = MagicMock() - TemperatureStatus.log_notice = MagicMock() - TemperatureStatus.log_warning = MagicMock() - TemperatureUpdater.log_notice = MagicMock() - TemperatureUpdater.log_warning = MagicMock() - - -def teardown_function(): - FanStatus.log_notice.reset() - FanStatus.log_warning.reset() - FanUpdater.log_notice.reset() - FanUpdater.log_notice.reset() - TemperatureStatus.log_notice.reset() - TemperatureStatus.log_warning.reset() - TemperatureUpdater.log_warning.reset() - TemperatureUpdater.log_warning.reset() - - -def test_fanstatus_set_presence(): - fan_status = FanStatus(SYSLOG_IDENTIFIER) - ret = fan_status.set_presence(True) - assert fan_status.presence - assert not ret - - ret = fan_status.set_presence(False) - assert not fan_status.presence - assert ret - - -def test_fanstatus_set_under_speed(): - fan_status = FanStatus(SYSLOG_IDENTIFIER) - ret = fan_status.set_under_speed(NOT_AVAILABLE, NOT_AVAILABLE, NOT_AVAILABLE) - assert not ret - - ret = fan_status.set_under_speed(NOT_AVAILABLE, NOT_AVAILABLE, 0) - assert not ret - - ret = fan_status.set_under_speed(NOT_AVAILABLE, 0, 0) - assert not ret - - ret = fan_status.set_under_speed(0, 0, 0) - assert not ret - - ret = fan_status.set_under_speed(80, 100, 19) - assert ret - assert fan_status.under_speed - assert not fan_status.is_ok() - - ret = fan_status.set_under_speed(81, 100, 19) - assert ret - assert not fan_status.under_speed - assert fan_status.is_ok() - - -def test_fanstatus_set_over_speed(): - fan_status = FanStatus(SYSLOG_IDENTIFIER) - ret = fan_status.set_over_speed(NOT_AVAILABLE, NOT_AVAILABLE, NOT_AVAILABLE) - assert not ret - - ret = fan_status.set_over_speed(NOT_AVAILABLE, NOT_AVAILABLE, 0) - assert not ret - - ret = fan_status.set_over_speed(NOT_AVAILABLE, 0, 0) - assert not ret - - ret = fan_status.set_over_speed(0, 0, 0) - assert not ret - - ret = fan_status.set_over_speed(120, 100, 19) - assert ret - assert fan_status.over_speed - assert not fan_status.is_ok() - - ret = fan_status.set_over_speed(120, 100, 21) - assert ret - assert not fan_status.over_speed - assert fan_status.is_ok() - - -def test_fanupdater_fan_absence(): - chassis = MockChassis() - chassis.make_absence_fan() - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - fan_updater.update() - fan_list = chassis.get_all_fans() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED - fan_updater.log_warning.assert_called() - - fan_list[0].presence = True - fan_updater.update() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN - fan_updater.log_notice.assert_called() - - -def test_fanupdater_fan_fault(): - chassis = MockChassis() - chassis.make_fault_fan() - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - fan_updater.update() - fan_list = chassis.get_all_fans() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED - fan_updater.log_warning.assert_called() - - fan_list[0].status = True - fan_updater.update() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN - fan_updater.log_notice.assert_called() - - -def test_fanupdater_fan_under_speed(): - chassis = MockChassis() - chassis.make_under_speed_fan() - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - fan_updater.update() - fan_list = chassis.get_all_fans() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED - fan_updater.log_warning.assert_called_once() - - fan_list[0].make_normal_speed() - fan_updater.update() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN - fan_updater.log_notice.assert_called_once() - - -def test_fanupdater_fan_over_speed(): - chassis = MockChassis() - chassis.make_over_speed_fan() - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - fan_updater.update() - fan_list = chassis.get_all_fans() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED - fan_updater.log_warning.assert_called_once() - - fan_list[0].make_normal_speed() - fan_updater.update() - assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN - fan_updater.log_notice.assert_called_once() +@pytest.fixture(scope='function', autouse=True) +def configure_mocks(): + thermalctld.FanStatus.log_notice = mock.MagicMock() + thermalctld.FanStatus.log_warning = mock.MagicMock() + thermalctld.FanUpdater.log_notice = mock.MagicMock() + thermalctld.FanUpdater.log_warning = mock.MagicMock() + thermalctld.TemperatureStatus.log_notice = mock.MagicMock() + thermalctld.TemperatureStatus.log_warning = mock.MagicMock() + thermalctld.TemperatureUpdater.log_notice = mock.MagicMock() + thermalctld.TemperatureUpdater.log_warning = mock.MagicMock() + + yield + + thermalctld.FanStatus.log_notice.reset() + thermalctld.FanStatus.log_warning.reset() + thermalctld.FanUpdater.log_notice.reset() + thermalctld.FanUpdater.log_notice.reset() + thermalctld.TemperatureStatus.log_notice.reset() + thermalctld.TemperatureStatus.log_warning.reset() + thermalctld.TemperatureUpdater.log_warning.reset() + thermalctld.TemperatureUpdater.log_warning.reset() + + +class TestFanStatus(object): + """ + Test cases to cover functionality in FanStatus class + """ + def test_check_speed_value_available(self): + fan_status = thermalctld.FanStatus() + + ret = fan_status._check_speed_value_available(30, 32, 5, True) + assert ret == True + assert fan_status.log_warning.call_count == 0 + + ret = fan_status._check_speed_value_available(thermalctld.NOT_AVAILABLE, 32, 105, True) + assert ret == False + assert fan_status.log_warning.call_count == 1 + fan_status.log_warning.assert_called_with('Invalid tolerance value: 105') + + # Reset + fan_status.log_warning.reset_mock() + + ret = fan_status._check_speed_value_available(thermalctld.NOT_AVAILABLE, 32, 5, False) + assert ret == False + assert fan_status.log_warning.call_count == 0 + + ret = fan_status._check_speed_value_available(thermalctld.NOT_AVAILABLE, 32, 5, True) + assert ret == False + assert fan_status.log_warning.call_count == 1 + fan_status.log_warning.assert_called_with('Fan speed or target_speed or tolerance became unavailable, speed=N/A, target_speed=32, tolerance=5') + + def test_set_presence(self): + fan_status = thermalctld.FanStatus() + ret = fan_status.set_presence(True) + assert fan_status.presence + assert not ret + + ret = fan_status.set_presence(False) + assert not fan_status.presence + assert ret + + def test_set_under_speed(self): + fan_status = thermalctld.FanStatus() + ret = fan_status.set_under_speed(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE) + assert not ret + + ret = fan_status.set_under_speed(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE, 0) + assert not ret + + ret = fan_status.set_under_speed(thermalctld.NOT_AVAILABLE, 0, 0) + assert not ret + + ret = fan_status.set_under_speed(0, 0, 0) + assert not ret + + ret = fan_status.set_under_speed(80, 100, 19) + assert ret + assert fan_status.under_speed + assert not fan_status.is_ok() + + ret = fan_status.set_under_speed(81, 100, 19) + assert ret + assert not fan_status.under_speed + assert fan_status.is_ok() + + def test_set_over_speed(self): + fan_status = thermalctld.FanStatus() + ret = fan_status.set_over_speed(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE) + assert not ret + + ret = fan_status.set_over_speed(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE, 0) + assert not ret + + ret = fan_status.set_over_speed(thermalctld.NOT_AVAILABLE, 0, 0) + assert not ret + + ret = fan_status.set_over_speed(0, 0, 0) + assert not ret + + ret = fan_status.set_over_speed(120, 100, 19) + assert ret + assert fan_status.over_speed + assert not fan_status.is_ok() + + ret = fan_status.set_over_speed(120, 100, 21) + assert ret + assert not fan_status.over_speed + assert fan_status.is_ok() + + +class TestFanUpdater(object): + """ + Test cases to cover functionality in FanUpdater class + """ + def test_deinit(self): + fan_updater = thermalctld.FanUpdater(MockChassis()) + fan_updater.fan_status_dict = {'key1': 'value1', 'key2': 'value2'} + fan_updater.table._del = mock.MagicMock() + + fan_updater.deinit() + assert fan_updater.table._del.call_count == 2 + expected_calls = [mock.call('key1'), mock.call('key2')] + fan_updater.table._del.assert_has_calls(expected_calls, any_order=True) + + @mock.patch('thermalctld.try_get', mock.MagicMock(return_value=thermalctld.NOT_AVAILABLE)) + @mock.patch('thermalctld.update_entity_info', mock.MagicMock()) + def test_refresh_fan_drawer_status_fan_drawer_get_name_not_impl(self): + # Test case where fan_drawer.get_name is not implemented + fan_updater = thermalctld.FanUpdater(MockChassis()) + mock_fan_drawer = mock.MagicMock() + fan_updater._refresh_fan_drawer_status(mock_fan_drawer, 1) + assert thermalctld.update_entity_info.call_count == 0 + + # TODO: Add a test case for _refresh_fan_drawer_status with a good fan drawer + + def test_update_fan_with_exception(self): + chassis = MockChassis() + chassis.make_error_fan() + fan = MockFan() + fan.make_over_speed() + chassis.get_all_fans().append(fan) + + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + assert fan.get_status_led() == MockFan.STATUS_LED_COLOR_RED + assert fan_updater.log_warning.call_count == 1 + + # TODO: Clean this up once we no longer need to support Python 2 + if sys.version_info.major == 3: + fan_updater.log_warning.assert_called_with("Failed to update fan status - Exception('Failed to get speed')") + else: + fan_updater.log_warning.assert_called_with("Failed to update fan status - Exception('Failed to get speed',)") + + def test_set_fan_led_exception(self): + fan_status = thermalctld.FanStatus() + mock_fan_drawer = mock.MagicMock() + mock_fan = MockFan() + mock_fan.set_status_led = mock.MagicMock(side_effect=NotImplementedError) + + fan_updater = thermalctld.FanUpdater(MockChassis()) + fan_updater._set_fan_led(mock_fan_drawer, mock_fan, 'Test Fan', fan_status) + assert fan_updater.log_warning.call_count == 1 + fan_updater.log_warning.assert_called_with('Failed to set status LED for fan Test Fan, set_status_led not implemented') + + def test_fan_absent(self): + chassis = MockChassis() + chassis.make_absent_fan() + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + assert fan_updater.log_warning.call_count == 2 + expected_calls = [ + mock.call('Fan removed warning: FanDrawer 0 fan 1 was removed from the system, potential overheat hazard'), + mock.call('Insufficient number of working fans warning: 1 fan is not working') + ] + assert fan_updater.log_warning.mock_calls == expected_calls + + fan_list[0].set_presence(True) + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + assert fan_updater.log_notice.call_count == 2 + expected_calls = [ + mock.call('Fan removed warning cleared: FanDrawer 0 fan 1 was inserted'), + mock.call('Insufficient number of working fans warning cleared: all fans are back to normal') + ] + assert fan_updater.log_notice.mock_calls == expected_calls + + def test_fan_faulty(self): + chassis = MockChassis() + chassis.make_faulty_fan() + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + assert fan_updater.log_warning.call_count == 2 + expected_calls = [ + mock.call('Fan fault warning: FanDrawer 0 fan 1 is broken'), + mock.call('Insufficient number of working fans warning: 1 fan is not working') + ] + assert fan_updater.log_warning.mock_calls == expected_calls + + fan_list[0].set_status(True) + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + assert fan_updater.log_notice.call_count == 2 + expected_calls = [ + mock.call('Fan fault warning cleared: FanDrawer 0 fan 1 is back to normal'), + mock.call('Insufficient number of working fans warning cleared: all fans are back to normal') + ] + assert fan_updater.log_notice.mock_calls == expected_calls + + def test_fan_under_speed(self): + chassis = MockChassis() + chassis.make_under_speed_fan() + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + assert fan_updater.log_warning.call_count == 1 + fan_updater.log_warning.assert_called_with('Fan low speed warning: FanDrawer 0 fan 1 current speed=1, target speed=2, tolerance=0') + + fan_list[0].make_normal_speed() + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + assert fan_updater.log_notice.call_count == 1 + fan_updater.log_notice.assert_called_with('Fan low speed warning cleared: FanDrawer 0 fan 1 speed is back to normal') + + def test_fan_over_speed(self): + chassis = MockChassis() + chassis.make_over_speed_fan() + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + fan_list = chassis.get_all_fans() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_RED + assert fan_updater.log_warning.call_count == 1 + fan_updater.log_warning.assert_called_with('Fan high speed warning: FanDrawer 0 fan 1 target speed=1, current speed=2, tolerance=0') + + fan_list[0].make_normal_speed() + fan_updater.update() + assert fan_list[0].get_status_led() == MockFan.STATUS_LED_COLOR_GREEN + assert fan_updater.log_notice.call_count == 1 + fan_updater.log_notice.assert_called_with('Fan high speed warning cleared: FanDrawer 0 fan 1 speed is back to normal') + + def test_update_psu_fans(self): + chassis = MockChassis() + psu = MockPsu() + mock_fan = MockFan() + psu._fan_list.append(mock_fan) + chassis._psu_list.append(psu) + fan_updater = thermalctld.FanUpdater(chassis) + fan_updater.update() + assert fan_updater.log_warning.call_count == 0 + + fan_updater._refresh_fan_status = mock.MagicMock(side_effect=Exception("Test message")) + fan_updater.update() + assert fan_updater.log_warning.call_count == 1 + + # TODO: Clean this up once we no longer need to support Python 2 + if sys.version_info.major == 3: + fan_updater.log_warning.assert_called_with("Failed to update PSU fan status - Exception('Test message')") + else: + fan_updater.log_warning.assert_called_with("Failed to update PSU fan status - Exception('Test message',)") + + +class TestThermalMonitor(object): + """ + Test cases to cover functionality in ThermalMonitor class + """ + def test_main(self): + mock_chassis = MockChassis() + thermal_monitor = thermalctld.ThermalMonitor(mock_chassis) + thermal_monitor.fan_updater.update = mock.MagicMock() + thermal_monitor.temperature_updater.update = mock.MagicMock() + + thermal_monitor.main() + assert thermal_monitor.fan_updater.update.call_count == 1 + assert thermal_monitor.temperature_updater.update.call_count == 1 def test_insufficient_fan_number(): - fan_status1 = FanStatus(SYSLOG_IDENTIFIER) - fan_status2 = FanStatus(SYSLOG_IDENTIFIER) + fan_status1 = thermalctld.FanStatus() + fan_status2 = thermalctld.FanStatus() + fan_status1.set_presence(False) fan_status2.set_fault_status(False) - assert FanStatus.get_bad_fan_count() == 2 - FanStatus.reset_fan_counter() - assert FanStatus.get_bad_fan_count() == 0 + assert thermalctld.FanStatus.get_bad_fan_count() == 2 + assert fan_status1.get_bad_fan_count() == 2 + assert fan_status2.get_bad_fan_count() == 2 + + thermalctld.FanStatus.reset_fan_counter() + assert thermalctld.FanStatus.get_bad_fan_count() == 0 + assert fan_status1.get_bad_fan_count() == 0 + assert fan_status2.get_bad_fan_count() == 0 chassis = MockChassis() - chassis.make_absence_fan() - chassis.make_fault_fan() - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) + chassis.make_absent_fan() + chassis.make_faulty_fan() + fan_updater = thermalctld.FanUpdater(chassis) fan_updater.update() assert fan_updater.log_warning.call_count == 3 - fan_updater.log_warning.assert_called_with('Insufficient number of working fans warning: 2 fans are not working.') + expected_calls = [ + mock.call('Fan removed warning: FanDrawer 0 fan 1 was removed from the system, potential overheat hazard'), + mock.call('Fan fault warning: FanDrawer 1 fan 1 is broken'), + mock.call('Insufficient number of working fans warning: 2 fans are not working') + ] + assert fan_updater.log_warning.mock_calls == expected_calls fan_list = chassis.get_all_fans() - fan_list[0].presence = True + fan_list[0].set_presence(True) fan_updater.update() assert fan_updater.log_notice.call_count == 1 - fan_updater.log_warning.assert_called_with('Insufficient number of working fans warning: 1 fans are not working.') + fan_updater.log_warning.assert_called_with('Insufficient number of working fans warning: 1 fan is not working') - fan_list[1].status = True + fan_list[1].set_status(True) fan_updater.update() assert fan_updater.log_notice.call_count == 3 - fan_updater.log_notice.assert_called_with( - 'Insufficient number of working fans warning cleared: all fans are back to normal.') + expected_calls = [ + mock.call('Fan removed warning cleared: FanDrawer 0 fan 1 was inserted'), + mock.call('Fan fault warning cleared: FanDrawer 1 fan 1 is back to normal'), + mock.call('Insufficient number of working fans warning cleared: all fans are back to normal') + ] + assert fan_updater.log_notice.mock_calls == expected_calls def test_temperature_status_set_over_temper(): - temperatue_status = TemperatureStatus(SYSLOG_IDENTIFIER) - ret = temperatue_status.set_over_temperature(NOT_AVAILABLE, NOT_AVAILABLE) + temperature_status = thermalctld.TemperatureStatus() + ret = temperature_status.set_over_temperature(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE) assert not ret - ret = temperatue_status.set_over_temperature(NOT_AVAILABLE, 0) + ret = temperature_status.set_over_temperature(thermalctld.NOT_AVAILABLE, 0) assert not ret - ret = temperatue_status.set_over_temperature(0, NOT_AVAILABLE) + ret = temperature_status.set_over_temperature(0, thermalctld.NOT_AVAILABLE) assert not ret - ret = temperatue_status.set_over_temperature(2, 1) + ret = temperature_status.set_over_temperature(2, 1) assert ret - assert temperatue_status.over_temperature + assert temperature_status.over_temperature - ret = temperatue_status.set_over_temperature(1, 2) + ret = temperature_status.set_over_temperature(1, 2) assert ret - assert not temperatue_status.over_temperature + assert not temperature_status.over_temperature def test_temperstatus_set_under_temper(): - temperature_status = TemperatureStatus(SYSLOG_IDENTIFIER) - ret = temperature_status.set_under_temperature(NOT_AVAILABLE, NOT_AVAILABLE) + temperature_status = thermalctld.TemperatureStatus() + ret = temperature_status.set_under_temperature(thermalctld.NOT_AVAILABLE, thermalctld.NOT_AVAILABLE) assert not ret - ret = temperature_status.set_under_temperature(NOT_AVAILABLE, 0) + ret = temperature_status.set_under_temperature(thermalctld.NOT_AVAILABLE, 0) assert not ret - ret = temperature_status.set_under_temperature(0, NOT_AVAILABLE) + ret = temperature_status.set_under_temperature(0, thermalctld.NOT_AVAILABLE) assert not ret ret = temperature_status.set_under_temperature(1, 2) @@ -242,76 +398,145 @@ def test_temperstatus_set_under_temper(): assert not temperature_status.under_temperature -def test_temperupdater_over_temper(): - chassis = MockChassis() - chassis.make_over_temper_thermal() - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) - temperature_updater.update() - thermal_list = chassis.get_all_thermals() - temperature_updater.log_warning.assert_called_once() - - thermal_list[0].make_normal_temper() - temperature_updater.update() - temperature_updater.log_notice.assert_called_once() - - -def test_temperupdater_under_temper(): - chassis = MockChassis() - chassis.make_under_temper_thermal() - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) - temperature_updater.update() - thermal_list = chassis.get_all_thermals() - temperature_updater.log_warning.assert_called_once() - - thermal_list[0].make_normal_temper() - temperature_updater.update() - temperature_updater.log_notice.assert_called_once() - - -def test_update_fan_with_exception(): - chassis = MockChassis() - chassis.make_error_fan() - fan = MockFan() - fan.make_over_speed() - chassis.get_all_fans().append(fan) - - fan_updater = FanUpdater(SYSLOG_IDENTIFIER, chassis) - fan_updater.update() - assert fan.get_status_led() == MockFan.STATUS_LED_COLOR_RED - fan_updater.log_warning.assert_called() - - -def test_update_thermal_with_exception(): - chassis = MockChassis() - chassis.make_error_thermal() - thermal = MockThermal() - thermal.make_over_temper() - chassis.get_all_thermals().append(thermal) - - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) - temperature_updater.update() - temperature_updater.log_warning.assert_called() - -# Modular chassis related tests +def test_temperature_status_set_not_available(): + THERMAL_NAME = 'Chassis 1 Thermal 1' + temperature_status = thermalctld.TemperatureStatus() + temperature_status.temperature = 20.0 + + temperature_status.set_temperature(THERMAL_NAME, thermalctld.NOT_AVAILABLE) + assert temperature_status.temperature is None + assert temperature_status.log_warning.call_count == 1 + temperature_status.log_warning.assert_called_with('Temperature of {} became unavailable'.format(THERMAL_NAME)) + + +class TestTemperatureUpdater(object): + """ + Test cases to cover functionality in TemperatureUpdater class + """ + def test_deinit(self): + chassis = MockChassis() + temp_updater = thermalctld.TemperatureUpdater(chassis) + temp_updater.temperature_status_dict = {'key1': 'value1', 'key2': 'value2'} + temp_updater.table._del = mock.MagicMock() + + temp_updater.deinit() + assert temp_updater.table._del.call_count == 2 + expected_calls = [mock.call('key1'), mock.call('key2')] + temp_updater.table._del.assert_has_calls(expected_calls, any_order=True) + + def test_over_temper(self): + chassis = MockChassis() + chassis.make_over_temper_thermal() + temperature_updater = thermalctld.TemperatureUpdater(chassis) + temperature_updater.update() + thermal_list = chassis.get_all_thermals() + assert temperature_updater.log_warning.call_count == 1 + temperature_updater.log_warning.assert_called_with('High temperature warning: chassis 1 Thermal 1 current temperature 3C, high threshold 2C') + + thermal_list[0].make_normal_temper() + temperature_updater.update() + assert temperature_updater.log_notice.call_count == 1 + temperature_updater.log_notice.assert_called_with('High temperature warning cleared: chassis 1 Thermal 1 temperature restored to 2C, high threshold 3C') + + def test_under_temper(self): + chassis = MockChassis() + chassis.make_under_temper_thermal() + temperature_updater = thermalctld.TemperatureUpdater(chassis) + temperature_updater.update() + thermal_list = chassis.get_all_thermals() + assert temperature_updater.log_warning.call_count == 1 + temperature_updater.log_warning.assert_called_with('Low temperature warning: chassis 1 Thermal 1 current temperature 1C, low threshold 2C') + + thermal_list[0].make_normal_temper() + temperature_updater.update() + assert temperature_updater.log_notice.call_count == 1 + temperature_updater.log_notice.assert_called_with('Low temperature warning cleared: chassis 1 Thermal 1 temperature restored to 2C, low threshold 1C') + + def test_update_psu_thermals(self): + chassis = MockChassis() + psu = MockPsu() + mock_thermal = MockThermal() + psu._thermal_list.append(mock_thermal) + chassis._psu_list.append(psu) + temperature_updater = thermalctld.TemperatureUpdater(chassis) + temperature_updater.update() + assert temperature_updater.log_warning.call_count == 0 + + temperature_updater._refresh_temperature_status = mock.MagicMock(side_effect=Exception("Test message")) + temperature_updater.update() + assert temperature_updater.log_warning.call_count == 1 + + # TODO: Clean this up once we no longer need to support Python 2 + if sys.version_info.major == 3: + temperature_updater.log_warning.assert_called_with("Failed to update thermal status - Exception('Test message')") + else: + temperature_updater.log_warning.assert_called_with("Failed to update thermal status - Exception('Test message',)") + + def test_update_sfp_thermals(self): + chassis = MockChassis() + sfp = MockSfp() + mock_thermal = MockThermal() + sfp._thermal_list.append(mock_thermal) + chassis._sfp_list.append(sfp) + temperature_updater = thermalctld.TemperatureUpdater(chassis) + temperature_updater.update() + assert temperature_updater.log_warning.call_count == 0 + + temperature_updater._refresh_temperature_status = mock.MagicMock(side_effect=Exception("Test message")) + temperature_updater.update() + assert temperature_updater.log_warning.call_count == 1 + + # TODO: Clean this up once we no longer need to support Python 2 + if sys.version_info.major == 3: + temperature_updater.log_warning.assert_called_with("Failed to update thermal status - Exception('Test message')") + else: + temperature_updater.log_warning.assert_called_with("Failed to update thermal status - Exception('Test message',)") + + def test_update_thermal_with_exception(self): + chassis = MockChassis() + chassis.make_error_thermal() + thermal = MockThermal() + thermal.make_over_temper() + chassis.get_all_thermals().append(thermal) + + temperature_updater = thermalctld.TemperatureUpdater(chassis) + temperature_updater.update() + assert temperature_updater.log_warning.call_count == 2 + + # TODO: Clean this up once we no longer need to support Python 2 + if sys.version_info.major == 3: + expected_calls = [ + mock.call("Failed to update thermal status - Exception('Failed to get temperature')"), + mock.call('High temperature warning: chassis 1 Thermal 2 current temperature 3C, high threshold 2C') + ] + else: + expected_calls = [ + mock.call("Failed to update thermal status - Exception('Failed to get temperature',)"), + mock.call('High temperature warning: chassis 1 Thermal 2 current temperature 3C, high threshold 2C') + ] + assert temperature_updater.log_warning.mock_calls == expected_calls + + +# Modular chassis-related tests def test_updater_thermal_check_modular_chassis(): chassis = MockChassis() assert chassis.is_modular_chassis() == False - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + temperature_updater = thermalctld.TemperatureUpdater(chassis) assert temperature_updater.chassis_table == None chassis.set_modular_chassis(True) chassis.set_my_slot(-1) - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + temperature_updater = thermalctld.TemperatureUpdater(chassis) assert temperature_updater.chassis_table == None my_slot = 1 chassis.set_my_slot(my_slot) - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + temperature_updater = thermalctld.TemperatureUpdater(chassis) assert temperature_updater.chassis_table != None - assert temperature_updater.chassis_table.table_name == TEMPER_INFO_TABLE_NAME+'_'+str(my_slot) + assert temperature_updater.chassis_table.table_name == '{}_{}'.format(TEMPER_INFO_TABLE_NAME, str(my_slot)) def test_updater_thermal_check_chassis_table(): @@ -322,7 +547,7 @@ def test_updater_thermal_check_chassis_table(): chassis.set_modular_chassis(True) chassis.set_my_slot(1) - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + temperature_updater = thermalctld.TemperatureUpdater(chassis) temperature_updater.update() assert temperature_updater.chassis_table.get_size() == chassis.get_num_thermals() @@ -332,9 +557,6 @@ def test_updater_thermal_check_chassis_table(): temperature_updater.update() assert temperature_updater.chassis_table.get_size() == chassis.get_num_thermals() - temperature_updater.deinit() - assert temperature_updater.chassis_table.get_size() == 0 - def test_updater_thermal_check_min_max(): chassis = MockChassis() @@ -344,9 +566,120 @@ def test_updater_thermal_check_min_max(): chassis.set_modular_chassis(True) chassis.set_my_slot(1) - temperature_updater = TemperatureUpdater(SYSLOG_IDENTIFIER, chassis) + temperature_updater = thermalctld.TemperatureUpdater(chassis) temperature_updater.update() slot_dict = temperature_updater.chassis_table.get(thermal.get_name()) assert slot_dict['minimum_temperature'] == str(thermal.get_minimum_recorded()) assert slot_dict['maximum_temperature'] == str(thermal.get_maximum_recorded()) + + +def test_signal_handler(): + # Test SIGHUP + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.set = mock.MagicMock() + daemon_thermalctld.log_info = mock.MagicMock() + daemon_thermalctld.log_warning = mock.MagicMock() + daemon_thermalctld.signal_handler(thermalctld.signal.SIGHUP, None) + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert daemon_thermalctld.log_info.call_count == 1 + daemon_thermalctld.log_info.assert_called_with("Caught signal 'SIGHUP' - ignoring...") + assert daemon_thermalctld.log_warning.call_count == 0 + assert daemon_thermalctld.stop_event.set.call_count == 0 + assert thermalctld.exit_code == thermalctld.ERR_UNKNOWN + + # Test SIGINT + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.set = mock.MagicMock() + daemon_thermalctld.log_info = mock.MagicMock() + daemon_thermalctld.log_warning = mock.MagicMock() + test_signal = thermalctld.signal.SIGINT + daemon_thermalctld.signal_handler(test_signal, None) + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert daemon_thermalctld.log_info.call_count == 1 + daemon_thermalctld.log_info.assert_called_with("Caught signal 'SIGINT' - exiting...") + assert daemon_thermalctld.log_warning.call_count == 0 + assert daemon_thermalctld.stop_event.set.call_count == 1 + assert thermalctld.exit_code == (128 + test_signal) + + # Test SIGTERM + thermalctld.exit_code = thermalctld.ERR_UNKNOWN + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.set = mock.MagicMock() + daemon_thermalctld.log_info = mock.MagicMock() + daemon_thermalctld.log_warning = mock.MagicMock() + test_signal = thermalctld.signal.SIGTERM + daemon_thermalctld.signal_handler(test_signal, None) + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert daemon_thermalctld.log_info.call_count == 1 + daemon_thermalctld.log_info.assert_called_with("Caught signal 'SIGTERM' - exiting...") + assert daemon_thermalctld.log_warning.call_count == 0 + assert daemon_thermalctld.stop_event.set.call_count == 1 + assert thermalctld.exit_code == (128 + test_signal) + + # Test an unhandled signal + thermalctld.exit_code = thermalctld.ERR_UNKNOWN + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.set = mock.MagicMock() + daemon_thermalctld.log_info = mock.MagicMock() + daemon_thermalctld.log_warning = mock.MagicMock() + daemon_thermalctld.signal_handler(thermalctld.signal.SIGUSR1, None) + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert daemon_thermalctld.log_warning.call_count == 1 + daemon_thermalctld.log_warning.assert_called_with("Caught unhandled signal 'SIGUSR1' - ignoring...") + assert daemon_thermalctld.log_info.call_count == 0 + assert daemon_thermalctld.stop_event.set.call_count == 0 + assert thermalctld.exit_code == thermalctld.ERR_UNKNOWN + + +def test_daemon_run(): + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.wait = mock.MagicMock(return_value=True) + ret = daemon_thermalctld.run() + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert ret is False + + daemon_thermalctld = thermalctld.ThermalControlDaemon() + daemon_thermalctld.stop_event.wait = mock.MagicMock(return_value=False) + ret = daemon_thermalctld.run() + daemon_thermalctld.deinit() # Deinit becuase the test will hang if we assert + assert ret is True + + +def test_try_get(): + def good_callback(): + return 'good result' + + def unimplemented_callback(): + raise NotImplementedError + + ret = thermalctld.try_get(good_callback) + assert ret == 'good result' + + ret = thermalctld.try_get(unimplemented_callback) + assert ret == thermalctld.NOT_AVAILABLE + + ret = thermalctld.try_get(unimplemented_callback, 'my default') + assert ret == 'my default' + + +def test_update_entity_info(): + mock_table = mock.MagicMock() + mock_fan = MockFan() + expected_fvp = thermalctld.swsscommon.FieldValuePairs( + [('position_in_parent', '1'), + ('parent_name', 'Parent Name') + ]) + + thermalctld.update_entity_info(mock_table, 'Parent Name', 'Key Name', mock_fan, 1) + assert mock_table.set.call_count == 1 + mock_table.set.assert_called_with('Key Name', expected_fvp) + + +@mock.patch('thermalctld.ThermalControlDaemon.run') +def test_main(mock_run): + mock_run.return_value = False + + ret = thermalctld.main() + assert mock_run.call_count == 1 + assert ret != 0