Skip to content

Commit c5be3ca

Browse files
authored
[psud] Increase unit test coverage; Refactor mock platform (#154)
- Refactor psud such that the `run()` method does not contain an infinite loop, thus allowing us to unit test it - 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) - Eliminate the need to check for a `PSUD_UNIT_TESTING` environment variable in daemon code - Increase overall unit test unit test coverage from 78% to 97%
1 parent 260cf2d commit c5be3ca

File tree

13 files changed

+663
-343
lines changed

13 files changed

+663
-343
lines changed

sonic-psud/pytest.ini

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
[pytest]
2-
addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -v
2+
addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv

sonic-psud/scripts/psud

+78-68
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,14 @@
99
The loop interval is PSU_INFO_UPDATE_PERIOD_SECS in seconds.
1010
"""
1111

12-
import os
1312
import signal
1413
import sys
1514
import threading
1615
from datetime import datetime
1716

17+
from sonic_platform.psu import Psu
1818
from sonic_py_common import daemon_base, logger
19-
20-
# If unit testing is occurring, mock swsscommon and module_base
21-
if os.getenv("PSUD_UNIT_TESTING") == "1":
22-
from tests.mock_platform import MockPsu as Psu
23-
from tests import mock_swsscommon as swsscommon
24-
else:
25-
from sonic_platform.psu import Psu
26-
from swsscommon import swsscommon
19+
from swsscommon import swsscommon
2720

2821

2922
#
@@ -32,8 +25,8 @@ else:
3225

3326
# TODO: Once we no longer support Python 2, we can eliminate this and get the
3427
# name using the 'name' field (e.g., `signal.SIGINT.name`) starting with Python 3.5
35-
SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n) \
36-
for n in dir(signal) if n.startswith('SIG') and '_' not in n )
28+
SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n)
29+
for n in dir(signal) if n.startswith('SIG') and '_' not in n)
3730

3831
SYSLOG_IDENTIFIER = "psud"
3932

@@ -85,8 +78,11 @@ PSUUTIL_LOAD_ERROR = 1
8578
platform_psuutil = None
8679
platform_chassis = None
8780

81+
exit_code = 0
8882

8983
# temporary wrappers that are compliable with both new platform api and old-style plugin mode
84+
85+
9086
def _wrapper_get_num_psus():
9187
if platform_chassis is not None:
9288
try:
@@ -131,7 +127,7 @@ def psu_db_update(psu_tbl, psu_num):
131127
psu_tbl.set(get_psu_key(psu_index), fvs)
132128

133129

134-
# try get information from platform API and return a default value if caught NotImplementedError
130+
# try get information from platform API and return a default value if we catch NotImplementedError
135131
def try_get(callback, default=None):
136132
"""
137133
Handy function to invoke the callback and catch NotImplementedError
@@ -257,11 +253,10 @@ class PsuChassisInfo(logger.Logger):
257253
self.master_status_good = master_status_good
258254

259255
# Update the PSU master status LED
260-
try:
261-
color = Psu.STATUS_LED_COLOR_GREEN if master_status_good else Psu.STATUS_LED_COLOR_RED
262-
Psu.set_status_master_led(color)
263-
except NotImplementedError:
264-
self.log_warning("set_status_master_led() not implemented")
256+
# set_status_master_led() is a class method implemented in PsuBase
257+
# so we do not need to catch NotImplementedError here
258+
color = Psu.STATUS_LED_COLOR_GREEN if master_status_good else Psu.STATUS_LED_COLOR_RED
259+
Psu.set_status_master_led(color)
265260

266261
log_on_status_changed(self, self.master_status_good,
267262
'PSU supplied power warning cleared: supplied power is back to normal.',
@@ -351,32 +346,21 @@ class DaemonPsud(daemon_base.DaemonBase):
351346
def __init__(self, log_identifier):
352347
super(DaemonPsud, self).__init__(log_identifier)
353348

354-
self.stop = threading.Event()
349+
# Set minimum logging level to INFO
350+
self.set_min_log_priority_info()
351+
352+
self.stop_event = threading.Event()
353+
self.num_psus = 0
355354
self.psu_status_dict = {}
355+
self.chassis_tbl = None
356356
self.fan_tbl = None
357+
self.psu_tbl = None
357358
self.psu_chassis_info = None
358359
self.first_run = True
359360

360-
# Signal handler
361-
def signal_handler(self, sig, frame):
362-
if sig == signal.SIGHUP:
363-
self.log_info("Caught SIGHUP - ignoring...")
364-
elif sig == signal.SIGINT:
365-
self.log_info("Caught SIGINT - exiting...")
366-
self.stop.set()
367-
elif sig == signal.SIGTERM:
368-
self.log_info("Caught SIGTERM - exiting...")
369-
self.stop.set()
370-
else:
371-
self.log_warning("Caught unhandled signal '{}'".format(SIGNALS_TO_NAMES_DICT[sig]))
372-
373-
# Run daemon
374-
def run(self):
375361
global platform_psuutil
376362
global platform_chassis
377363

378-
self.log_info("Starting up...")
379-
380364
# Load new platform api class
381365
try:
382366
import sonic_platform.platform
@@ -394,52 +378,70 @@ class DaemonPsud(daemon_base.DaemonBase):
394378

395379
# Connect to STATE_DB and create psu/chassis info tables
396380
state_db = daemon_base.db_connect("STATE_DB")
397-
chassis_tbl = swsscommon.Table(state_db, CHASSIS_INFO_TABLE)
398-
psu_tbl = swsscommon.Table(state_db, PSU_INFO_TABLE)
381+
self.chassis_tbl = swsscommon.Table(state_db, CHASSIS_INFO_TABLE)
382+
self.psu_tbl = swsscommon.Table(state_db, PSU_INFO_TABLE)
399383
self.fan_tbl = swsscommon.Table(state_db, FAN_INFO_TABLE)
400384
self.phy_entity_tbl = swsscommon.Table(state_db, PHYSICAL_ENTITY_INFO_TABLE)
401385

402386
# Post psu number info to STATE_DB
403-
psu_num = _wrapper_get_num_psus()
404-
fvs = swsscommon.FieldValuePairs([(CHASSIS_INFO_PSU_NUM_FIELD, str(psu_num))])
405-
chassis_tbl.set(CHASSIS_INFO_KEY, fvs)
387+
self.num_psus = _wrapper_get_num_psus()
388+
fvs = swsscommon.FieldValuePairs([(CHASSIS_INFO_PSU_NUM_FIELD, str(self.num_psus))])
389+
self.chassis_tbl.set(CHASSIS_INFO_KEY, fvs)
406390

407-
# Start main loop
408-
self.log_info("Start daemon main loop")
391+
def __del__(self):
392+
# Delete all the information from DB and then exit
393+
for psu_index in range(1, self.num_psus + 1):
394+
self.psu_tbl._del(get_psu_key(psu_index))
409395

410-
while not self.stop.wait(PSU_INFO_UPDATE_PERIOD_SECS):
411-
self._update_psu_entity_info()
412-
psu_db_update(psu_tbl, psu_num)
413-
self.update_psu_data(psu_tbl)
414-
self._update_led_color(psu_tbl)
396+
self.chassis_tbl._del(CHASSIS_INFO_KEY)
397+
self.chassis_tbl._del(CHASSIS_INFO_POWER_KEY_TEMPLATE.format(1))
415398

416-
if platform_chassis and platform_chassis.is_modular_chassis():
417-
self.update_psu_chassis_info(chassis_tbl)
399+
# Override signal handler from DaemonBase
400+
def signal_handler(self, sig, frame):
401+
FATAL_SIGNALS = [signal.SIGINT, signal.SIGTERM]
402+
NONFATAL_SIGNALS = [signal.SIGHUP]
418403

419-
self.first_run = False
404+
global exit_code
420405

421-
self.log_info("Stop daemon main loop")
406+
if sig in FATAL_SIGNALS:
407+
self.log_info("Caught signal '{}' - exiting...".format(SIGNALS_TO_NAMES_DICT[sig]))
408+
exit_code = 128 + sig # Make sure we exit with a non-zero code so that supervisor will try to restart us
409+
self.stop_event.set()
410+
elif sig in NONFATAL_SIGNALS:
411+
self.log_info("Caught signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig]))
412+
else:
413+
self.log_warning("Caught unhandled signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig]))
422414

423-
# Delete all the information from DB and then exit
424-
for psu_index in range(1, psu_num + 1):
425-
psu_tbl._del(get_psu_key(psu_index))
415+
# Main daemon logic
416+
def run(self):
417+
if self.stop_event.wait(PSU_INFO_UPDATE_PERIOD_SECS):
418+
# We received a fatal signal
419+
return False
426420

427-
chassis_tbl._del(CHASSIS_INFO_KEY)
428-
chassis_tbl._del(CHASSIS_INFO_POWER_KEY_TEMPLATE.format(1))
421+
self._update_psu_entity_info()
422+
psu_db_update(self.psu_tbl, self.num_psus)
423+
self.update_psu_data()
424+
self._update_led_color()
429425

430-
self.log_info("Shutting down...")
426+
if platform_chassis and platform_chassis.is_modular_chassis():
427+
self.update_psu_chassis_info()
431428

432-
def update_psu_data(self, psu_tbl):
429+
if self.first_run:
430+
self.first_run = False
431+
432+
return True
433+
434+
def update_psu_data(self):
433435
if not platform_chassis:
434436
return
435437

436438
for index, psu in enumerate(platform_chassis.get_all_psus()):
437439
try:
438-
self._update_single_psu_data(index + 1, psu, psu_tbl)
440+
self._update_single_psu_data(index + 1, psu)
439441
except Exception as e:
440442
self.log_warning("Failed to update PSU data - {}".format(e))
441443

442-
def _update_single_psu_data(self, index, psu, psu_tbl):
444+
def _update_single_psu_data(self, index, psu):
443445
name = get_psu_key(index)
444446
presence = _wrapper_get_psu_presence(index)
445447
power_good = False
@@ -517,7 +519,7 @@ class DaemonPsud(daemon_base.DaemonBase):
517519
(PSU_INFO_POWER_FIELD, str(power)),
518520
(PSU_INFO_FRU_FIELD, str(is_replaceable)),
519521
])
520-
psu_tbl.set(name, fvs)
522+
self.psu_tbl.set(name, fvs)
521523

522524
def _update_psu_entity_info(self):
523525
if not platform_chassis:
@@ -567,15 +569,15 @@ class DaemonPsud(daemon_base.DaemonBase):
567569
except NotImplementedError:
568570
self.log_warning("set_status_led() not implemented")
569571

570-
def _update_led_color(self, psu_tbl):
572+
def _update_led_color(self):
571573
if not platform_chassis:
572574
return
573575

574576
for index, psu_status in self.psu_status_dict.items():
575577
fvs = swsscommon.FieldValuePairs([
576578
('led_status', str(try_get(psu_status.psu.get_status_led, NOT_AVAILABLE)))
577-
])
578-
psu_tbl.set(get_psu_key(index), fvs)
579+
])
580+
self.psu_tbl.set(get_psu_key(index), fvs)
579581
self._update_psu_fan_led_status(psu_status.psu, index)
580582

581583
def _update_psu_fan_led_status(self, psu, psu_index):
@@ -588,14 +590,14 @@ class DaemonPsud(daemon_base.DaemonBase):
588590
])
589591
self.fan_tbl.set(fan_name, fvs)
590592

591-
def update_psu_chassis_info(self, chassis_tbl):
593+
def update_psu_chassis_info(self):
592594
if not platform_chassis:
593595
return
594596

595597
if not self.psu_chassis_info:
596598
self.psu_chassis_info = PsuChassisInfo(SYSLOG_IDENTIFIER, platform_chassis)
597599

598-
self.psu_chassis_info.run_power_budget(chassis_tbl)
600+
self.psu_chassis_info.run_power_budget(self.chassis_tbl)
599601
self.psu_chassis_info.update_master_status()
600602

601603

@@ -606,8 +608,16 @@ class DaemonPsud(daemon_base.DaemonBase):
606608

607609
def main():
608610
psud = DaemonPsud(SYSLOG_IDENTIFIER)
609-
psud.run()
611+
612+
psud.log_info("Starting up...")
613+
614+
while psud.run():
615+
pass
616+
617+
psud.log_info("Shutting down...")
618+
619+
return exit_code
610620

611621

612622
if __name__ == '__main__':
613-
main()
623+
sys.exit(main())

sonic-psud/setup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
tests_require=[
2424
'mock>=2.0.0; python_version < "3.3"',
2525
'pytest',
26-
'pytest-cov'
26+
'pytest-cov',
27+
'sonic_platform_common'
2728
],
2829
classifiers=[
2930
'Development Status :: 4 - Beta',

sonic-psud/tests/mock_device_base.py

-11
This file was deleted.

0 commit comments

Comments
 (0)