Repository: OpenAssetIO/OpenAssetIO (language: python)

Issue:
API Compliance Suite support for C++ plugins
# What

Load C++ plugins in the API compliance suite if no Python plugin is found.

# Why

The C++ plugin system has now been added, but the API Compliance Suite needs updating to be able to validate these plugins.

# ACs

- API Compliance Suite test harness attempts to load a Python plugin, and if one is not found it then tries to load a C++ plugin.
- Testing this involves unit tests only (mock the factories).



Discussion / hints:

Fix patch:
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index bea5a48de..9ddffea44 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -15,6 +15,11 @@ v1.0.0-beta.x.y
   a `CppPluginSystemManagerPlugin` object.
   [#1115](https://github.com/OpenAssetIO/OpenAssetIO/issues/1115)
 
+- `openassetio.test.manager`, notably used for Api Compliance tests,
+  updated to automatically load C++ plugins if a python plugin of the
+  requested identifier cannot be found.
+  [#1294](https://github.com/OpenAssetIO/OpenAssetIO/issues/1294)
+
 - Added `LoggerInterface.isSeverityLogged` to allow users to
   short-circuit log message construction if the logger indicates the
   intended severity will be filtered. Updated `SeverityFilter` to use


Test patch:
diff --git a/src/openassetio-python/package/openassetio/test/manager/_implementation.py b/src/openassetio-python/package/openassetio/test/manager/_implementation.py
index 956fac285..75fff7b9f 100644
--- a/src/openassetio-python/package/openassetio/test/manager/_implementation.py
+++ b/src/openassetio-python/package/openassetio/test/manager/_implementation.py
@@ -20,9 +20,14 @@
 """
 import unittest
 
-from openassetio import hostApi, log, pluginSystem
+from openassetio import hostApi, log
+from openassetio.pluginSystem import (
+    CppPluginSystemManagerImplementationFactory,
+    PythonPluginSystemManagerImplementationFactory,
+)
 from openassetio.trait import TraitsData
 from .. import kTestHarnessTraitId, kCasePropertyKey
+from ...errors import InputValidationException
 
 
 __all__ = ["createHarness"]
@@ -32,15 +37,30 @@ def createHarness(managerIdentifier, settings=None):
     """
     Create the test harness used begin test case execution.
     @private
+
+    @exception errors.InputValidationException Raised if no plugin
+    provides the specified identifier.
     """
     if settings is None:
         settings = {}
 
     hostInterface = _ValidatorHarnessHostInterface()
     logger = log.SeverityFilter(log.ConsoleLogger())
-    managerFactoryImplementation = pluginSystem.PythonPluginSystemManagerImplementationFactory(
-        logger
-    )
+
+    managerFactoryImplementation = PythonPluginSystemManagerImplementationFactory(logger)
+
+    # If the python plugin cannot be found on a scan (occurs lazily when
+    # identifiers is called), then move forward with C++, as it could
+    # be there.
+    if managerIdentifier not in managerFactoryImplementation.identifiers():
+        managerFactoryImplementation = CppPluginSystemManagerImplementationFactory(logger)
+
+    if managerIdentifier not in managerFactoryImplementation.identifiers():
+        msg = (
+            "Test Harness: Could not find Python or Cpp plugin with identifier '%s'"
+            % managerIdentifier
+        )
+        raise InputValidationException(msg)
 
     def createManager(initialize=True):
         manager = hostApi.ManagerFactory.createManagerForInterface(
diff --git a/src/openassetio-python/tests/package/test/manager/test__implementation.py b/src/openassetio-python/tests/package/test/manager/test__implementation.py
index 307ce3f84..0992e073b 100644
--- a/src/openassetio-python/tests/package/test/manager/test__implementation.py
+++ b/src/openassetio-python/tests/package/test/manager/test__implementation.py
@@ -32,8 +32,15 @@
 from openassetio.test.manager._implementation import (
     _ValidatorHarnessHostInterface,
     _ValidatorTestLoader,
+    createHarness,
 )
 from openassetio.trait import TraitsData
+from openassetio.hostApi import ManagerFactory
+from openassetio.pluginSystem import (
+    PythonPluginSystemManagerImplementationFactory,
+    CppPluginSystemManagerImplementationFactory,
+)
+from openassetio import errors
 
 
 class Test_Loader_loadTestsFromTestCase:
@@ -216,6 +223,117 @@ def test_has_fixtures_and_manager(self, a_test_case, a_fixture_dict, mock_manage
         assert a_test_case._locale is a_locale
 
 
+# Test that the test harness chooses the correct language manager
+# implementation factory based on the reported results of scanning the
+# system environment
+class Test_createHarness:
+    def test_no_managers_available(self, monkeypatch):
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            PythonPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: [""],
+        )
+        monkeypatch.setattr(
+            CppPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: [""],
+        )
+        with pytest.raises(
+            errors.InputValidationException,
+            match="Test Harness: Could not find Python or Cpp plugin with identifier "
+            "'an.asset.manager'",
+        ):
+            createHarness("an.asset.manager")
+
+    def test_python_manager_available_only(self, monkeypatch):
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            PythonPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["an.asset.manager"],
+        )
+        createHarness("an.asset.manager")
+        ManagerFactory.createManagerForInterface.assert_called_once()
+        assert isinstance(
+            ManagerFactory.createManagerForInterface.call_args[0][2],
+            PythonPluginSystemManagerImplementationFactory,
+        )
+
+    def test_cpp_manager_available_only(self, monkeypatch):
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            CppPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["an.asset.manager"],
+        )
+        createHarness("an.asset.manager")
+        ManagerFactory.createManagerForInterface.assert_called_once()
+        assert isinstance(
+            ManagerFactory.createManagerForInterface.call_args[0][2],
+            CppPluginSystemManagerImplementationFactory,
+        )
+
+    def test_python_and_cpp_manager_available(self, monkeypatch):
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            PythonPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["an.asset.manager"],
+        )
+        monkeypatch.setattr(
+            CppPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["an.asset.manager"],
+        )
+        createHarness("an.asset.manager")
+        ManagerFactory.createManagerForInterface.assert_called_once()
+        # We should preference python if both are available.
+        assert isinstance(
+            ManagerFactory.createManagerForInterface.call_args[0][2],
+            PythonPluginSystemManagerImplementationFactory,
+        )
+
+    def test_specified_cpp_manager_chosen(self, monkeypatch):
+
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            PythonPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["a.python.manager"],
+        )
+        monkeypatch.setattr(
+            CppPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["a.cpp.manager"],
+        )
+        createHarness("a.cpp.manager")
+        ManagerFactory.createManagerForInterface.assert_called_once()
+        assert isinstance(
+            ManagerFactory.createManagerForInterface.call_args[0][2],
+            CppPluginSystemManagerImplementationFactory,
+        )
+
+    def test_specified_python_manager_chosen(self, monkeypatch):
+        monkeypatch.setattr(ManagerFactory, "createManagerForInterface", mock.Mock())
+        monkeypatch.setattr(
+            PythonPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["a.python.manager"],
+        )
+        monkeypatch.setattr(
+            CppPluginSystemManagerImplementationFactory,
+            "identifiers",
+            lambda self: ["a.cpp.manager"],
+        )
+        createHarness("a.python.manager")
+        ManagerFactory.createManagerForInterface.assert_called_once()
+        assert isinstance(
+            ManagerFactory.createManagerForInterface.call_args[0][2],
+            PythonPluginSystemManagerImplementationFactory,
+        )
+
+
 @pytest.fixture
 def a_test_case(a_fixture_dict, mock_manager, a_locale):
     return FixtureAugmentedTestCase(a_fixture_dict, mock_manager, a_locale)