Skip to content

Commit e179ffc

Browse files
authored
[psud] Refactor unit tests; increase unit test coverage (sonic-net#146)
- Encapsulate all PsuChassisInfo test cases in a TestPsuChassisInfo class - Move TestPsuChassisInfo to its own file, test_PsuChassisInfo.py - Increase coverage of PsuChassisInfo class - Increase coverage of TestDaemonPsud class - In all test scripts, use `import psud` rather than `from psud import *` and reference everything using the `psud.` namespace. Also do the same with `mock` - Remove unnecessary try/except around import statements in psud - Add unit tests for signal_handler - Fix `TypeError: cannot concatenate 'str' and 'int' objects` in signal_handler Overall psud unit test coverage increases from 51% to 78%.
1 parent 068bccc commit e179ffc

File tree

7 files changed

+906
-346
lines changed

7 files changed

+906
-346
lines changed

sonic-psud/scripts/psud

Lines changed: 55 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env python2
1+
#!/usr/bin/env python3
22

33
"""
44
psud
@@ -9,27 +9,32 @@
99
The loop interval is PSU_INFO_UPDATE_PERIOD_SECS in seconds.
1010
"""
1111

12-
try:
13-
import os
14-
import signal
15-
import sys
16-
import threading
17-
from datetime import datetime
18-
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 import mock_swsscommon as swsscommon
23-
else:
24-
from swsscommon import swsscommon
25-
except ImportError as e:
26-
raise ImportError(str(e) + " - required module not found")
12+
import os
13+
import signal
14+
import sys
15+
import threading
16+
from datetime import datetime
17+
18+
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
2727

2828

2929
#
3030
# Constants ====================================================================
3131
#
3232

33+
# TODO: Once we no longer support Python 2, we can eliminate this and get the
34+
# 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 )
37+
3338
SYSLOG_IDENTIFIER = "psud"
3439

3540
PLATFORM_SPECIFIC_MODULE_NAME = "psuutil"
@@ -91,7 +96,7 @@ def _wrapper_get_num_psus():
9196
return platform_psuutil.get_num_psus()
9297

9398

94-
def _wrapper_get_psus_presence(psu_index):
99+
def _wrapper_get_psu_presence(psu_index):
95100
if platform_chassis is not None:
96101
try:
97102
return platform_chassis.get_psu(psu_index - 1).get_presence()
@@ -100,7 +105,7 @@ def _wrapper_get_psus_presence(psu_index):
100105
return platform_psuutil.get_psu_presence(psu_index)
101106

102107

103-
def _wrapper_get_psus_status(psu_index):
108+
def _wrapper_get_psu_status(psu_index):
104109
if platform_chassis is not None:
105110
try:
106111
return platform_chassis.get_psu(psu_index - 1).get_powergood_status()
@@ -120,9 +125,9 @@ def get_psu_key(psu_index):
120125
def psu_db_update(psu_tbl, psu_num):
121126
for psu_index in range(1, psu_num + 1):
122127
fvs = swsscommon.FieldValuePairs([(PSU_INFO_PRESENCE_FIELD,
123-
'true' if _wrapper_get_psus_presence(psu_index) else 'false'),
128+
'true' if _wrapper_get_psu_presence(psu_index) else 'false'),
124129
(PSU_INFO_STATUS_FIELD,
125-
'true' if _wrapper_get_psus_status(psu_index) else 'false')])
130+
'true' if _wrapper_get_psu_status(psu_index) else 'false')])
126131
psu_tbl.set(get_psu_key(psu_index), fvs)
127132

128133

@@ -251,22 +256,20 @@ class PsuChassisInfo(logger.Logger):
251256

252257
self.master_status_good = master_status_good
253258

254-
return True
255-
256-
def _set_psu_master_led(self, master_status):
259+
# Update the PSU master status LED
257260
try:
258-
try:
259-
if os.getenv("PSUD_UNIT_TESTING") == "1":
260-
from tests.mock_platform import MockPsu as Psu
261-
else:
262-
from sonic_platform.psu import Psu
263-
except ImportError as e:
264-
raise ImportError(str(e) + " - required module not found")
265-
266-
color = Psu.STATUS_LED_COLOR_GREEN if master_status else Psu.STATUS_LED_COLOR_RED
261+
color = Psu.STATUS_LED_COLOR_GREEN if master_status_good else Psu.STATUS_LED_COLOR_RED
267262
Psu.set_status_master_led(color)
268-
except NotImplementedError as e:
269-
pass
263+
except NotImplementedError:
264+
self.log_warning("set_status_master_led() not implemented")
265+
266+
log_on_status_changed(self, self.master_status_good,
267+
'PSU supplied power warning cleared: supplied power is back to normal.',
268+
'PSU supplied power warning: {}W supplied-power less than {}W consumed-power'.format(
269+
self.total_supplied_power, self.total_consumed_power)
270+
)
271+
272+
return True
270273

271274
# PSU status ===================================================================
272275
#
@@ -365,7 +368,7 @@ class DaemonPsud(daemon_base.DaemonBase):
365368
self.log_info("Caught SIGTERM - exiting...")
366369
self.stop.set()
367370
else:
368-
self.log_warning("Caught unhandled signal '" + sig + "'")
371+
self.log_warning("Caught unhandled signal '{}'".format(SIGNALS_TO_NAMES_DICT[sig]))
369372

370373
# Run daemon
371374
def run(self):
@@ -410,9 +413,8 @@ class DaemonPsud(daemon_base.DaemonBase):
410413
self.update_psu_data(psu_tbl)
411414
self._update_led_color(psu_tbl)
412415

413-
if platform_chassis is not None and platform_chassis.is_modular_chassis():
416+
if platform_chassis and platform_chassis.is_modular_chassis():
414417
self.update_psu_chassis_info(chassis_tbl)
415-
self.update_master_led_color(chassis_tbl)
416418

417419
self.first_run = False
418420

@@ -439,7 +441,7 @@ class DaemonPsud(daemon_base.DaemonBase):
439441

440442
def _update_single_psu_data(self, index, psu, psu_tbl):
441443
name = get_psu_key(index)
442-
presence = _wrapper_get_psus_presence(index)
444+
presence = _wrapper_get_psu_presence(index)
443445
power_good = False
444446
voltage = None
445447
voltage_high_threshold = None
@@ -450,7 +452,7 @@ class DaemonPsud(daemon_base.DaemonBase):
450452
power = None
451453
is_replaceable = try_get(psu.is_replaceable, False)
452454
if presence:
453-
power_good = _wrapper_get_psus_status(index)
455+
power_good = _wrapper_get_psu_status(index)
454456
voltage = try_get(psu.get_voltage)
455457
voltage_high_threshold = try_get(psu.get_voltage_high_threshold)
456458
voltage_low_threshold = try_get(psu.get_voltage_low_threshold)
@@ -542,17 +544,17 @@ class DaemonPsud(daemon_base.DaemonBase):
542544
:return:
543545
"""
544546
psu_name = get_psu_key(psu_index)
545-
presence = _wrapper_get_psus_presence(psu_index)
547+
presence = _wrapper_get_psu_presence(psu_index)
546548
fan_list = psu.get_all_fans()
547549
for index, fan in enumerate(fan_list):
548550
fan_name = try_get(fan.get_name, '{} FAN {}'.format(psu_name, index + 1))
549-
direction = try_get(fan.get_direction) if presence else NOT_AVAILABLE
550-
speed = try_get(fan.get_speed) if presence else NOT_AVAILABLE
551+
direction = try_get(fan.get_direction, NOT_AVAILABLE) if presence else NOT_AVAILABLE
552+
speed = try_get(fan.get_speed, NOT_AVAILABLE) if presence else NOT_AVAILABLE
551553
status = "True" if presence else "False"
552554
fvs = swsscommon.FieldValuePairs(
553555
[(FAN_INFO_PRESENCE_FIELD, str(presence)),
554-
(FAN_INFO_STATUS_FIELD, str(status)),
555-
(FAN_INFO_DIRECTION_FIELD, str(direction)),
556+
(FAN_INFO_STATUS_FIELD, status),
557+
(FAN_INFO_DIRECTION_FIELD, direction),
556558
(FAN_INFO_SPEED_FIELD, str(speed)),
557559
(FAN_INFO_TIMESTAMP_FIELD, datetime.now().strftime('%Y%m%d %H:%M:%S'))
558560
])
@@ -562,22 +564,16 @@ class DaemonPsud(daemon_base.DaemonBase):
562564
try:
563565
color = psu.STATUS_LED_COLOR_GREEN if psu_status.is_ok() else psu.STATUS_LED_COLOR_RED
564566
psu.set_status_led(color)
565-
except NotImplementedError as e:
566-
pass
567+
except NotImplementedError:
568+
self.log_warning("set_status_led() not implemented")
567569

568570
def _update_led_color(self, psu_tbl):
569571
if not platform_chassis:
570572
return
571573

572574
for index, psu_status in self.psu_status_dict.items():
573-
try:
574-
fvs = swsscommon.FieldValuePairs([
575-
('led_status', str(try_get(psu_status.psu.get_status_led)))
576-
])
577-
except Exception as e:
578-
self.log_warning('Failed to get led status for psu {}'.format(index))
579-
fvs = swsscommon.FieldValuePairs([
580-
('led_status', NOT_AVAILABLE)
575+
fvs = swsscommon.FieldValuePairs([
576+
('led_status', str(try_get(psu_status.psu.get_status_led, NOT_AVAILABLE)))
581577
])
582578
psu_tbl.set(get_psu_key(index), fvs)
583579
self._update_psu_fan_led_status(psu_status.psu, index)
@@ -587,15 +583,9 @@ class DaemonPsud(daemon_base.DaemonBase):
587583
fan_list = psu.get_all_fans()
588584
for index, fan in enumerate(fan_list):
589585
fan_name = try_get(fan.get_name, '{} FAN {}'.format(psu_name, index + 1))
590-
try:
591-
fvs = swsscommon.FieldValuePairs([
592-
(FAN_INFO_LED_STATUS_FIELD, str(try_get(fan.get_status_led)))
593-
])
594-
except Exception as e:
595-
self.log_warning('Failed to get led status for fan {}'.format(fan_name))
596-
fvs = swsscommon.FieldValuePairs([
597-
(FAN_INFO_LED_STATUS_FIELD, NOT_AVAILABLE)
598-
])
586+
fvs = swsscommon.FieldValuePairs([
587+
(FAN_INFO_LED_STATUS_FIELD, str(try_get(fan.get_status_led, NOT_AVAILABLE)))
588+
])
599589
self.fan_tbl.set(fan_name, fvs)
600590

601591
def update_psu_chassis_info(self, chassis_tbl):
@@ -606,25 +596,14 @@ class DaemonPsud(daemon_base.DaemonBase):
606596
self.psu_chassis_info = PsuChassisInfo(SYSLOG_IDENTIFIER, platform_chassis)
607597

608598
self.psu_chassis_info.run_power_budget(chassis_tbl)
609-
610-
def update_master_led_color(self, chassis_tbl):
611-
if not platform_chassis or not self.psu_chassis_info:
612-
return
613-
614-
psu_chassis_info = self.psu_chassis_info
615-
if psu_chassis_info.update_master_status():
616-
log_on_status_changed(self, psu_chassis_info.master_status_good,
617-
'PSU supplied power warning cleared: supplied power is back to normal.',
618-
'PSU supplied power warning: {}W supplied-power less than {}W consumed-power'.format(
619-
psu_chassis_info.total_supplied_power, psu_chassis_info.total_consumed_power)
620-
)
621-
psu_chassis_info._set_psu_master_led(psu_chassis_info.master_status_good)
599+
self.psu_chassis_info.update_master_status()
622600

623601

624602
#
625603
# Main =========================================================================
626604
#
627605

606+
628607
def main():
629608
psud = DaemonPsud(SYSLOG_IDENTIFIER)
630609
psud.run()

sonic-psud/tests/mock_platform.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,21 @@ def get_serial(self):
3030

3131

3232
class MockPsu(MockDevice):
33+
master_led_color = MockDevice.STATUS_LED_COLOR_OFF
3334

34-
psu_master_led_color = MockDevice.STATUS_LED_COLOR_OFF
35-
36-
def __init__(self, psu_presence, psu_status, psu_name):
37-
self.name = psu_name
35+
def __init__(self, presence, status, name, position_in_parent):
36+
self.name = name
3837
self.presence = True
39-
self.psu_status = psu_status
38+
self.status = status
4039
self.status_led_color = self.STATUS_LED_COLOR_OFF
40+
self.position_in_parent = position_in_parent
41+
self._fan_list = []
42+
43+
def get_all_fans(self):
44+
return self._fan_list
4145

4246
def get_powergood_status(self):
43-
return self.psu_status
47+
return self.status
4448

4549
def set_status_led(self, color):
4650
self.status_led_color = color
@@ -50,7 +54,10 @@ def get_status_led(self):
5054
return self.status_led_color
5155

5256
def set_status(self, status):
53-
self.psu_status = status
57+
self.status = status
58+
59+
def get_position_in_parent(self):
60+
return self.position_in_parent
5461

5562
def set_maximum_supplied_power(self, supplied_power):
5663
self.max_supplied_power = supplied_power
@@ -60,11 +67,11 @@ def get_maximum_supplied_power(self):
6067

6168
@classmethod
6269
def set_status_master_led(cls, color):
63-
cls.psu_master_led_color = color
70+
cls.master_led_color = color
6471

6572
@classmethod
6673
def get_status_master_led(cls):
67-
return cls.psu_master_led_color
74+
return cls.master_led_color
6875

6976

7077
class MockFanDrawer(MockDevice):
@@ -86,6 +93,31 @@ def get_maximum_consumed_power(self):
8693
return self.max_consumed_power
8794

8895

96+
class MockFan(MockDevice):
97+
FAN_DIRECTION_INTAKE = "intake"
98+
FAN_DIRECTION_EXHAUST = "exhaust"
99+
100+
def __init__(self, name, direction, speed=50):
101+
self.name = name
102+
self.direction = direction
103+
self.speed = speed
104+
self.status_led_color = self.STATUS_LED_COLOR_OFF
105+
106+
def get_direction(self):
107+
return self.direction
108+
109+
def get_speed(self):
110+
return self.speed
111+
112+
def set_status_led(self, color):
113+
self.status_led_color = color
114+
return True
115+
116+
def get_status_led(self):
117+
return self.status_led_color
118+
119+
120+
89121
class MockModule(MockDevice):
90122
def __init__(self, module_presence, module_status, module_name):
91123
self.name = module_name
@@ -106,7 +138,6 @@ def get_maximum_consumed_power(self):
106138

107139

108140
class MockChassis:
109-
110141
def __init__(self):
111142
self.psu_list = []
112143
self.fan_drawer_list = []

sonic-psud/tests/mock_swsscommon.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,28 @@ def get(self, key):
2020
return None
2121

2222

23-
class FieldValuePairs(dict):
24-
def __init__(self, len):
25-
self.fv_dict = {}
23+
class FieldValuePairs:
24+
fv_dict = {}
2625

27-
def __setitem__(self, key, val_tuple):
28-
self.fv_dict[val_tuple[0]] = val_tuple[1]
26+
def __init__(self, tuple_list):
27+
if isinstance(tuple_list, list) and isinstance(tuple_list[0], tuple):
28+
self.fv_dict = dict(tuple_list)
29+
30+
def __setitem__(self, key, kv_tuple):
31+
self.fv_dict[kv_tuple[0]] = kv_tuple[1]
32+
33+
def __getitem__(self, key):
34+
return self.fv_dict[key]
35+
36+
def __eq__(self, other):
37+
if not isinstance(other, FieldValuePairs):
38+
# don't attempt to compare against unrelated types
39+
return NotImplemented
40+
41+
return self.fv_dict == other.fv_dict
42+
43+
def __repr__(self):
44+
return repr(self.fv_dict)
45+
46+
def __str__(self):
47+
return repr(self.fv_dict)

0 commit comments

Comments
 (0)