Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions tools/tests/+ommtest/+helper/+mock/MockGraphDatabase.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
classdef MockGraphDatabase < handle
% MockGraphDatabase - Stands for an external graph database, in memory
%
% Records are structs with fields Identifier, TypeIRI and Document,
% where Document is the node as JSON text with MATLAB-shim field names.

properties (Access = private)
Records
end

methods
function obj = MockGraphDatabase()
obj.Records = containers.Map('KeyType', 'char', 'ValueType', 'any');
end

function put(obj, record)
obj.Records(char(record.Identifier)) = record;
end

function record = get(obj, identifier)
record = obj.Records(char(identifier));
end

function records = all(obj)
values = obj.Records.values;
records = [values{:}];
end

function n = count(obj)
n = obj.Records.Count;
end
end
end
16 changes: 16 additions & 0 deletions tools/tests/+ommtest/+helper/+mock/MockGraphDeserializer.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
classdef MockGraphDeserializer < openminds.abstract.BaseDeserializer
% MockGraphDeserializer - Reads instances back from mock database records
%
% Only the record parsing lives here. Type dispatch, instance
% construction and cross-record link wiring come from
% openminds.abstract.BaseDeserializer.

methods (Access = protected)
function rawStructs = parseToStructs(~, records)
rawStructs = cell(1, numel(records));
for i = 1:numel(records)
rawStructs{i} = jsondecode(char(records(i).Document));
end
end
end
end
31 changes: 31 additions & 0 deletions tools/tests/+ommtest/+helper/+mock/MockGraphMetadataStore.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
classdef MockGraphMetadataStore < openminds.interface.MetadataStore
% MockGraphMetadataStore - Metadata store backed by the mock graph database

properties (SetAccess = immutable)
Database ommtest.helper.mock.MockGraphDatabase
end

methods
function obj = MockGraphMetadataStore(database)
obj = obj@openminds.interface.MetadataStore();
obj.Database = database;
obj.Serializer = ommtest.helper.mock.MockGraphSerializer();
end

function identifiers = save(obj, instances, ~)
if isa(instances, 'openminds.Collection')
instances = instances.getAll();
end
records = obj.Serializer.serialize(instances);
for i = 1:numel(records)
obj.Database.put(records(i));
end
identifiers = [records.Identifier];
end

function instances = load(obj, ~)
deserializer = ommtest.helper.mock.MockGraphDeserializer();
instances = deserializer.deserialize(obj.Database.all());
end
end
end
38 changes: 38 additions & 0 deletions tools/tests/+ommtest/+helper/+mock/MockGraphResolver.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
classdef MockGraphResolver < openminds.interface.LinkResolver
% MockGraphResolver - Resolves references against the mock graph database
%
% Holds the database client as instance state, which the instance-method
% resolver contract exists for. Populates a reference in place when its
% type is known, and builds an instance of the recorded type when not.

properties (Constant)
IRIPrefix = "https://graph.example/instances/"
end

properties (SetAccess = immutable)
Database ommtest.helper.mock.MockGraphDatabase
end

methods
function obj = MockGraphResolver(database)
obj.Database = database;
end

function instance = resolveNode(obj, instance)
record = obj.Database.get(instance.id);
data = jsondecode(char(record.Document));

if isa(instance, 'openminds.internal.MixedTypeReference')
% The type was unknown until now: build the recorded type
typeEnum = openminds.enum.Types.fromAtType(record.TypeIRI);
instance = feval(typeEnum.ClassName, data);
else
instance.fromStruct(data);
end
end

function tf = canResolve(obj, IRI)
tf = all(startsWith(IRI, obj.IRIPrefix));
end
end
end
36 changes: 36 additions & 0 deletions tools/tests/+ommtest/+helper/+mock/MockGraphSerializer.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
classdef MockGraphSerializer < openminds.abstract.BaseSerializer
% MockGraphSerializer - Serializes instances into mock database records
%
% A non-JSON-LD output format built on the shared serialization core:
% graph traversal, reference handling and embedding come from
% openminds.abstract.BaseSerializer; only the record format is here.

properties (Constant)
DefaultFileExtension = ".mockrecord"
end

methods
function obj = MockGraphSerializer(config)
arguments
config.?openminds.internal.serializer.SerializationConfig
end
config.OutputMode = "multiple"; % one record per node
nvPairs = namedargs2cell(config);
obj = obj@openminds.abstract.BaseSerializer(nvPairs{:});
end
end

methods (Access = protected)
function records = formatOutput(~, processedStructs)
records = cell(1, numel(processedStructs));
for i = 1:numel(processedStructs)
S = processedStructs{i};
records{i} = struct( ...
'Identifier', string(S.at_id), ...
'TypeIRI', string(S.at_type), ...
'Document', string(jsonencode(S)));
end
records = [records{:}];
end
end
end
127 changes: 127 additions & 0 deletions tools/tests/unitTests/ExternalIntegrationTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
classdef ExternalIntegrationTest < matlab.unittest.TestCase
% ExternalIntegrationTest - An external backend built only on the extension points
%
% Exercises the contract that lets a library integrate a database or
% format without changing openMINDS_MATLAB: a mock graph database with
% its own serializer (openminds.abstract.BaseSerializer), deserializer
% (openminds.abstract.BaseDeserializer), store
% (openminds.interface.MetadataStore) and resolver
% (openminds.interface.LinkResolver). If a change here breaks these
% tests, it breaks every external integration, openminds-kg-sync
% included.

properties (Access = private)
Database ommtest.helper.mock.MockGraphDatabase
Store ommtest.helper.mock.MockGraphMetadataStore
end

methods (TestMethodSetup)
function createDatabase(testCase)
testCase.Database = ommtest.helper.mock.MockGraphDatabase();
testCase.Store = ommtest.helper.mock.MockGraphMetadataStore(testCase.Database);
end

function resetResolverRegistry(testCase)
registry = openminds.internal.resolver.LinkResolverRegistry.instance();
registry.reset()
testCase.addTeardown(@() registry.reset())
end
end

methods (Test)
function testSaveThroughCustomSerializer(testCase)
% Every node of the collection becomes one record in the database.

collection = ommtest.helper.buildFixtureCollection();
testCase.Store.save(collection);

testCase.verifyEqual(double(testCase.Database.count()), 7)
end

function testLoadRebuildsTheConnectedGraph(testCase)
% Loading through the custom deserializer must reproduce values,
% wire links between records, and preserve embedded values.

testCase.Store.save(ommtest.helper.buildFixtureCollection());

reloaded = openminds.Collection('MetadataStore', testCase.Store);
testCase.assertEqual(double(reloaded.length()), 7)

person = reloaded.list(openminds.enum.Types("Person"));
testCase.verifyEqual(person.givenName, "Ada")
testCase.verifyEqual(person.contactInformation.email, "ada@example.org", ...
'The linked contact record was not wired to the person.')

subject = reloaded.list(openminds.enum.Types("Subject"));
testCase.verifyEqual(string(subject.species.id), ...
"https://openminds.om-i.org/instances/species/homoSapiens")

state = subject.studiedState;
specimenAge = state.age;
quantity = specimenAge.age;
testCase.verifyEqual(quantity.value, 42, ...
'The embedded quantitative value did not survive the round trip.')
end

function testRoundTripIsCanonicallyEquivalent(testCase)
% The graph reloaded from the database serializes to exactly the
% same canonical JSON-LD document as the original graph.

original = ommtest.helper.buildFixtureCollection();
testCase.Store.save(original);
reloaded = openminds.Collection('MetadataStore', testCase.Store);

serializer = openminds.internal.serializer.JsonLdSerializer('OutputMode', 'single');
testCase.verifyEqual( ...
serializer.serialize(reloaded.getAll()), ...
serializer.serialize(original.getAll()))
end

function testUnknownTypeReferenceResolvesByReplacement(testCase)
% A reference whose type is only known to the database resolves to
% an instance of the recorded type through the registered resolver.

identifier = openminds.core.digitalidentifier.GenericIdentifier( ...
'id', "https://graph.example/instances/generic-001");
identifier.identifier = "external-thing";
identifier.type = "mock";
testCase.Store.save(identifier);

openminds.registerLinkResolver( ...
ommtest.helper.mock.MockGraphResolver(testCase.Database));

dataset = openminds.core.Dataset();
dataset.fullName = "D";
dataset.digitalIdentifier = openminds.internal.MixedTypeReference( ...
"https://graph.example/instances/generic-001");

dataset.resolve('NumLinksToResolve', 1);

resolved = dataset.digitalIdentifier;
testCase.verifyClass(resolved, 'openminds.core.digitalidentifier.GenericIdentifier')
testCase.verifyEqual(resolved.identifier, "external-thing")
end

function testTypedReferenceResolvesInPlace(testCase)
% A reference whose type is already known is populated in place.

document = struct( ...
'at_id', "https://graph.example/instances/person-002", ...
'at_type', "https://openminds.om-i.org/types/Person", ...
'givenName', "Grace");
testCase.Database.put(struct( ...
'Identifier', "https://graph.example/instances/person-002", ...
'TypeIRI', "https://openminds.om-i.org/types/Person", ...
'Document', string(jsonencode(document))));

openminds.registerLinkResolver( ...
ommtest.helper.mock.MockGraphResolver(testCase.Database));

personStub = openminds.core.Person( ...
'id', "https://graph.example/instances/person-002");
personStub.resolve();

testCase.verifyEqual(personStub.givenName, "Grace")
end
end
end
Loading