From 7aa45e8ce7f52c0bc83769b08a5f7d01efc99e77 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 13 Dec 2023 17:41:05 -0800 Subject: [PATCH 1/2] Add test coverage for Chatbot init --- chatbot_core/v2/__init__.py | 21 +++++-- tests/units/__init__.py | 18 ++++++ tests/units/mocks.py | 87 +++++++++++++++++++++++++++ tests/units/test_base_classes.py | 100 +++++++++++++++++++++++++++++-- 4 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 tests/units/__init__.py create mode 100644 tests/units/mocks.py diff --git a/chatbot_core/v2/__init__.py b/chatbot_core/v2/__init__.py index b71a820..b20f1ba 100644 --- a/chatbot_core/v2/__init__.py +++ b/chatbot_core/v2/__init__.py @@ -130,11 +130,18 @@ def _on_mentioned_user_message(self, body: dict): """ MQ handler for requesting message for current bot """ - if body.get('cid', None) in list(self.current_conversations) and not body.get('omit_reply', False): - self.handle_incoming_shout(body) - else: - self.log.warning(f'Skipping processing of mentioned user message with data: {body} ' - f'as it is not in current conversations') + if body.get('omit_reply'): + self.log.debug(f"Explicitly requested no response: messageID=" + f"{body.get('messageID')}") + return + if body.get('cid') not in list(self.current_conversations): + self.log.info(f"Ignoring message " + f"(messageID={body.get('messageID')}) outside of " + f"current conversations " + f"({self.current_conversations}") + self.log.debug(f"{body}") + return + self.handle_incoming_shout(body) @create_mq_callback() def _on_user_message(self, body: dict): @@ -432,3 +439,7 @@ def _pause_responses(self, duration: int = 5): def pre_run(self, **kwargs): self._setup_listeners() + + def shutdown(self): + self.shout_thread.cancel() + self.shout_thread.join() diff --git a/tests/units/__init__.py b/tests/units/__init__.py new file mode 100644 index 0000000..97331b9 --- /dev/null +++ b/tests/units/__init__.py @@ -0,0 +1,18 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# +# Copyright 2008-2021 Neongecko.com Inc. | All Rights Reserved +# +# Notice of License - Duplicating this Notice of License near the start of any file containing +# a derivative of this software is a condition of license for this software. +# Friendly Licensing: +# No charge, open source royalty free use of the Neon AI software source and object is offered for +# educational users, noncommercial enthusiasts, Public Benefit Corporations (and LLCs) and +# Social Purpose Corporations (and LLCs). Developers can contact developers@neon.ai +# For commercial licensing, distribution of derivative works or redistribution please contact licenses@neon.ai +# Distributed on an "AS IS” basis without warranties or conditions of any kind, either express or implied. +# Trademarks of Neongecko: Neon AI(TM), Neon Assist (TM), Neon Communicator(TM), Klat(TM) +# Authors: Guy Daniels, Daniel McKnight, Regina Bloomstine, Elon Gasper, Richard Leeds +# +# Specialized conversational reconveyance options from Conversation Processing Intelligence Corp. +# US Patents 2008-2021: US7424516, US20140161250, US20140177813, US8638908, US8068604, US8553852, US10530923, US10530924 +# China Patent: CN102017585 - Europe Patent: EU2156652 - Patents Pending diff --git a/tests/units/mocks.py b/tests/units/mocks.py new file mode 100644 index 0000000..f946dff --- /dev/null +++ b/tests/units/mocks.py @@ -0,0 +1,87 @@ +# NEON AI (TM) SOFTWARE, Software Development Kit & Application Development System +# +# Copyright 2008-2021 Neongecko.com Inc. | All Rights Reserved +# +# Notice of License - Duplicating this Notice of License near the start of any file containing +# a derivative of this software is a condition of license for this software. +# Friendly Licensing: +# No charge, open source royalty free use of the Neon AI software source and object is offered for +# educational users, noncommercial enthusiasts, Public Benefit Corporations (and LLCs) and +# Social Purpose Corporations (and LLCs). Developers can contact developers@neon.ai +# For commercial licensing, distribution of derivative works or redistribution please contact licenses@neon.ai +# Distributed on an "AS IS” basis without warranties or conditions of any kind, either express or implied. +# Trademarks of Neongecko: Neon AI(TM), Neon Assist (TM), Neon Communicator(TM), Klat(TM) +# Authors: Guy Daniels, Daniel McKnight, Regina Bloomstine, Elon Gasper, Richard Leeds +# +# Specialized conversational reconveyance options from Conversation Processing Intelligence Corp. +# US Patents 2008-2021: US7424516, US20140161250, US20140177813, US8638908, US8068604, US8553852, US10530923, US10530924 +# China Patent: CN102017585 - Europe Patent: EU2156652 - Patents Pending +from typing import Optional +from unittest.mock import MagicMock +from chatbot_core.chatbot_abc import ChatBotABC + + +class MockMQ(MagicMock): + def __init__(self, config, service_name, vhost): + self.service_name = service_name + self.config = config + self.vhost = vhost + self.current_conversations = set() + self.is_running = True + self._service_id = "test_id" + + self.vhost_prefix = None + self.testing_envs = set() + + +class TestBot(ChatBotABC): + def on_vote(self, prompt_id: str, selected: str, voter: str): + pass + + def on_discussion(self, user: str, shout: str): + pass + + def on_proposed_response(self): + pass + + def on_selection(self, prompt: str, user: str, response: str): + pass + + def on_ready_for_next(self, user: str): + pass + + def at_chatbot(self, user: str, shout: str, timestamp: str) -> str: + pass + + def ask_proctor(self, prompt: str, user: str, cid: str, dom: str): + pass + + def ask_chatbot(self, user: str, shout: str, timestamp: str) -> str: + pass + + def ask_history(self, user: str, shout: str, dom: str, cid: str) -> str: + pass + + def ask_appraiser(self, options: dict) -> str: + pass + + def ask_discusser(self, options: dict) -> str: + pass + + def _send_first_prompt(self): + pass + + def handle_shout(self, *args, **kwargs): + pass + + def vote_response(self, response_user: str, cid: Optional[str] = None): + pass + + def _handle_next_shout(self): + pass + + def _pause_responses(self, duration: int = 5): + pass + + def parse_init(self, *args, **kwargs) -> tuple: + pass diff --git a/tests/units/test_base_classes.py b/tests/units/test_base_classes.py index 17e5265..1c1159f 100644 --- a/tests/units/test_base_classes.py +++ b/tests/units/test_base_classes.py @@ -19,25 +19,115 @@ import unittest +from unittest.mock import patch +from ovos_utils.log import LOG + +from .mocks import MockMQ + class ChatBotV1Tests(unittest.TestCase): - from chatbot_core.v1 import ChatBot + from klat_connector.mach_server import MachKlatServer + from klat_connector import start_socket + server = MachKlatServer() + socket = start_socket(addr="0.0.0.0") + + @patch("chatbot_core.utils.bot_utils.clean_up_bot") + def test_init(self, clean_up): + from chatbot_core.chatbot_abc import ChatBotABC + from chatbot_core.v1 import ChatBot + + domain = "test_domain" + bot_kwargs = ChatBot(socket=self.socket, domain=domain, + username="test", password="pass", on_server=False, + is_prompter=False) + self.assertIsInstance(bot_kwargs, ChatBotABC) + self.assertEqual(bot_kwargs.socket, self.socket) + self.assertEqual(bot_kwargs.start_domain, domain) + self.assertEqual(bot_kwargs.username, "test") + self.assertEqual(bot_kwargs._bot_id, "test") + self.assertEqual(bot_kwargs.password, "pass") + self.assertFalse(bot_kwargs.on_server) + self.assertFalse(bot_kwargs.is_prompter) + self.assertTrue(bot_kwargs.enable_responses) + from chatbot_core.utils.enum import BotTypes + self.assertEqual(bot_kwargs.bot_type, BotTypes.SUBMIND) + self.assertTrue(bot_kwargs.shout_thread.is_alive()) + + bot_kwargs.exit() + self.assertEqual(bot_kwargs.shout_queue.qsize(), 0) + clean_up.assert_called_once_with(bot_kwargs) + + bot_args = ChatBot(self.socket, domain, "test", "") + self.assertIsInstance(bot_args, ChatBotABC) + self.assertEqual(bot_args.socket, self.socket) + self.assertEqual(bot_args.start_domain, domain) + self.assertEqual(bot_args.username, "test") + self.assertEqual(bot_args._bot_id, "test") + self.assertIsNone(bot_args.password) + self.assertTrue(bot_args.on_server) + self.assertFalse(bot_args.is_prompter) + self.assertTrue(bot_args.enable_responses) + from chatbot_core.utils.enum import BotTypes + self.assertEqual(bot_args.bot_type, BotTypes.SUBMIND) + self.assertTrue(bot_args.shout_thread.is_alive()) + + bot_args.exit() + self.assertEqual(bot_args.shout_queue.qsize(), 0) + clean_up.assert_called_with(bot_args) + # TODO class ChatBotV2Tests(unittest.TestCase): - from chatbot_core.v2 import ChatBot + @patch("chatbot_core.v2.KlatAPIMQ", new=MockMQ) + def test_init(self): + from chatbot_core.v2 import ChatBot + config = {"test": True, + "MQ": {"mq_key": "val"}, + "chatbots": {"test_bot": {"bot_config": True}}} + from chatbot_core.utils.enum import BotTypes + bot_kwargs = ChatBot(config=config, service_name="test_bot", + vhost="/test", bot_type=BotTypes.OBSERVER) + self.assertEqual(bot_kwargs.bot_type, BotTypes.OBSERVER) + self.assertEqual(bot_kwargs._bot_id, "test_bot") + self.assertEqual(bot_kwargs.config, config["MQ"]) + self.assertEqual(bot_kwargs.bot_config, config["chatbots"]["test_bot"]) + self.assertEqual(bot_kwargs.vhost, "/test") + self.assertTrue(bot_kwargs.shout_thread.is_alive()) + bot_kwargs.shutdown() + self.assertFalse(bot_kwargs.shout_thread.is_alive()) + + bot_args = ChatBot(config, "args_bot", "/chat") + self.assertEqual(bot_args.bot_type, BotTypes.SUBMIND) + self.assertEqual(bot_args._bot_id, "args_bot") + self.assertEqual(bot_args.config, config["MQ"]) + self.assertEqual(bot_args.bot_config, dict()) + self.assertEqual(bot_args.vhost, "/chat") + self.assertTrue(bot_args.shout_thread.is_alive()) + bot_args.shutdown() + self.assertFalse(bot_args.shout_thread.is_alive()) # TODO class ChatBotABCTests(unittest.TestCase): - from chatbot_core.chatbot_abc import ChatBotABC - # TODO + def test_base_class(self): + from queue import Queue + from chatbot_core.chatbot_abc import ChatBotABC + from .mocks import TestBot + bot_id = "test" + test_config = {"config": True} + bot = TestBot(bot_id, test_config) + self.assertIsInstance(bot, ChatBotABC) + self.assertEqual(bot._bot_id, bot_id) + self.assertEqual(bot.bot_config, test_config) + self.assertIsInstance(bot.shout_queue, Queue) + self.assertEqual(bot.log, LOG) + self.assertEqual(bot.log.name, bot_id) class NeonTests(unittest.TestCase): from chatbot_core.neon import NeonBot - # TODO + # TODO Deprecate class class ParlaiTests(unittest.TestCase): From 95e45b9ca14b72da3bc55b6688740129eb010fa7 Mon Sep 17 00:00:00 2001 From: Daniel McKnight Date: Wed, 13 Dec 2023 17:45:52 -0800 Subject: [PATCH 2/2] Remove Python 3.10-3.11 tests needing klat-connector support --- .github/workflows/unit_tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 010c14d..26ef8a4 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -9,7 +9,8 @@ jobs: unit_tests: strategy: matrix: - python-version: [ 3.7, 3.8, 3.9, '3.10', '3.11'] + python-version: [ 3.7, 3.8, 3.9] + # TODO: 3.10 and 3.11 need support in klat-connector runs-on: ubuntu-latest timeout-minutes: 15 steps: