Skip to content

Commit

Permalink
[Core] Configurable hasCapability responses
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
feltech committed Jul 10, 2024
1 parent 79765b8 commit 768fa0b
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 2 deletions.
90 changes: 90 additions & 0 deletions plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,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,
Expand Down Expand Up @@ -212,6 +232,12 @@ 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.
Expand Down Expand Up @@ -259,6 +285,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)
Expand Down Expand Up @@ -292,6 +330,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,),
Expand Down Expand Up @@ -330,6 +380,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,),
Expand Down Expand Up @@ -408,6 +470,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,),
Expand Down Expand Up @@ -446,6 +522,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,),
Expand Down
9 changes: 9 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
96 changes: 94 additions & 2 deletions tests/bal_business_logic_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from openassetio.access import (
PolicyAccess,
PublishingAccess,
DefaultEntityAccess,
RelationsAccess,
ResolveAccess,
EntityTraitsAccess,
Expand All @@ -41,6 +42,7 @@
BatchElementError,
BatchElementException,
ConfigurationException,
NotImplementedException,
)
from openassetio.test.manager.harness import FixtureAugmentedTestCase
from openassetio.trait import TraitsData
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
"""
Expand All @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions tests/resources/library_business_logic_suite_capabilities_all.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"capabilities": [
"entityReferenceIdentification",
"managementPolicyQueries",
"statefulContexts",
"customTerminology",
"resolution",
"publishing",
"relationshipQueries",
"existenceQueries",
"defaultEntityReferences",
"entityTraitIntrospection"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"capabilities": [
"entityReferenceIdentification",
"managementPolicyQueries",
"entityTraitIntrospection"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"capabilities": []
}

0 comments on commit 768fa0b

Please sign in to comment.