Skip to content

Commit 1f65d3d

Browse files
committed
[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 <[email protected]>
1 parent 465795f commit 1f65d3d

7 files changed

+220
-2
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ v1.0.0-alpha.x
66

77
### New features
88

9+
- Added support for configuring the result of `hasCapability(...)`
10+
queries. This allows hosts to test their logic when dealing with
11+
managers that have limited capability.
12+
[#84](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/issues/84)
13+
914
- Added support for `OPENASSETIO_BAL_IDENTIFIER` environment variable,
1015
for overriding the identifier advertised by the BAL plugin/manager.
1116
[#116](https://github.com/OpenAssetIO/OpenAssetIO-Manager-BAL/pull/116)

plugin/openassetio_manager_bal/BasicAssetLibraryInterface.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,26 @@ def settings(self, hostSession):
121121
return self.__settings.copy()
122122

123123
def hasCapability(self, capability):
124+
"""
125+
Override to report either real or configured capabilities.
126+
127+
The default set of capabilities reflect the true capabilities
128+
of BAL.
129+
130+
The reported available capabilities can be configured in the
131+
JSON library using the "capabilities" key, which is a list of
132+
capability name strings, as defined in
133+
`ManagerInterface.kCapabilityNames` - useful for testing host
134+
application logic.
135+
136+
API methods associated with disabled capabilities will
137+
short-circuit and call the base class implementation (which will
138+
raise a `NotImplementedException`).
139+
"""
140+
if self.__library.get("capabilities") is not None:
141+
capabilityStr = self.kCapabilityNames[capability]
142+
return capabilityStr in self.__library["capabilities"]
143+
124144
if capability in (
125145
ManagerInterface.Capability.kEntityReferenceIdentification,
126146
ManagerInterface.Capability.kManagementPolicyQueries,
@@ -215,6 +235,10 @@ def isEntityReferenceString(self, someString, hostSession):
215235

216236
@simulated_delay
217237
def entityExists(self, entityRefs, context, _hostSession, successCallback, errorCallback):
238+
if not self.hasCapability(self.Capability.kExistenceQueries):
239+
super().entityExists(entityRefs, context, _hostSession, successCallback, errorCallback)
240+
return
241+
218242
for idx, ref in enumerate(entityRefs):
219243
try:
220244
# Use resolve-for-read access mode as closest analog.
@@ -262,6 +286,18 @@ def resolve(
262286
errorCallback,
263287
):
264288
# pylint: disable=too-many-locals
289+
if not self.hasCapability(self.Capability.kResolution):
290+
super().resolve(
291+
entityReferences,
292+
traitSet,
293+
access,
294+
context,
295+
hostSession,
296+
successCallback,
297+
errorCallback,
298+
)
299+
return
300+
265301
for idx, ref in enumerate(entityReferences):
266302
try:
267303
entity_info = self.__parse_entity_ref(ref.toString(), access)
@@ -295,6 +331,18 @@ def preflight(
295331
successCallback,
296332
errorCallback,
297333
):
334+
if not self.hasCapability(self.Capability.kPublishing):
335+
super().preflight(
336+
targetEntityRefs,
337+
traitsDatas,
338+
access,
339+
context,
340+
hostSession,
341+
successCallback,
342+
errorCallback,
343+
)
344+
return
345+
298346
if not self.__validate_access(
299347
"preflight",
300348
(PublishingAccess.kWrite,),
@@ -333,6 +381,18 @@ def register(
333381
successCallback,
334382
errorCallback,
335383
):
384+
if not self.hasCapability(self.Capability.kPublishing):
385+
super().register(
386+
targetEntityRefs,
387+
entityTraitsDatas,
388+
access,
389+
context,
390+
hostSession,
391+
successCallback,
392+
errorCallback,
393+
)
394+
return
395+
336396
if not self.__validate_access(
337397
"register",
338398
(PublishingAccess.kWrite,),
@@ -411,6 +471,20 @@ def getWithRelationship(
411471
successCallback,
412472
errorCallback,
413473
):
474+
if not self.hasCapability(self.Capability.kRelationshipQueries):
475+
super().getWithRelationship(
476+
entityReferences,
477+
relationshipTraitsData,
478+
resultTraitSet,
479+
pageSize,
480+
access,
481+
context,
482+
_hostSession,
483+
successCallback,
484+
errorCallback,
485+
)
486+
return
487+
414488
if not self.__validate_access(
415489
"relationship query",
416490
(RelationsAccess.kRead,),
@@ -449,6 +523,20 @@ def getWithRelationships(
449523
successCallback,
450524
errorCallback,
451525
):
526+
if not self.hasCapability(self.Capability.kRelationshipQueries):
527+
super().getWithRelationships(
528+
entityReference,
529+
relationshipTraitsDatas,
530+
resultTraitSet,
531+
pageSize,
532+
access,
533+
context,
534+
_hostSession,
535+
successCallback,
536+
errorCallback,
537+
)
538+
return
539+
452540
if not self.__validate_access(
453541
"relationship query",
454542
(RelationsAccess.kRead,),

schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@
55
"description": "The data store that backs an instance of the BAL manager",
66
"type": "object",
77
"properties": {
8+
"capabilities": {
9+
"description": "List of capabilities that BAL should advertise",
10+
"type": "array",
11+
"items": {
12+
"type": "string",
13+
"description": "The name of a capability, as defined in ManagerInterface.kCapabilityNames"
14+
},
15+
"additionalProperties": false
16+
},
817
"variables": {
918
"description": "Arbitrary variables to be substituted in string trait properties",
1019
"type": "object",

tests/bal_business_logic_suite.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from openassetio.access import (
3434
PolicyAccess,
3535
PublishingAccess,
36+
DefaultEntityAccess,
3637
RelationsAccess,
3738
ResolveAccess,
3839
EntityTraitsAccess,
@@ -41,6 +42,7 @@
4142
BatchElementError,
4243
BatchElementException,
4344
ConfigurationException,
45+
NotImplementedException,
4446
)
4547
from openassetio.test.manager.harness import FixtureAugmentedTestCase
4648
from openassetio.trait import TraitsData
@@ -83,8 +85,8 @@ def setUp(self):
8385
"resources",
8486
self._library,
8587
)
86-
self._manager.initialize(new_settings)
8788
self.addCleanup(self.cleanUp)
89+
self._manager.initialize(new_settings)
8890

8991
def cleanUp(self):
9092
self._manager.initialize(self.__old_settings)
@@ -370,7 +372,7 @@ def test_returns_expected_policy_for_managerDriven_for_all_trait_sets(self):
370372
self.assertListEqual(actual, expected)
371373

372374

373-
class Test_hasCapability(FixtureAugmentedTestCase):
375+
class Test_hasCapability_default(FixtureAugmentedTestCase):
374376
"""
375377
Tests that BAL reports expected capabilities
376378
"""
@@ -395,6 +397,96 @@ def test_when_hasCapability_called_on_managerInterface_then_has_mandatory_capabi
395397
)
396398

397399

400+
class Test_hasCapability_override_none(LibraryOverrideTestCase):
401+
_library = "library_business_logic_suite_capabilities_none.json"
402+
403+
def setUp(self):
404+
# Override base class because otherwise it'll raise. The setUp
405+
# in this case _is_ the test.
406+
pass
407+
408+
def test_when_when_library_lists_no_capabilities_then_raises(self):
409+
with self.assertRaises(ConfigurationException) as exc:
410+
# Call base class setup, which will re-initialize the
411+
# manager with the alternative self._library JSON file.
412+
super().setUp()
413+
414+
self.assertEqual(
415+
str(exc.exception),
416+
"Manager implementation for 'org.openassetio.examples.manager.bal' does not"
417+
" support the required capabilities: entityReferenceIdentification,"
418+
" managementPolicyQueries, entityTraitIntrospection",
419+
)
420+
421+
422+
class Test_hasCapability_override_all(LibraryOverrideTestCase):
423+
_library = "library_business_logic_suite_capabilities_all.json"
424+
425+
def test_when_library_lists_all_capabilities_then_hasCapability_is_true_for_all(self):
426+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kStatefulContexts))
427+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kCustomTerminology))
428+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))
429+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kResolution))
430+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kPublishing))
431+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kRelationshipQueries))
432+
self.assertTrue(self._manager.hasCapability(Manager.Capability.kExistenceQueries))
433+
434+
435+
class Test_hasCapability_override_minimal(LibraryOverrideTestCase):
436+
_library = "library_business_logic_suite_capabilities_minimal.json"
437+
438+
def test_when_library_lists_minimal_capabilities_then_hasCapability_is_false_for_all(self):
439+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kStatefulContexts))
440+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kCustomTerminology))
441+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kDefaultEntityReferences))
442+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kResolution))
443+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kPublishing))
444+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kRelationshipQueries))
445+
self.assertFalse(self._manager.hasCapability(Manager.Capability.kExistenceQueries))
446+
447+
def test_when_capability_not_supported_then_methods_raise_NotImplementedException(self):
448+
context = self.createTestContext()
449+
450+
with self.assertRaises(NotImplementedException):
451+
self._manager.defaultEntityReference(
452+
[],
453+
DefaultEntityAccess.kRead,
454+
context,
455+
lambda *a: self.fail("Unexpected success"),
456+
lambda *a: self.fail("Unexpected element error"),
457+
)
458+
459+
with self.assertRaises(NotImplementedException):
460+
self._manager.updateTerminology({})
461+
462+
with self.assertRaises(NotImplementedException):
463+
self._manager.resolve([], set(), ResolveAccess.kRead, context)
464+
465+
with self.assertRaises(NotImplementedException):
466+
self._manager.preflight([], [], PublishingAccess.kWrite, context)
467+
468+
with self.assertRaises(NotImplementedException):
469+
self._manager.register([], [], PublishingAccess.kWrite, context)
470+
471+
with self.assertRaises(NotImplementedException):
472+
self._manager.getWithRelationship(
473+
[], TraitsData(), 1, RelationsAccess.kRead, context, set()
474+
)
475+
476+
with self.assertRaises(NotImplementedException):
477+
self._manager.getWithRelationships(
478+
self._manager.createEntityReference("bal:///"),
479+
[],
480+
1,
481+
RelationsAccess.kRead,
482+
context,
483+
set(),
484+
)
485+
486+
with self.assertRaises(NotImplementedException):
487+
self._manager.entityExists([], context)
488+
489+
398490
class Test_entityTraits(FixtureAugmentedTestCase):
399491
def test_when_missing_entity_queried_for_write_then_empty_trait_set_returned(self):
400492
# Missing entities are writable with unrestricted trait set.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"capabilities": [
3+
"entityReferenceIdentification",
4+
"managementPolicyQueries",
5+
"statefulContexts",
6+
"customTerminology",
7+
"resolution",
8+
"publishing",
9+
"relationshipQueries",
10+
"existenceQueries",
11+
"defaultEntityReferences",
12+
"entityTraitIntrospection"
13+
]
14+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"capabilities": [
3+
"entityReferenceIdentification",
4+
"managementPolicyQueries",
5+
"entityTraitIntrospection"
6+
]
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"capabilities": []
3+
}

0 commit comments

Comments
 (0)