Skip to content

Commit 5b6d9c0

Browse files
authored
[syseepromd] Add unit tests; Refactor to allow for greater unit test coverage (sonic-net#156)
- Refactor syseepromd to eliminate infinite loops to allow for increased unit test coverage - Refactor signal handler and ensure daemon always exits with non-zero exit code so that supervisor will restart it - Add unit tests Unit test coverage increases from 0% to 90%
1 parent 0bd9f69 commit 5b6d9c0

File tree

11 files changed

+418
-95
lines changed

11 files changed

+418
-95
lines changed

sonic-syseepromd/pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[pytest]
2+
addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml --junitxml=test-results.xml -vv

sonic-syseepromd/scripts/syseepromd

Lines changed: 92 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -8,83 +8,100 @@
88
With this daemon, show syseeprom CLI will be able to get data from state DB instead of access hw or cache.
99
'''
1010

11-
try:
12-
import signal
13-
import sys
14-
import threading
11+
import signal
12+
import sys
13+
import threading
1514

16-
from sonic_py_common import daemon_base
17-
from swsscommon import swsscommon
18-
except ImportError as e:
19-
raise ImportError(str(e) + " - required module not found")
15+
from sonic_py_common import daemon_base
16+
from swsscommon import swsscommon
17+
18+
19+
# TODO: Once we no longer support Python 2, we can eliminate this and get the
20+
# name using the 'name' field (e.g., `signal.SIGINT.name`) starting with Python 3.5
21+
SIGNALS_TO_NAMES_DICT = dict((getattr(signal, n), n)
22+
for n in dir(signal) if n.startswith('SIG') and '_' not in n)
2023

2124
PLATFORM_SPECIFIC_MODULE_NAME = 'eeprom'
2225
PLATFORM_SPECIFIC_CLASS_NAME = 'board'
2326

2427
EEPROM_INFO_UPDATE_PERIOD_SECS = 60
2528

26-
POST_EEPROM_SUCCESS = 0
29+
ERR_NONE = 0
2730
ERR_PLATFORM_NOT_SUPPORT = 1
2831
ERR_FAILED_EEPROM = 2
2932
ERR_FAILED_UPDATE_DB = 3
3033
ERR_INVALID_PARAMETER = 4
31-
ERR_EEPROMUTIL_LOAD = 5
34+
ERR_EEPROM_LOAD = 5
3235

3336
EEPROM_TABLE_NAME = 'EEPROM_INFO'
37+
3438
SYSLOG_IDENTIFIER = 'syseepromd'
3539

40+
exit_code = 0
41+
3642

3743
class DaemonSyseeprom(daemon_base.DaemonBase):
38-
def __init__(self, log_identifier):
39-
super(DaemonSyseeprom, self).__init__(log_identifier)
44+
def __init__(self):
45+
super(DaemonSyseeprom, self).__init__(SYSLOG_IDENTIFIER)
46+
47+
# Set minimum logging level to INFO
48+
self.set_min_log_priority_info()
4049

4150
self.stop_event = threading.Event()
4251
self.eeprom = None
52+
self.eeprom_tbl = None
4353

44-
state_db = daemon_base.db_connect("STATE_DB")
45-
self.eeprom_tbl = swsscommon.Table(state_db, EEPROM_TABLE_NAME)
46-
self.eepromtbl_keys = []
54+
# First, try to load the new platform API
55+
try:
56+
import sonic_platform
57+
self.eeprom = sonic_platform.platform.Platform().get_chassis().get_eeprom()
58+
except Exception as e:
59+
self.log_warning(
60+
"Failed to load platform-specific eeprom from sonic_platform package due to {}. Trying deprecated plugin method ...".format(repr(e)))
4761

48-
def _wrapper_read_eeprom(self):
49-
if self.eeprom is not None:
62+
# If we didn't successfully load the class from the sonic_platform package, try loading the old plugin
5063
try:
51-
return self.eeprom.read_eeprom()
52-
except (NotImplementedError, IOError):
53-
pass
64+
self.eeprom = self.load_platform_util(PLATFORM_SPECIFIC_MODULE_NAME, PLATFORM_SPECIFIC_CLASS_NAME)
65+
except Exception as e:
66+
self.log_error("Failed to load platform-specific eeprom from deprecated plugin: {}".format(repr(e)))
5467

55-
try:
56-
return self.eeprom.read_eeprom()
57-
except IOError:
58-
pass
68+
if not self.eeprom:
69+
sys.exit(ERR_EEPROM_LOAD)
5970

60-
def _wrapper_update_eeprom_db(self, eeprom):
61-
if self.eeprom is not None:
62-
try:
63-
return self.eeprom.update_eeprom_db(eeprom)
64-
except NotImplementedError:
65-
pass
71+
# Connect to STATE_DB
72+
state_db = daemon_base.db_connect("STATE_DB")
73+
self.eeprom_tbl = swsscommon.Table(state_db, EEPROM_TABLE_NAME)
74+
self.eepromtbl_keys = []
6675

67-
return self.eeprom.update_eeprom_db(eeprom)
76+
# Post system EEPROM info to state DB once at start-up
77+
rc = self.post_eeprom_to_db()
78+
if rc != ERR_NONE:
79+
self.log_error("Failed to post system EEPROM info to database")
80+
81+
def __del__(self):
82+
# Delete all the information from DB
83+
self.clear_db()
6884

6985
def post_eeprom_to_db(self):
70-
eeprom = self._wrapper_read_eeprom()
71-
if eeprom is None:
72-
self.log_error("Failed to read eeprom")
86+
eeprom_data = self.eeprom.read_eeprom()
87+
if eeprom_data is None:
88+
self.log_error("Failed to read EEPROM")
7389
return ERR_FAILED_EEPROM
7490

75-
err = self._wrapper_update_eeprom_db(eeprom)
91+
err = self.eeprom.update_eeprom_db(eeprom_data)
7692
if err:
77-
self.log_error("Failed to update eeprom info to database")
93+
self.log_error("Failed to update EEPROM info in database")
7894
return ERR_FAILED_UPDATE_DB
7995

8096
self.eepromtbl_keys = self.eeprom_tbl.getKeys()
8197

82-
return POST_EEPROM_SUCCESS
98+
return ERR_NONE
8399

84100
def clear_db(self):
85-
keys = self.eeprom_tbl.getKeys()
86-
for key in keys:
87-
self.eeprom_tbl._del(key)
101+
if self.eeprom_tbl:
102+
keys = self.eeprom_tbl.getKeys()
103+
for key in keys:
104+
self.eeprom_tbl._del(key)
88105

89106
def detect_eeprom_table_integrity(self):
90107
keys = self.eeprom_tbl.getKeys()
@@ -98,75 +115,55 @@ class DaemonSyseeprom(daemon_base.DaemonBase):
98115

99116
return True
100117

101-
# Signal handler
118+
# Override signal handler from DaemonBase
102119
def signal_handler(self, sig, frame):
103-
if sig == signal.SIGHUP:
104-
self.log_info("Caught SIGHUP - ignoring...")
105-
elif sig == signal.SIGINT:
106-
self.log_info("Caught SIGINT - exiting...")
107-
self.stop_event.set()
108-
elif sig == signal.SIGTERM:
109-
self.log_info("Caught SIGTERM - exiting...")
120+
FATAL_SIGNALS = [signal.SIGINT, signal.SIGTERM]
121+
NONFATAL_SIGNALS = [signal.SIGHUP]
122+
123+
global exit_code
124+
125+
if sig in FATAL_SIGNALS:
126+
self.log_info("Caught signal '{}' - exiting...".format(SIGNALS_TO_NAMES_DICT[sig]))
127+
exit_code = 128 + sig # Make sure we exit with a non-zero code so that supervisor will try to restart us
110128
self.stop_event.set()
129+
elif sig in NONFATAL_SIGNALS:
130+
self.log_info("Caught signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig]))
111131
else:
112-
self.log_warning("Caught unhandled signal '" + sig + "'")
132+
self.log_warning("Caught unhandled signal '{}' - ignoring...".format(SIGNALS_TO_NAMES_DICT[sig]))
113133

114-
# Run daemon
134+
# Main daemon logic
115135
def run(self):
116-
self.log_info("Starting up...")
117-
118-
# First, try to load the new platform API
119-
try:
120-
import sonic_platform
121-
self.chassis = sonic_platform.platform.Platform().get_chassis()
122-
self.eeprom = self.chassis.get_eeprom()
123-
except Exception as e:
124-
self.log_warning("Failed to load data from eeprom using sonic_platform package due to {}, retrying using deprecated plugin method".format(repr(e)))
125-
126-
# If we didn't successfully load the class from the sonic_platform package, try loading the old plugin
127-
if not self.eeprom:
128-
try:
129-
self.eeprom = self.load_platform_util(PLATFORM_SPECIFIC_MODULE_NAME, PLATFORM_SPECIFIC_CLASS_NAME)
130-
except Exception as e:
131-
self.log_error("Failed to load platform-specific eeprom implementation: {}".format(repr(e)))
132-
133-
if not self.eeprom:
134-
sys.exit(ERR_EEPROMUTIL_LOAD)
135-
136-
# Connect to STATE_DB and post syseeprom info to state DB
137-
rc = self.post_eeprom_to_db()
138-
if rc != POST_EEPROM_SUCCESS:
139-
self.log_error("Failed to post eeprom to database")
140-
141-
# Start main loop
142-
self.log_info("Start daemon main loop")
143-
144-
while not self.stop_event.wait(EEPROM_INFO_UPDATE_PERIOD_SECS):
145-
rc = self.detect_eeprom_table_integrity()
146-
if not rc:
147-
self.log_info("sys eeprom table was changed, need update")
148-
self.clear_db()
149-
rcs = self.post_eeprom_to_db()
150-
if rcs != POST_EEPROM_SUCCESS:
151-
self.log_error("Failed to post eeprom to database")
152-
continue
153-
154-
self.log_info("Stop daemon main loop")
136+
if self.stop_event.wait(EEPROM_INFO_UPDATE_PERIOD_SECS):
137+
# We received a fatal signal
138+
return False
155139

156-
# Delete all the information from DB and then exit
157-
self.clear_db()
140+
rc = self.detect_eeprom_table_integrity()
141+
if not rc:
142+
self.log_info("System EEPROM table was changed, needs update")
143+
self.clear_db()
144+
rcs = self.post_eeprom_to_db()
145+
if rcs != ERR_NONE:
146+
self.log_error("Failed to post EEPROM to database")
158147

159-
self.log_info("Shutting down...")
148+
return True
160149

161150
#
162151
# Main =========================================================================
163152
#
164153

165154

166155
def main():
167-
syseepromd = DaemonSyseeprom(SYSLOG_IDENTIFIER)
168-
syseepromd.run()
156+
syseepromd = DaemonSyseeprom()
157+
158+
syseepromd.log_info("Starting up...")
159+
160+
while syseepromd.run():
161+
pass
162+
163+
syseepromd.log_info("Shutting down...")
164+
165+
return exit_code
169166

170167

171168
if __name__ == '__main__':
172-
main()
169+
sys.exit(main())

sonic-syseepromd/setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[aliases]
2+
test=pytest

sonic-syseepromd/setup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
setup_requires=[
1717
'wheel'
1818
],
19+
tests_require=[
20+
'mock>=2.0.0; python_version < "3.3"',
21+
'pytest',
22+
'pytest-cov',
23+
'sonic_platform_common'
24+
],
1925
classifiers=[
2026
'Development Status :: 4 - Beta',
2127
'Environment :: No Input/Output (Daemon)',

sonic-syseepromd/tests/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""
2+
Mock implementation of sonic_platform package for unit testing
3+
"""
4+
5+
from . import chassis
6+
from . import platform
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""
2+
Mock implementation of sonic_platform package for unit testing
3+
"""
4+
5+
# TODO: Clean this up once we no longer need to support Python 2
6+
import sys
7+
if sys.version_info.major == 3:
8+
from unittest import mock
9+
else:
10+
import mock
11+
12+
from sonic_platform_base.chassis_base import ChassisBase
13+
14+
15+
class Chassis(ChassisBase):
16+
def __init__(self):
17+
ChassisBase.__init__(self)
18+
self.eeprom = mock.MagicMock()
19+
20+
def get_eeprom(self):
21+
return self.eeprom
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Mock implementation of sonic_platform package for unit testing
3+
"""
4+
5+
from sonic_platform_base.platform_base import PlatformBase
6+
from sonic_platform.chassis import Chassis
7+
8+
9+
class Platform(PlatformBase):
10+
def __init__(self):
11+
PlatformBase.__init__(self)
12+
self._chassis = Chassis()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'''
2+
Mock implementation of swsscommon package for unit testing
3+
'''
4+
5+
from . import swsscommon
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'''
2+
Mock implementation of swsscommon package for unit testing
3+
'''
4+
5+
STATE_DB = ''
6+
7+
8+
class Table:
9+
def __init__(self, db, table_name):
10+
self.table_name = table_name
11+
self.mock_dict = {}
12+
13+
def _del(self, key):
14+
del self.mock_dict[key]
15+
pass
16+
17+
def set(self, key, fvs):
18+
self.mock_dict[key] = fvs.fv_dict
19+
pass
20+
21+
def get(self, key):
22+
if key in self.mock_dict:
23+
return self.mock_dict[key]
24+
return None
25+
26+
27+
class FieldValuePairs:
28+
fv_dict = {}
29+
30+
def __init__(self, tuple_list):
31+
if isinstance(tuple_list, list) and isinstance(tuple_list[0], tuple):
32+
self.fv_dict = dict(tuple_list)
33+
34+
def __setitem__(self, key, kv_tuple):
35+
self.fv_dict[kv_tuple[0]] = kv_tuple[1]
36+
37+
def __getitem__(self, key):
38+
return self.fv_dict[key]
39+
40+
def __eq__(self, other):
41+
if not isinstance(other, FieldValuePairs):
42+
# don't attempt to compare against unrelated types
43+
return NotImplemented
44+
45+
return self.fv_dict == other.fv_dict
46+
47+
def __repr__(self):
48+
return repr(self.fv_dict)
49+
50+
def __str__(self):
51+
return repr(self.fv_dict)

0 commit comments

Comments
 (0)