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 time
|
||||||
|
|
||||||
import software.utils as utils
|
import software.utils as utils
|
||||||
import software.software_config as cfg
|
import software.config as cfg
|
||||||
import software.constants as constants
|
import software.constants as constants
|
||||||
from software.software_functions import LOG
|
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
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import configparser
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
|
||||||
from oslo_config import cfg
|
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
|
# setup a shareable config
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -41,3 +62,100 @@ pecan_opts = [
|
|||||||
|
|
||||||
# register the configuration for this component
|
# register the configuration for this component
|
||||||
CONF.register_opts(pecan_opts, group=PECAN_CONFIG_GROUP)
|
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 import ostree_utils
|
||||||
from software.software_functions import configure_logging
|
from software.software_functions import configure_logging
|
||||||
from software.software_functions import LOG
|
from software.software_functions import LOG
|
||||||
import software.software_config as cfg
|
import software.config as cfg
|
||||||
from software.base import PatchService
|
from software.base import PatchService
|
||||||
from software.exceptions import OSTreeCommandFail
|
from software.exceptions import OSTreeCommandFail
|
||||||
import software.utils as utils
|
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 repo_root_dir
|
||||||
from software.software_functions import PatchData
|
from software.software_functions import PatchData
|
||||||
|
|
||||||
import software.software_config as cfg
|
import software.config as cfg
|
||||||
import software.utils as utils
|
import software.utils as utils
|
||||||
|
|
||||||
import software.messages as messages
|
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…
x
Reference in New Issue
Block a user