From 1f65d3d1237815057f0efe52fc1b15edfb915071 Mon Sep 17 00:00:00 2001 From: David Feltell Date: Wed, 10 Jul 2024 17:48:32 +0100 Subject: [PATCH] [Core] Configurable `hasCapability` responses Closes #84. Host applications must be able to tolerate manager plugins that do not support the full suite of (optional) capabilities that a manager may implement. However, there was no way for hosts to use BAL to test their logic for dealing with different combinations of capabilities. In addition, configurable capabilities are required to enable e2e testing of the capability-based routing used by the upcoming hybrid plugin system (see OpenAssetIO/OpenAssetIO#1202). So add a simple way to override the default set of capabilities reported by BAL, by adding an optional "capabilities" list element to the library, where each element is a stringified capability, as defined in `kCapabilityNames`. Presence of a capability in this list indicates that it is supported. If the list is not found, then the default (i.e. true) set of capabilities is used. Short-circuit methods where the capability is unsupported. Instead, call the base class (which will raise an error). This logic is similar to that used in the SimpleCppManager (see OpenAssetIO/OpenAssetIO#1324). Signed-off-by: David Feltell --- RELEASE_NOTES.md | 5 + .../BasicAssetLibraryInterface.py | 88 +++++++++++++++++ schema.json | 9 ++ tests/bal_business_logic_suite.py | 96 ++++++++++++++++++- ...business_logic_suite_capabilities_all.json | 14 +++ ...ness_logic_suite_capabilities_minimal.json | 7 ++ ...usiness_logic_suite_capabilities_none.json | 3 + 7 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 tests/resources/library_business_logic_suite_capabilities_all.json create mode 100644 tests/resources/library_business_logic_suite_capabilities_minimal.json create mode 100644 tests/resources/library_business_logic_suite_capabilities_none.json diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index a4900ea..f660e95 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,11 @@ v1.0.0-alpha.x ### New features +- Added support for configuring the result of `hasCapability(...)` + queries. This allows hosts to test their logic when dealing with + managers that have limited capability. + [#84](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/issues/84) + - Added support for `OPENASSETIO_BAL_IDENTIFIER` environment variable, for overriding the identifier advertised by the BAL plugin/manager. [#116](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/pull/116) diff --git a/plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py b/plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py index b3b814d..d87867c 100644 --- a/plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py +++ b/plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py @@ -121,6 +121,26 @@ def settings(self, hostSession): return self.__settings.copy() def hasCapability(self, capability): + """ + Override to report either real or configured capabilities. + + The default set of capabilities reflect the true capabilities + of BAL. + + The reported available capabilities can be configured in the + JSON library using the "capabilities" key, which is a list of + capability name strings, as defined in + `ManagerInterface.kCapabilityNames` - useful for testing host + application logic. + + API methods associated with disabled capabilities will + short-circuit and call the base class implementation (which will + raise a `NotImplementedException`). + """ + if self.__library.get("capabilities") is not None: + capabilityStr = self.kCapabilityNames[capability] + return capabilityStr in self.__library["capabilities"] + if capability in ( ManagerInterface.Capability.kEntityReferenceIdentification, ManagerInterface.Capability.kManagementPolicyQueries, @@ -215,6 +235,10 @@ def isEntityReferenceString(self, someString, hostSession): @simulated_delay def entityExists(self, entityRefs, context, _hostSession, successCallback, errorCallback): + if not self.hasCapability(self.Capability.kExistenceQueries): + super().entityExists(entityRefs, context, _hostSession, successCallback, errorCallback) + return + for idx, ref in enumerate(entityRefs): try: # Use resolve-for-read access mode as closest analog. @@ -262,6 +286,18 @@ def resolve( errorCallback, ): # pylint: disable=too-many-locals + if not self.hasCapability(self.Capability.kResolution): + super().resolve( + entityReferences, + traitSet, + access, + context, + hostSession, + successCallback, + errorCallback, + ) + return + for idx, ref in enumerate(entityReferences): try: entity_info = self.__parse_entity_ref(ref.toString(), access) @@ -295,6 +331,18 @@ def preflight( successCallback, errorCallback, ): + if not self.hasCapability(self.Capability.kPublishing): + super().preflight( + targetEntityRefs, + traitsDatas, + access, + context, + hostSession, + successCallback, + errorCallback, + ) + return + if not self.__validate_access( "preflight", (PublishingAccess.kWrite,), @@ -333,6 +381,18 @@ def register( successCallback, errorCallback, ): + if not self.hasCapability(self.Capability.kPublishing): + super().register( + targetEntityRefs, + entityTraitsDatas, + access, + context, + hostSession, + successCallback, + errorCallback, + ) + return + if not self.__validate_access( "register", (PublishingAccess.kWrite,), @@ -411,6 +471,20 @@ def getWithRelationship( successCallback, errorCallback, ): + if not self.hasCapability(self.Capability.kRelationshipQueries): + super().getWithRelationship( + entityReferences, + relationshipTraitsData, + resultTraitSet, + pageSize, + access, + context, + _hostSession, + successCallback, + errorCallback, + ) + return + if not self.__validate_access( "relationship query", (RelationsAccess.kRead,), @@ -449,6 +523,20 @@ def getWithRelationships( successCallback, errorCallback, ): + if not self.hasCapability(self.Capability.kRelationshipQueries): + super().getWithRelationships( + entityReference, + relationshipTraitsDatas, + resultTraitSet, + pageSize, + access, + context, + _hostSession, + successCallback, + errorCallback, + ) + return + if not self.__validate_access( "relationship query", (RelationsAccess.kRead,), diff --git a/schema.json b/schema.json index ffff1cf..cd39fa8 100644 --- a/schema.json +++ b/schema.json @@ -5,6 +5,15 @@ "description": "The data store that backs an instance of the BAL manager", "type": "object", "properties": { + "capabilities": { + "description": "List of capabilities that BAL should advertise", + "type": "array", + "items": { + "type": "string", + "description": "The name of a capability, as defined in ManagerInterface.kCapabilityNames" + }, + "additionalProperties": false + }, "variables": { "description": "Arbitrary variables to be substituted in string trait properties", "type": "object", diff --git a/tests/bal_business_logic_suite.py b/tests/bal_business_logic_suite.py index 2e4875c..93cd4a4 100644 --- a/tests/bal_business_logic_suite.py +++ b/tests/bal_business_logic_suite.py @@ -33,6 +33,7 @@ from openassetio.access import ( PolicyAccess, PublishingAccess, + DefaultEntityAccess, RelationsAccess, ResolveAccess, EntityTraitsAccess, @@ -41,6 +42,7 @@ BatchElementError, BatchElementException, ConfigurationException, + NotImplementedException, ) from openassetio.test.manager.harness import FixtureAugmentedTestCase from openassetio.trait import TraitsData @@ -83,8 +85,8 @@ def setUp(self): "resources", self._library, ) - self._manager.initialize(new_settings) self.addCleanup(self.cleanUp) + self._manager.initialize(new_settings) def cleanUp(self): self._manager.initialize(self.__old_settings) @@ -370,7 +372,7 @@ def test_returns_expected_policy_for_managerDriven_for_all_trait_sets(self): self.assertListEqual(actual, expected) -class Test_hasCapability(FixtureAugmentedTestCase): +class Test_hasCapability_default(FixtureAugmentedTestCase): """ Tests that BAL reports expected capabilities """ @@ -395,6 +397,96 @@ def test_when_hasCapability_called_on_managerInterface_then_has_mandatory_capabi ) +class Test_hasCapability_override_none(LibraryOverrideTestCase): + _library = "library_business_logic_suite_capabilities_none.json" + + def setUp(self): + # Override base class because otherwise it'll raise. The setUp + # in this case _is_ the test. + pass + + def test_when_when_library_lists_no_capabilities_then_raises(self): + with self.assertRaises(ConfigurationException) as exc: + # Call base class setup, which will re-initialize the + # manager with the alternative self._library JSON file. + super().setUp() + + self.assertEqual( + str(exc.exception), + "Manager implementation for 'org.openassetio.examples.manager.bal' does not" + " support the required capabilities: entityReferenceIdentification," + " managementPolicyQueries, entityTraitIntrospection", + ) + + +class Test_hasCapability_override_all(LibraryOverrideTestCase): + _library = "library_business_logic_suite_capabilities_all.json" + + def test_when_library_lists_all_capabilities_then_hasCapability_is_true_for_all(self): + self.assertTrue(self._manager.hasCapability(Manager.Capability.kStatefulContexts)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kCustomTerminology)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kResolution)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kPublishing)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kRelationshipQueries)) + self.assertTrue(self._manager.hasCapability(Manager.Capability.kExistenceQueries)) + + +class Test_hasCapability_override_minimal(LibraryOverrideTestCase): + _library = "library_business_logic_suite_capabilities_minimal.json" + + def test_when_library_lists_minimal_capabilities_then_hasCapability_is_false_for_all(self): + self.assertFalse(self._manager.hasCapability(Manager.Capability.kStatefulContexts)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kCustomTerminology)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kResolution)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kPublishing)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kRelationshipQueries)) + self.assertFalse(self._manager.hasCapability(Manager.Capability.kExistenceQueries)) + + def test_when_capability_not_supported_then_methods_raise_NotImplementedException(self): + context = self.createTestContext() + + with self.assertRaises(NotImplementedException): + self._manager.defaultEntityReference( + [], + DefaultEntityAccess.kRead, + context, + lambda *a: self.fail("Unexpected success"), + lambda *a: self.fail("Unexpected element error"), + ) + + with self.assertRaises(NotImplementedException): + self._manager.updateTerminology({}) + + with self.assertRaises(NotImplementedException): + self._manager.resolve([], set(), ResolveAccess.kRead, context) + + with self.assertRaises(NotImplementedException): + self._manager.preflight([], [], PublishingAccess.kWrite, context) + + with self.assertRaises(NotImplementedException): + self._manager.register([], [], PublishingAccess.kWrite, context) + + with self.assertRaises(NotImplementedException): + self._manager.getWithRelationship( + [], TraitsData(), 1, RelationsAccess.kRead, context, set() + ) + + with self.assertRaises(NotImplementedException): + self._manager.getWithRelationships( + self._manager.createEntityReference("bal:///"), + [], + 1, + RelationsAccess.kRead, + context, + set(), + ) + + with self.assertRaises(NotImplementedException): + self._manager.entityExists([], context) + + class Test_entityTraits(FixtureAugmentedTestCase): def test_when_missing_entity_queried_for_write_then_empty_trait_set_returned(self): # Missing entities are writable with unrestricted trait set. diff --git a/tests/resources/library_business_logic_suite_capabilities_all.json b/tests/resources/library_business_logic_suite_capabilities_all.json new file mode 100644 index 0000000..36f0cc2 --- /dev/null +++ b/tests/resources/library_business_logic_suite_capabilities_all.json @@ -0,0 +1,14 @@ +{ + "capabilities": [ + "entityReferenceIdentification", + "managementPolicyQueries", + "statefulContexts", + "customTerminology", + "resolution", + "publishing", + "relationshipQueries", + "existenceQueries", + "defaultEntityReferences", + "entityTraitIntrospection" + ] +} diff --git a/tests/resources/library_business_logic_suite_capabilities_minimal.json b/tests/resources/library_business_logic_suite_capabilities_minimal.json new file mode 100644 index 0000000..f1e4335 --- /dev/null +++ b/tests/resources/library_business_logic_suite_capabilities_minimal.json @@ -0,0 +1,7 @@ +{ + "capabilities": [ + "entityReferenceIdentification", + "managementPolicyQueries", + "entityTraitIntrospection" + ] +} diff --git a/tests/resources/library_business_logic_suite_capabilities_none.json b/tests/resources/library_business_logic_suite_capabilities_none.json new file mode 100644 index 0000000..4a37536 --- /dev/null +++ b/tests/resources/library_business_logic_suite_capabilities_none.json @@ -0,0 +1,3 @@ +{ + "capabilities": [] +}