Repository: DevOps-MBSE/AaC (language: python)

Issue:
📋 [TASK] - Improve Tests in Execute Unit Tests
### Parent Feature

#844

### Task Description

As an AaC Test Developer, I want to expand the test coverage in my unit tests, so that I can have more confidence in the robustness of my code.

### Acceptance Criteria

- [ ] Update test logic as necessary to expand test coverage. 
  - [ ] Happy path and sad path at a minimum
- [ ] (Stretch) Incorporate a behave feature file for testing if applicable.
- [ ] Run `flake8` on the targeted test directory and address findings.
- [ ] Document any identified gaps.

### Unresolved questions

_No response_


Discussion / hints:

Fix patch:
diff --git a/python/src/aac/__init__.py b/python/src/aac/__init__.py
index 58a313515..748778ff5 100644
--- a/python/src/aac/__init__.py
+++ b/python/src/aac/__init__.py
@@ -12,7 +12,7 @@
 import logging
 import os
 
-__version__ = "0.4.31"
+__version__ = "0.4.32"
 
 __log_file_name__ = os.path.join(os.path.dirname(__file__), "aac.log")
 
diff --git a/python/src/aac/execute/aac_execution_result.py b/python/src/aac/execute/aac_execution_result.py
index 35ac0a22e..2a4f1e674 100644
--- a/python/src/aac/execute/aac_execution_result.py
+++ b/python/src/aac/execute/aac_execution_result.py
@@ -73,10 +73,15 @@ class ExecutionResult:
 
     def add_message(self, message: ExecutionMessage) -> None:
         """Add a message to the list of messages."""
+        if type(message) is not ExecutionMessage:
+            raise TypeError("Message should be of class ExecutionMessage")
         self.messages.append(message)
 
     def add_messages(self, messages: list[ExecutionMessage]) -> None:
         """Add messages to the list of messages."""
+        for message in messages:
+            if type(message) is not ExecutionMessage:
+                raise TypeError("Message should be of class ExecutionMessage")
         self.messages.extend(messages)
 
     def is_success(self) -> bool:
diff --git a/python/src/aac/execute/plugin_runner.py b/python/src/aac/execute/plugin_runner.py
index 1f9911aba..2cee2d462 100644
--- a/python/src/aac/execute/plugin_runner.py
+++ b/python/src/aac/execute/plugin_runner.py
@@ -79,14 +79,22 @@ def add_command_callback(
         self, command_name: str, command_callback: Callable
     ) -> None:
         """Add a command callback to the plugin runner."""
+        if not callable(command_callback):
+            raise TypeError("Command Callback is not Callable")
         self.command_to_callback[command_name] = command_callback
 
     def add_constraint_callback(
         self, constraint_name: str, constraint_callback: Callable
     ) -> None:
         """Add a constraint callback to the plugin runner."""
+        if not callable(constraint_callback):
+            raise TypeError("Constraint Callback is not Callable")
         self.constraint_to_callback[constraint_name] = constraint_callback
 
+    def get_command_callback(self, command_name: str) -> Callable:
+        """Return the callback for the given constraint name."""
+        return self.command_to_callback[command_name]
+
     def get_constraint_callback(self, constraint_name: str) -> Callable:
         """Return the callback for the given constraint name."""
         return self.constraint_to_callback[constraint_name]


Test patch:
diff --git a/python/tests/test_aac/execute/my_plugin.aac b/python/tests/test_aac/execute/my_plugin.aac
new file mode 100644
index 000000000..8a53e3f54
--- /dev/null
+++ b/python/tests/test_aac/execute/my_plugin.aac
@@ -0,0 +1,116 @@
+plugin:
+  name: My plugin
+  package: happy
+  description: |
+    An AaC plugin definition used to test the gen-plugin command behavior.
+  commands:
+    - name: test-command-one
+      help_text: |
+        A test command used to test the gen-plugin command behavior.
+      run_before:
+        - plugin: Check AaC
+          command: check
+      run_after:
+        - plugin: Generate
+          command: generate
+      input:
+        - name: aac-plugin-file
+          type: file
+          description: The path to the architecture file with the plugin definition.
+      acceptance:
+        - name: Command test
+          scenarios:
+            - name: Simple Command Test
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+    - name: test-command-two
+      help_text: |
+        A second test command used to test the gen-plugin command behavior.
+      run_before:
+        - plugin: Check AaC
+          command: check
+      run_after:
+        - plugin: Generate
+          command: generate
+      input:
+        - name: aac-plugin-file
+          type: file
+          description: The path to the architecture file with the plugin definition.
+      acceptance:
+        - name: Command test two
+          scenarios:
+            - name: Simple Command Test Two
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+    - name: test-command-three
+      help_text: |
+        A third test command used to test the gen-plugin command behavior.
+      run_before:
+        - plugin: Check AaC
+          command: check
+      run_after:
+        - plugin: Generate
+          command: generate
+      input:
+        - name: aac-plugin-file
+          type: file
+          description: The path to the architecture file with the plugin definition.
+      acceptance:
+        - name: Command test three
+          scenarios:
+            - name: Simple Command Test Three
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+  context_constraints:
+    - name: The sky is blue
+      description: Ensures the sky is blue.
+      acceptance:
+        - name: Context test
+          scenarios:
+            - name: Simple Constraint Test
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+  schema_constraints:
+    - name: Schemas have happy names
+      description: Check every schema declared provides positive vibes.
+      universal: true
+      acceptance:
+        - name: Schema test
+          scenarios:
+            - name: Simple Schema Test
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+  primitive_constraints:
+    - name: Primitives have happy names
+      description: Check every primitive declared provides positive vibes.
+      acceptance:
+        - name: Primitive test
+          scenarios:
+            - name: Simple Primitive Test
+              given:
+                - An AaC file containing schemas with no extra fields.
+              when:
+                - The AaC check command is run on the schema.
+              then:
+                - The check commands provides the output 'All AaC constraint checks were successful.'
+
diff --git a/python/tests/test_aac/execute/test_aac_execution_result.py b/python/tests/test_aac/execute/test_aac_execution_result.py
new file mode 100644
index 000000000..95755fa28
--- /dev/null
+++ b/python/tests/test_aac/execute/test_aac_execution_result.py
@@ -0,0 +1,84 @@
+from unittest import TestCase
+from aac.execute.aac_execution_result import ExecutionResult, ExecutionMessage, ExecutionStatus, MessageLevel
+
+
+class TestExecutionResult(TestCase):
+
+    def test_execution_result_name(self):
+        execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+        self.assertEqual(execution_result.plugin_name, "name")
+        self.assertEqual(execution_result.plugin_command_name, "command_name")
+
+    def test_execution_result_messages(self):
+        execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+        execution_result.add_messages([
+            ExecutionMessage("message 1", MessageLevel.INFO, None, None),
+            ExecutionMessage("message 2", MessageLevel.INFO, None, None),
+            ExecutionMessage("message 3", MessageLevel.INFO, None, None),
+            ExecutionMessage("message 4", MessageLevel.INFO, None, None)
+        ])
+
+        messages = execution_result.get_messages_as_string()
+        self.assertEqual(messages, "message 1\nmessage 2\nmessage 3\nmessage 4\n")
+
+    def test_execution_result_message_levels(self):
+        execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+        execution_result.add_messages([
+            ExecutionMessage("message 1", MessageLevel.DEBUG, None, None),
+            ExecutionMessage("message 2", MessageLevel.INFO, None, None),
+            ExecutionMessage("message 3", MessageLevel.WARNING, None, None),
+            ExecutionMessage("message 4", MessageLevel.ERROR, None, None)
+        ])
+        messages_list = execution_result.messages
+        self.assertEqual(messages_list[0].level, MessageLevel.DEBUG)
+        self.assertEqual(messages_list[1].level, MessageLevel.INFO)
+        self.assertEqual(messages_list[2].level, MessageLevel.WARNING)
+        self.assertEqual(messages_list[3].level, MessageLevel.ERROR)
+
+    def test_execution_result_status_code(self):
+        execution_result = ExecutionResult("name", "command_name", ExecutionStatus.GENERAL_FAILURE, [])
+        self.assertFalse(execution_result.is_success())
+        execution_result.status_code = ExecutionStatus.SUCCESS
+        self.assertTrue(execution_result.is_success())
+
+    def test_execution_result_status_enum(self):
+        execution_success = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+        execution_constraint_failure = ExecutionResult("name", "command_name", ExecutionStatus.CONSTRAINT_FAILURE, [])
+        execution_constraint_warning = ExecutionResult("name", "command_name", ExecutionStatus.CONSTRAINT_WARNING, [])
+        execution_parser_failure = ExecutionResult("name", "command_name", ExecutionStatus.PARSER_FAILURE, [])
+        execution_plugin_failure = ExecutionResult("name", "command_name", ExecutionStatus.PLUGIN_FAILURE, [])
+        execution_operation_cancelled = ExecutionResult("name", "command_name", ExecutionStatus.OPERATION_CANCELLED, [])
+        execution_general_failure = ExecutionResult("name", "command_name", ExecutionStatus.GENERAL_FAILURE, [])
+
+        self.assertEqual(execution_success.status_code, ExecutionStatus.SUCCESS)
+        self.assertEqual(execution_constraint_failure.status_code, ExecutionStatus.CONSTRAINT_FAILURE)
+        self.assertEqual(execution_constraint_warning.status_code, ExecutionStatus.CONSTRAINT_WARNING)
+        self.assertEqual(execution_parser_failure.status_code, ExecutionStatus.PARSER_FAILURE)
+        self.assertEqual(execution_plugin_failure.status_code, ExecutionStatus.PLUGIN_FAILURE)
+        self.assertEqual(execution_operation_cancelled.status_code, ExecutionStatus.OPERATION_CANCELLED)
+        self.assertEqual(execution_general_failure.status_code, ExecutionStatus.GENERAL_FAILURE)
+
+    def test_execution_result_message_levels_invalid(self):
+        with self.assertRaises(TypeError):
+            execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+            execution_result.add_message("message", "INFO", None, None)
+        with self.assertRaises(AttributeError):
+            execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+            execution_result.add_message("message", MessageLevel.info, None, None)
+
+    def test_execution_result_invalid_name(self):
+        with self.assertRaises(TypeError):
+            execution_result = ExecutionResult(123, "command_name", ExecutionStatus.GENERAL_FAILURE, [])  # noqa: F841
+        with self.assertRaises(TypeError):
+            execution_result = ExecutionResult("name", 456, ExecutionStatus.GENERAL_FAILURE, [])  # noqa: F841
+
+    def test_execution_result_invalid_status(self):
+        with self.assertRaises(AttributeError):
+            execution_result = ExecutionResult("name", "command_name", ExecutionStatus.general_failure, [])  # noqa: F841
+        with self.assertRaises(TypeError):
+            execution_result = ExecutionResult("name", "command_name", "general_failure", [])  # noqa: F841
+
+    def test_execution_result_no_message(self):
+        with self.assertRaises(TypeError):
+            execution_result = ExecutionResult("name", "command_name", ExecutionStatus.SUCCESS, [])
+            execution_result.add_message("message")
diff --git a/python/tests/test_aac/execute/test_plugin_manager.py b/python/tests/test_aac/execute/test_plugin_manager.py
new file mode 100644
index 000000000..668bf0b0d
--- /dev/null
+++ b/python/tests/test_aac/execute/test_plugin_manager.py
@@ -0,0 +1,21 @@
+from unittest import TestCase
+
+from aac.execute.plugin_manager import get_plugin_manager
+
+
+class TestPluginManager(TestCase):
+    def test_plugin_manager(self):
+        plugin_manager = get_plugin_manager()
+        plugins = plugin_manager.get_plugins()
+        plugin = next(iter(plugins))
+        self.assertTrue(str(plugin), ''.join(str(plugins)))  # noqa: A504
+
+        plugin_manager.unregister(plugin)
+        plugins = plugin_manager.get_plugins()
+        self.assertNotIn(str(plugin), ''.join(str(plugins)))  # noqa: A504
+
+        plugin_manager.register(plugin)
+        plugins = plugin_manager.get_plugins()
+        self.assertTrue(str(plugin), ''.join(str(plugins)))  # noqa: A504
+
+# More investigations for allowing a fail test with pluggy required.  It currently accepts any type, so a fail test is not clear.
diff --git a/python/tests/test_aac/execute/test_plugin_runner.py b/python/tests/test_aac/execute/test_plugin_runner.py
index 8b236a7d3..87234a228 100644
--- a/python/tests/test_aac/execute/test_plugin_runner.py
+++ b/python/tests/test_aac/execute/test_plugin_runner.py
@@ -1,13 +1,17 @@
-import unittest
-from aac.execute.plugin_runner import AacCommandArgument, AacCommand
+from os.path import join, dirname
+from unittest import TestCase
 
-class TestAacCommandArgument(unittest.TestCase):
+from aac.context.language_context import LanguageContext
+from aac.execute.plugin_runner import AacCommandArgument, AacCommand, PluginRunner
+
+
+class TestAacCommandArgument(TestCase):
     def test_init(self):
         arg = AacCommandArgument(name="test", description="test description", data_type="str")
         self.assertEqual(arg.name, "test")
         self.assertEqual(arg.description, "test description")
         self.assertEqual(arg.data_type, "str")
-        self.assertEqual(arg.default, None)
+        self.assertIsNone(arg.default)
 
     def test_invalid_name(self):
         with self.assertRaises(TypeError):
@@ -21,12 +25,13 @@ def test_invalid_data_type(self):
         with self.assertRaises(TypeError):
             AacCommandArgument(name="test", description="test description", data_type=123)
 
-class TestAacCommand(unittest.TestCase):
+
+class TestAacCommand(TestCase):
     def test_init(self):
         arg = AacCommandArgument(name="test", description="test description", data_type="str")
-        callback = lambda: "Hello, World!"
+        callback = hello_world
         command = AacCommand(name="test_command", description="test command description", callback=callback, arguments=[arg])
-        
+
         self.assertEqual(command.name, "test_command")
         self.assertEqual(command.description, "test command description")
         self.assertEqual(command.callback(), "Hello, World!")
@@ -35,11 +40,11 @@ def test_init(self):
 
     def test_invalid_name(self):
         with self.assertRaises(TypeError):
-            AacCommand(name=123, description="test description", callback=lambda: None, arguments=[])
+            AacCommand(name=123, description="test description", callback=none_return, arguments=[])
 
     def test_invalid_description(self):
         with self.assertRaises(TypeError):
-            AacCommand(name="test", description=123, callback=lambda: None, arguments=[])
+            AacCommand(name="test", description=123, callback=none_return, arguments=[])
 
     def test_invalid_callback(self):
         with self.assertRaises(TypeError):
@@ -47,7 +52,62 @@ def test_invalid_callback(self):
 
     def test_invalid_arguments(self):
         with self.assertRaises(TypeError):
-            AacCommand(name="test", description="test description", callback=lambda: None, arguments="not a list")
+            AacCommand(name="test", description="test description", callback=none_return, arguments="not a list")
+
+
+class TestPluginRunner(TestCase):
+    def test_plugin_runner(self):
+        aac_file_path = join(dirname(__file__), "my_plugin.aac")
+        context = LanguageContext()
+        definitions = context.parse_and_load(aac_file_path)
+
+        plugin_runner = PluginRunner(definitions[0], {}, {})
+        plugin_runner.add_command_callback("command", command_callback)
+        plugin_runner.add_constraint_callback("constraint", constraint_callback)
+
+        command = plugin_runner.get_command_callback("command")
+        constraint = plugin_runner.get_constraint_callback("constraint")
+        self.assertEqual("Command Callback", command())
+        self.assertEqual("Constraint Callback", constraint())
+        self.assertEqual("My plugin", plugin_runner.get_plugin_name())
+
+    def test_plugin_runner_list_instead_of_dict(self):
+        with self.assertRaises(TypeError):
+            aac_file_path = join(dirname(__file__), "my_plugin.aac")
+            context = LanguageContext()
+            definitions = context.parse_and_load(aac_file_path)
+
+            plugin_runner = PluginRunner(definitions[0], [], [])  # noqa: F841
+
+    def test_plugin_runner_no_callback(self):
+        with self.assertRaises(TypeError):
+            aac_file_path = join(dirname(__file__), "my_plugin.aac")
+            context = LanguageContext()
+            definitions = context.parse_and_load(aac_file_path)
+
+            plugin_runner = PluginRunner(definitions[0], {}, {})
+            plugin_runner.add_command_callback("command", "Command Callback")
+
+        with self.assertRaises(TypeError):
+            aac_file_path = join(dirname(__file__), "my_plugin.aac")
+            context = LanguageContext()
+            definitions = context.parse_and_load(aac_file_path)
+
+            plugin_runner = PluginRunner(definitions[0], {}, {})
+            plugin_runner.add_constraint_callback("constraint", "Constraint Callback")
+
+
+def command_callback():
+    return "Command Callback"
+
+
+def constraint_callback():
+    return "Constraint Callback"
+
+
+def none_return():
+    return None
+
 
-if __name__ == "__main__":
-    unittest.main()
\ No newline at end of file
+def hello_world():
+    return "Hello, World!"