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": [] +}