Update unit tests for new software component
- cmd files are replaced by software_client - software_config.py is renamed to config.py and includes the previous config setup. - unit tests from sw-patch are being migrated here Story: 2010676 Task: 47917 Signed-off-by: Al Bailey <al.bailey@windriver.com> Change-Id: I886b4abd63a9b7057efd2b6440211a9c1f06f6f3
This commit is contained in:
parent
4624457333
commit
31366985ab
@ -12,7 +12,7 @@ import sys
|
||||
import time
|
||||
|
||||
import software.utils as utils
|
||||
import software.software_config as cfg
|
||||
import software.config as cfg
|
||||
import software.constants as constants
|
||||
from software.software_functions import LOG
|
||||
|
||||
|
@ -1,78 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
"""
|
||||
API console script for Unified Software Management
|
||||
"""
|
||||
import gc
|
||||
import socket
|
||||
from wsgiref import simple_server
|
||||
|
||||
from oslo_log import log as logging
|
||||
from software.api.app import setup_app
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
# todo(abailey): these need to be part of config
|
||||
API_PORT = 5496
|
||||
# Limit socket blocking to 5 seconds to allow for thread to shutdown
|
||||
API_SOCKET_TIMEOUT = 5.0
|
||||
|
||||
|
||||
class RestAPI():
|
||||
"""The base WSGI application"""
|
||||
def __init__(self):
|
||||
self.app = setup_app()
|
||||
self.running = False
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
class MyHandler(simple_server.WSGIRequestHandler):
|
||||
"""Overridden WSGIReqestHandler"""
|
||||
def address_string(self):
|
||||
# In the future, we could provide a config option to allow
|
||||
# reverse DNS lookups.
|
||||
return self.client_address[0]
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for API"""
|
||||
# todo(abailey): process configuration
|
||||
host = "127.0.0.1"
|
||||
port = API_PORT
|
||||
|
||||
# todo(abailey): configure logging
|
||||
LOG.info(" + Starting Unified Software Management API")
|
||||
|
||||
try:
|
||||
simple_server.WSGIServer.address_family = socket.AF_INET
|
||||
wsgi = simple_server.make_server(
|
||||
host, port,
|
||||
RestAPI(),
|
||||
handler_class=MyHandler
|
||||
)
|
||||
wsgi.socket.settimeout(API_SOCKET_TIMEOUT)
|
||||
|
||||
running = True
|
||||
while running: # run until an exception is raised
|
||||
wsgi.handle_request()
|
||||
|
||||
# Call garbage collect after wsgi request is handled,
|
||||
# to ensure any open file handles are closed in the case
|
||||
# of an upload.
|
||||
gc.collect()
|
||||
except KeyboardInterrupt:
|
||||
LOG.warning(" - Received Control C. Shutting down.")
|
||||
except BaseException: # pylint: disable=broad-exception-caught
|
||||
LOG.exception(" - Unhandled API exception")
|
||||
LOG.info(" - Stopping Unified Software Management API")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,40 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
"""
|
||||
Command Line Interface for Unified Software Management
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
BASENAME = 'software'
|
||||
commands = ('capabilities', 'info', 'bash_completion')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SoftwareShell:
|
||||
"""CLI Shell"""
|
||||
def main(self, argv):
|
||||
"""Parse and run the commands for this CLI"""
|
||||
print(f"Under construction {argv}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for CLI"""
|
||||
try:
|
||||
SoftwareShell().main(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
print(f"... terminating {BASENAME} client", file=sys.stderr)
|
||||
sys.exit(130)
|
||||
except Exception as ex: # pylint: disable=broad-exception-caught
|
||||
logger.debug(ex, exc_info=1)
|
||||
print(f"ERROR: {ex}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -4,7 +4,28 @@ Copyright (c) 2023 Wind River Systems, Inc.
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
import configparser
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
from oslo_config import cfg
|
||||
import tsconfig.tsconfig as tsc
|
||||
|
||||
import software.utils as utils
|
||||
import software.constants as constants
|
||||
|
||||
controller_mcast_group = None
|
||||
agent_mcast_group = None
|
||||
controller_port = 0
|
||||
agent_port = 0
|
||||
api_port = 0
|
||||
mgmt_if = None
|
||||
nodetype = None
|
||||
platform_conf_mtime = 0
|
||||
software_conf_mtime = 0
|
||||
software_conf = '/etc/software/software.conf'
|
||||
|
||||
# setup a shareable config
|
||||
CONF = cfg.CONF
|
||||
@ -41,3 +62,100 @@ pecan_opts = [
|
||||
|
||||
# register the configuration for this component
|
||||
CONF.register_opts(pecan_opts, group=PECAN_CONFIG_GROUP)
|
||||
|
||||
|
||||
def read_config():
|
||||
global software_conf_mtime
|
||||
global software_conf
|
||||
|
||||
if software_conf_mtime == os.stat(software_conf).st_mtime:
|
||||
# The file has not changed since it was last read
|
||||
return
|
||||
|
||||
defaults = {
|
||||
'controller_mcast_group': "239.1.1.3",
|
||||
'agent_mcast_group': "239.1.1.4",
|
||||
'api_port': "5493",
|
||||
'controller_port': "5494",
|
||||
'agent_port': "5495",
|
||||
}
|
||||
|
||||
global controller_mcast_group
|
||||
global agent_mcast_group
|
||||
global api_port
|
||||
global controller_port
|
||||
global agent_port
|
||||
|
||||
config = configparser.ConfigParser(defaults)
|
||||
|
||||
config.read(software_conf)
|
||||
software_conf_mtime = os.stat(software_conf).st_mtime
|
||||
|
||||
controller_mcast_group = config.get('runtime',
|
||||
'controller_multicast')
|
||||
agent_mcast_group = config.get('runtime', 'agent_multicast')
|
||||
|
||||
api_port = config.getint('runtime', 'api_port')
|
||||
controller_port = config.getint('runtime', 'controller_port')
|
||||
agent_port = config.getint('runtime', 'agent_port')
|
||||
|
||||
# The platform.conf file has no section headers, which causes problems
|
||||
# for ConfigParser. So we'll fake it out.
|
||||
ini_str = '[platform_conf]\n' + open(tsc.PLATFORM_CONF_FILE, 'r').read()
|
||||
ini_fp = io.StringIO(ini_str)
|
||||
config.read_file(ini_fp)
|
||||
|
||||
try:
|
||||
value = str(config.get('platform_conf', 'nodetype'))
|
||||
|
||||
global nodetype
|
||||
nodetype = value
|
||||
except configparser.Error:
|
||||
logging.exception("Failed to read nodetype from config")
|
||||
|
||||
|
||||
def get_mgmt_ip():
|
||||
# Check if initial config is complete
|
||||
if not os.path.exists('/etc/platform/.initial_config_complete'):
|
||||
return None
|
||||
mgmt_hostname = socket.gethostname()
|
||||
return utils.gethostbyname(mgmt_hostname)
|
||||
|
||||
|
||||
# Because the software daemons are launched before manifests are
|
||||
# applied, the content of some settings in platform.conf can change,
|
||||
# such as the management interface. As such, we can't just directly
|
||||
# use tsc.management_interface
|
||||
#
|
||||
def get_mgmt_iface():
|
||||
# Check if initial config is complete
|
||||
if not os.path.exists(constants.INITIAL_CONFIG_COMPLETE_FLAG):
|
||||
return None
|
||||
|
||||
global mgmt_if
|
||||
global platform_conf_mtime
|
||||
|
||||
if mgmt_if is not None and \
|
||||
platform_conf_mtime == os.stat(tsc.PLATFORM_CONF_FILE).st_mtime:
|
||||
# The platform.conf file hasn't been modified since we read it,
|
||||
# so return the cached value.
|
||||
return mgmt_if
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
# The platform.conf file has no section headers, which causes problems
|
||||
# for ConfigParser. So we'll fake it out.
|
||||
ini_str = '[platform_conf]\n' + open(tsc.PLATFORM_CONF_FILE, 'r').read()
|
||||
ini_fp = io.StringIO(ini_str)
|
||||
config.read_file(ini_fp)
|
||||
|
||||
try:
|
||||
value = str(config.get('platform_conf', 'management_interface'))
|
||||
|
||||
mgmt_if = value
|
||||
|
||||
platform_conf_mtime = os.stat(tsc.PLATFORM_CONF_FILE).st_mtime
|
||||
except configparser.Error:
|
||||
logging.exception("Failed to read management_interface from config")
|
||||
return None
|
||||
return mgmt_if
|
||||
|
@ -18,7 +18,7 @@ import time
|
||||
from software import ostree_utils
|
||||
from software.software_functions import configure_logging
|
||||
from software.software_functions import LOG
|
||||
import software.software_config as cfg
|
||||
import software.config as cfg
|
||||
from software.base import PatchService
|
||||
from software.exceptions import OSTreeCommandFail
|
||||
import software.utils as utils
|
||||
|
@ -1,124 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2023 Wind River Systems, Inc.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
"""
|
||||
import configparser
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
|
||||
import tsconfig.tsconfig as tsc
|
||||
|
||||
import software.utils as utils
|
||||
import software.constants as constants
|
||||
|
||||
controller_mcast_group = None
|
||||
agent_mcast_group = None
|
||||
controller_port = 0
|
||||
agent_port = 0
|
||||
api_port = 0
|
||||
mgmt_if = None
|
||||
nodetype = None
|
||||
platform_conf_mtime = 0
|
||||
software_conf_mtime = 0
|
||||
software_conf = '/etc/software/software.conf'
|
||||
|
||||
|
||||
def read_config():
|
||||
global software_conf_mtime
|
||||
global software_conf
|
||||
|
||||
if software_conf_mtime == os.stat(software_conf).st_mtime:
|
||||
# The file has not changed since it was last read
|
||||
return
|
||||
|
||||
defaults = {
|
||||
'controller_mcast_group': "239.1.1.3",
|
||||
'agent_mcast_group': "239.1.1.4",
|
||||
'api_port': "5493",
|
||||
'controller_port': "5494",
|
||||
'agent_port': "5495",
|
||||
}
|
||||
|
||||
global controller_mcast_group
|
||||
global agent_mcast_group
|
||||
global api_port
|
||||
global controller_port
|
||||
global agent_port
|
||||
|
||||
config = configparser.ConfigParser(defaults)
|
||||
|
||||
config.read(software_conf)
|
||||
software_conf_mtime = os.stat(software_conf).st_mtime
|
||||
|
||||
controller_mcast_group = config.get('runtime',
|
||||
'controller_multicast')
|
||||
agent_mcast_group = config.get('runtime', 'agent_multicast')
|
||||
|
||||
api_port = config.getint('runtime', 'api_port')
|
||||
controller_port = config.getint('runtime', 'controller_port')
|
||||
agent_port = config.getint('runtime', 'agent_port')
|
||||
|
||||
# The platform.conf file has no section headers, which causes problems
|
||||
# for ConfigParser. So we'll fake it out.
|
||||
ini_str = '[platform_conf]\n' + open(tsc.PLATFORM_CONF_FILE, 'r').read()
|
||||
ini_fp = io.StringIO(ini_str)
|
||||
config.read_file(ini_fp)
|
||||
|
||||
try:
|
||||
value = str(config.get('platform_conf', 'nodetype'))
|
||||
|
||||
global nodetype
|
||||
nodetype = value
|
||||
except configparser.Error:
|
||||
logging.exception("Failed to read nodetype from config")
|
||||
|
||||
|
||||
def get_mgmt_ip():
|
||||
# Check if initial config is complete
|
||||
if not os.path.exists('/etc/platform/.initial_config_complete'):
|
||||
return None
|
||||
mgmt_hostname = socket.gethostname()
|
||||
return utils.gethostbyname(mgmt_hostname)
|
||||
|
||||
|
||||
# Because the software daemons are launched before manifests are
|
||||
# applied, the content of some settings in platform.conf can change,
|
||||
# such as the management interface. As such, we can't just directly
|
||||
# use tsc.management_interface
|
||||
#
|
||||
def get_mgmt_iface():
|
||||
# Check if initial config is complete
|
||||
if not os.path.exists(constants.INITIAL_CONFIG_COMPLETE_FLAG):
|
||||
return None
|
||||
|
||||
global mgmt_if
|
||||
global platform_conf_mtime
|
||||
|
||||
if mgmt_if is not None and \
|
||||
platform_conf_mtime == os.stat(tsc.PLATFORM_CONF_FILE).st_mtime:
|
||||
# The platform.conf file hasn't been modified since we read it,
|
||||
# so return the cached value.
|
||||
return mgmt_if
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
|
||||
# The platform.conf file has no section headers, which causes problems
|
||||
# for ConfigParser. So we'll fake it out.
|
||||
ini_str = '[platform_conf]\n' + open(tsc.PLATFORM_CONF_FILE, 'r').read()
|
||||
ini_fp = io.StringIO(ini_str)
|
||||
config.read_file(ini_fp)
|
||||
|
||||
try:
|
||||
value = str(config.get('platform_conf', 'management_interface'))
|
||||
|
||||
mgmt_if = value
|
||||
|
||||
platform_conf_mtime = os.stat(tsc.PLATFORM_CONF_FILE).st_mtime
|
||||
except configparser.Error:
|
||||
logging.exception("Failed to read management_interface from config")
|
||||
return None
|
||||
return mgmt_if
|
@ -57,7 +57,7 @@ from software.software_functions import patch_dir
|
||||
from software.software_functions import repo_root_dir
|
||||
from software.software_functions import PatchData
|
||||
|
||||
import software.software_config as cfg
|
||||
import software.config as cfg
|
||||
import software.utils as utils
|
||||
|
||||
import software.messages as messages
|
||||
|
@ -1,33 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
"""Unit tests for software.cmd.api"""
|
||||
|
||||
# standard imports
|
||||
import logging
|
||||
from unittest import mock
|
||||
from wsgiref.simple_server import WSGIServer
|
||||
|
||||
# third-party libraries
|
||||
from oslo_log import fixture as log_fixture
|
||||
import testtools
|
||||
|
||||
# local imports
|
||||
from software.cmd import api
|
||||
|
||||
|
||||
class SoftwareCmdAPITestCase(testtools.TestCase):
|
||||
"""Unit tests for software.cmd.api"""
|
||||
|
||||
@mock.patch.object(WSGIServer, 'handle_request')
|
||||
def test_main(self, mock_handle_request):
|
||||
"""Test main method"""
|
||||
# Info and Warning logs are expected for this unit test.
|
||||
# 'ERROR' logs are not expected.
|
||||
self.useFixture(
|
||||
log_fixture.SetLogLevel(['software'], logging.ERROR)
|
||||
)
|
||||
mock_handle_request.side_effect = KeyboardInterrupt
|
||||
api.main()
|
@ -1,24 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
"""Unit tests for shell.py"""
|
||||
|
||||
# standard imports
|
||||
from unittest import mock
|
||||
|
||||
# third party imports
|
||||
import testtools
|
||||
|
||||
# local imports
|
||||
from software.cmd import shell
|
||||
|
||||
|
||||
class SoftwareShellTestCase(testtools.TestCase):
|
||||
"""Unit tests for shell"""
|
||||
|
||||
@mock.patch('sys.argv', [''])
|
||||
def test_no_args(self):
|
||||
"""Test main method with no args"""
|
||||
shell.main()
|
155
software/software/tests/test_software_client.py
Normal file
155
software/software/tests/test_software_client.py
Normal file
@ -0,0 +1,155 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import testtools
|
||||
from unittest import mock
|
||||
|
||||
from software import software_client
|
||||
|
||||
|
||||
API_PORT = "5493"
|
||||
URL_PREFIX = "http://127.0.0.1:" + API_PORT + "/software"
|
||||
|
||||
FAKE_SW_VERSION = "1.2.3"
|
||||
PATCH_FLAG_NO = "N"
|
||||
PATCH_FLAG_YES = "Y"
|
||||
STATE_APPLIED = "Applied"
|
||||
STATE_AVAILABLE = "Available"
|
||||
STATE_NA = "n/a"
|
||||
STATUS_DEV = "DEV"
|
||||
|
||||
FAKE_PATCH_ID_1 = "PATCH_1"
|
||||
FAKE_PATCH_1_META = {
|
||||
"apply_active_release_only": "",
|
||||
"description": "Patch 1 description",
|
||||
"install_instructions": "Patch 1 instructions",
|
||||
"patchstate": STATE_NA,
|
||||
"reboot_required": PATCH_FLAG_YES,
|
||||
"repostate": STATE_APPLIED,
|
||||
"requires": [],
|
||||
"status": STATUS_DEV,
|
||||
"summary": "Patch 1 summary",
|
||||
"sw_version": FAKE_SW_VERSION,
|
||||
"unremovable": PATCH_FLAG_NO,
|
||||
"warnings": "Patch 1 warnings",
|
||||
}
|
||||
|
||||
FAKE_PATCH_ID_2 = "PATCH_2"
|
||||
FAKE_PATCH_2_META = {
|
||||
"apply_active_release_only": "",
|
||||
"description": "Patch 2 description",
|
||||
"install_instructions": "Patch 2 instructions",
|
||||
"patchstate": STATE_AVAILABLE,
|
||||
"reboot_required": PATCH_FLAG_NO,
|
||||
"repostate": STATE_AVAILABLE,
|
||||
"requires": [FAKE_PATCH_ID_1],
|
||||
"status": STATUS_DEV,
|
||||
"summary": "Patch 2 summary",
|
||||
"sw_version": FAKE_SW_VERSION,
|
||||
"unremovable": PATCH_FLAG_NO,
|
||||
"warnings": "Patch 2 warnings",
|
||||
}
|
||||
|
||||
|
||||
class FakeResponse(object):
|
||||
"""This is used to mock a requests.get result"""
|
||||
def __init__(self, json_data, status_code):
|
||||
self.json_data = json_data
|
||||
self.status_code = status_code
|
||||
self.text = json.dumps(json_data)
|
||||
|
||||
def json(self):
|
||||
return self.json_data
|
||||
|
||||
|
||||
class SoftwareClientTestCase(testtools.TestCase):
|
||||
PROG = "software"
|
||||
|
||||
MOCK_ENV = {
|
||||
'OS_AUTH_URL': 'FAKE_OS_AUTH_URL',
|
||||
'OS_PROJECT_NAME': 'FAKE_OS_PROJECT_NAME',
|
||||
'OS_PROJECT_DOMAIN_NAME': 'FAKE_OS_PROJECT_DOMAIN_NAME',
|
||||
'OS_USERNAME': 'FAKE_OS_USERNAME',
|
||||
'OS_PASSWORD': 'FAKE_OS_PASSWORD',
|
||||
'OS_USER_DOMAIN_NAME': 'FAKE_OS_USER_DOMAIN_NAME',
|
||||
'OS_REGION_NAME': 'FAKE_OS_REGION_NAME',
|
||||
'OS_INTERFACE': 'FAKE_OS_INTERFACE'
|
||||
}
|
||||
|
||||
# mock_map is populated by the setUp method
|
||||
mock_map = {}
|
||||
|
||||
def setUp(self):
|
||||
super(SoftwareClientTestCase, self).setUp()
|
||||
|
||||
def _mock_requests_get(*args, **kwargs):
|
||||
key = args[0]
|
||||
_ = kwargs # kwargs is unused
|
||||
# if the key is not found in the mock_map
|
||||
# we return a 404 (not found)
|
||||
return self.mock_map.get(key,
|
||||
FakeResponse(None, 404))
|
||||
|
||||
patcher = mock.patch(
|
||||
'requests.get',
|
||||
side_effect=_mock_requests_get)
|
||||
self.mock_requests_get = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
|
||||
class SoftwareClientNonRootMixin(object):
|
||||
"""
|
||||
This Mixin Requires self.MOCK_ENV
|
||||
|
||||
Disable printing to stdout
|
||||
|
||||
Every client call invokes exit which raises SystemExit
|
||||
This asserts that happens.
|
||||
"""
|
||||
|
||||
def _test_method(self, shell_args=None):
|
||||
with mock.patch.dict(os.environ, self.MOCK_ENV):
|
||||
with mock.patch.object(sys, 'argv', shell_args):
|
||||
# mock 'print' so running unit tests will
|
||||
# not print to the tox output
|
||||
with mock.patch('builtins.print'):
|
||||
# Every client invocation invokes exit
|
||||
# which raises SystemExit
|
||||
self.assertRaises(SystemExit,
|
||||
software_client.main)
|
||||
|
||||
|
||||
class SoftwareClientHelpTestCase(SoftwareClientTestCase, SoftwareClientNonRootMixin):
|
||||
"""Test the sw-patch CLI calls that invoke 'help'
|
||||
|
||||
'check_for_os_region_name' is mocked to help determine
|
||||
which code path is used since many code paths can short
|
||||
circuit and invoke 'help' in failure cases.
|
||||
"""
|
||||
|
||||
@mock.patch('software.software_client.check_for_os_region_name')
|
||||
def test_main_no_args_calls_help(self, mock_check):
|
||||
"""When no arguments are called, this should invoke print_help"""
|
||||
shell_args = [self.PROG, ]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_check.assert_not_called()
|
||||
|
||||
@mock.patch('software.software_client.check_for_os_region_name')
|
||||
def test_main_help(self, mock_check):
|
||||
"""When no arguments are called, this should invoke print_help"""
|
||||
shell_args = [self.PROG, "--help"]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_check.assert_called()
|
||||
|
||||
@mock.patch('software.software_client.check_for_os_region_name')
|
||||
def test_main_invalid_action_calls_help(self, mock_check):
|
||||
"""invalid args should invoke print_help"""
|
||||
shell_args = [self.PROG, "invalid_arg"]
|
||||
self._test_method(shell_args=shell_args)
|
||||
mock_check.assert_called()
|
161
software/software/tests/test_software_controller_messages.py
Normal file
161
software/software/tests/test_software_controller_messages.py
Normal file
@ -0,0 +1,161 @@
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (c) 2023 Wind River Systems, Inc.
|
||||
#
|
||||
import testtools
|
||||
from unittest import mock
|
||||
|
||||
from software.messages import PatchMessage
|
||||
from software.software_controller import PatchMessageHello
|
||||
from software.software_controller import PatchMessageHelloAck
|
||||
from software.software_controller import PatchMessageSyncReq
|
||||
from software.software_controller import PatchMessageSyncComplete
|
||||
from software.software_controller import PatchMessageHelloAgent
|
||||
from software.software_controller import PatchMessageSendLatestFeedCommit
|
||||
from software.software_controller import PatchMessageHelloAgentAck
|
||||
from software.software_controller import PatchMessageQueryDetailed
|
||||
from software.software_controller import PatchMessageQueryDetailedResp
|
||||
from software.software_controller import PatchMessageAgentInstallReq
|
||||
from software.software_controller import PatchMessageAgentInstallResp
|
||||
from software.software_controller import PatchMessageDropHostReq
|
||||
|
||||
|
||||
FAKE_AGENT_ADDRESS = "127.0.0.1"
|
||||
FAKE_AGENT_MCAST_GROUP = "239.1.1.4"
|
||||
FAKE_CONTROLLER_ADDRESS = "127.0.0.1"
|
||||
FAKE_HOST_IP = "10.10.10.2"
|
||||
FAKE_OSTREE_FEED_COMMIT = "12345"
|
||||
|
||||
|
||||
class FakeSoftwareController(object):
|
||||
|
||||
def __init__(self):
|
||||
self.agent_address = FAKE_AGENT_ADDRESS
|
||||
self.allow_insvc_softwareing = True
|
||||
self.controller_address = FAKE_CONTROLLER_ADDRESS
|
||||
self.controller_neighbours = {}
|
||||
self.hosts = {}
|
||||
self.interim_state = {}
|
||||
self.latest_feed_commit = FAKE_OSTREE_FEED_COMMIT
|
||||
self.patch_op_counter = 0
|
||||
self.sock_in = None
|
||||
self.sock_out = None
|
||||
|
||||
# mock all the lock objects
|
||||
self.controller_neighbours_lock = mock.Mock()
|
||||
self.hosts_lock = mock.Mock()
|
||||
self.software_data_lock = mock.Mock()
|
||||
self.socket_lock = mock.Mock()
|
||||
|
||||
# mock the software data
|
||||
self.base_pkgdata = mock.Mock()
|
||||
self.software_data = mock.Mock()
|
||||
|
||||
def check_patch_states(self):
|
||||
pass
|
||||
|
||||
def drop_host(self, host_ip, sync_nbr=True):
|
||||
pass
|
||||
|
||||
def sync_from_nbr(self, host):
|
||||
pass
|
||||
|
||||
|
||||
class SoftwareControllerMessagesTestCase(testtools.TestCase):
|
||||
|
||||
message_classes = [
|
||||
PatchMessageHello,
|
||||
PatchMessageHelloAck,
|
||||
PatchMessageSyncReq,
|
||||
PatchMessageSyncComplete,
|
||||
PatchMessageHelloAgent,
|
||||
PatchMessageSendLatestFeedCommit,
|
||||
PatchMessageHelloAgentAck,
|
||||
PatchMessageQueryDetailed,
|
||||
PatchMessageQueryDetailedResp,
|
||||
PatchMessageAgentInstallReq,
|
||||
PatchMessageAgentInstallResp,
|
||||
PatchMessageDropHostReq,
|
||||
]
|
||||
|
||||
def test_message_class_creation(self):
|
||||
for message_class in SoftwareControllerMessagesTestCase.message_classes:
|
||||
test_obj = message_class()
|
||||
self.assertIsNotNone(test_obj)
|
||||
self.assertIsInstance(test_obj, PatchMessage)
|
||||
|
||||
@mock.patch('software.software_controller.pc', FakeSoftwareController())
|
||||
def test_message_class_encode(self):
|
||||
"""'encode' method populates self.message"""
|
||||
# mock the global software_controller 'pc' variable used by encode
|
||||
|
||||
# PatchMessageQueryDetailedResp does not support 'encode'
|
||||
# so it can be executed, but it will not change the message
|
||||
excluded = [
|
||||
PatchMessageQueryDetailedResp
|
||||
]
|
||||
for message_class in SoftwareControllerMessagesTestCase.message_classes:
|
||||
test_obj = message_class()
|
||||
# message variable should be empty dict (ie: False)
|
||||
self.assertFalse(test_obj.message)
|
||||
test_obj.encode()
|
||||
# message variable no longer empty (ie: True)
|
||||
if message_class not in excluded:
|
||||
self.assertTrue(test_obj.message)
|
||||
# decode one message into another
|
||||
test_obj2 = message_class()
|
||||
test_obj2.decode(test_obj.message)
|
||||
# decode does not populate 'message' so nothing to compare
|
||||
|
||||
@mock.patch('software.software_controller.pc', FakeSoftwareController())
|
||||
@mock.patch('software.config.agent_mcast_group', FAKE_AGENT_MCAST_GROUP)
|
||||
def test_message_class_send(self):
|
||||
"""'send' writes to a socket"""
|
||||
mock_sock = mock.Mock()
|
||||
|
||||
# socket sendto and sendall are not called by:
|
||||
# PatchMessageHelloAgentAck
|
||||
# PatchMessageQueryDetailedResp
|
||||
# PatchMessageAgentInstallResp,
|
||||
|
||||
send_to = [
|
||||
PatchMessageHello,
|
||||
PatchMessageHelloAck,
|
||||
PatchMessageSyncReq,
|
||||
PatchMessageSyncComplete,
|
||||
PatchMessageHelloAgent,
|
||||
PatchMessageSendLatestFeedCommit,
|
||||
PatchMessageAgentInstallReq,
|
||||
PatchMessageDropHostReq,
|
||||
]
|
||||
send_all = [
|
||||
PatchMessageQueryDetailed,
|
||||
]
|
||||
|
||||
for message_class in SoftwareControllerMessagesTestCase.message_classes:
|
||||
mock_sock.reset_mock()
|
||||
test_obj = message_class()
|
||||
test_obj.send(mock_sock)
|
||||
if message_class in send_to:
|
||||
mock_sock.sendto.assert_called()
|
||||
if message_class in send_all:
|
||||
mock_sock.sendall.assert_called()
|
||||
|
||||
@mock.patch('software.software_controller.pc', FakeSoftwareController())
|
||||
def test_message_class_handle(self):
|
||||
"""'handle' method tests"""
|
||||
addr = [FAKE_CONTROLLER_ADDRESS, ] # addr is a list
|
||||
mock_sock = mock.Mock()
|
||||
special_setup = {
|
||||
PatchMessageDropHostReq: ('ip', FAKE_HOST_IP),
|
||||
}
|
||||
|
||||
for message_class in SoftwareControllerMessagesTestCase.message_classes:
|
||||
mock_sock.reset_mock()
|
||||
test_obj = message_class()
|
||||
# some classes require special setup
|
||||
special = special_setup.get(message_class)
|
||||
if special:
|
||||
setattr(test_obj, special[0], special[1])
|
||||
test_obj.handle(mock_sock, addr)
|
Loading…
Reference in New Issue
Block a user