Skip to content
Merged
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
87 changes: 87 additions & 0 deletions tools/tests/+ommtest/+helper/InstanceLibraryFixture.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
classdef InstanceLibraryFixture < matlab.unittest.fixtures.Fixture
%InstanceLibraryFixture Read a small instance library for the duration of a test
%
% The instance library is one object per session, read by default from
% the library downloaded under the user folder. A test that reads it
% there depends on a network, on a download of seventy megabytes, and
% on whatever upstream holds on the day. This fixture copies a small
% library kept with the tests into a temporary folder and makes that the
% library in use, so a test reads a known set of instances, and may
% change the copy as it likes.
%
% Usage:
% fixture = testCase.applyFixture(ommtest.helper.InstanceLibraryFixture);
% fixture.Library % the instance library reading the copy
% fixture.Folder % the copy, one subfolder per library version
%
% Use fixture.Library rather than getSingleton with no argument: the
% library asked for by no argument is the one at the default location,
% and asking for it replaces the fixture's library with the downloaded
% one. Ask for getSingleton(fixture.Folder) to get the fixture's library
% back after it has been rebuilt.
%
% The library kept with the tests is under tools/tests/fixtures/instances
% and holds, for the "latest" version: two age categories and a
% biological sex, stored one type per folder under terminologies; a
% license and a content type, stored under the plural folder names
% their IRIs also use; and a parcellation entity, stored in a subfolder
% of its atlas. Every type exists in every model version from v3.0 on.
%
% Afterwards the library in use is deleted, and the next use reads the
% default location again. It is deleted rather than read again here, so
% that a suite which never needs the downloaded library never downloads
% it. A handle to the library obtained before the fixture is applied is
% deleted when it is applied, as any change of location deletes the
% library in use.
%
% Each application makes its own copy, so nothing a test changes is seen
% by another.
%
% See also matlab.unittest.fixtures.Fixture, openminds.internal.InstanceLibrary

properties (SetAccess = private)
% Folder - The temporary copy of the library this fixture reads
Folder (1,1) string = missing

% Library - The instance library reading that copy
Library
end

methods
function setup(fixture)
fixture.Folder = string(tempname());
copyfile(sourceFolder(), fixture.Folder);
fixture.addTeardown(@() rmdir(fixture.Folder, "s"));

% Registered before the library is replaced, so the restore is
% on record from the moment there is something to restore, and
% it runs before the copy is removed, since teardowns run in
% the reverse of the order they were added.
fixture.Library = ...
openminds.internal.InstanceLibrary.getSingleton(fixture.Folder);
fixture.addTeardown(@() delete(fixture.Library));

fixture.SetupDescription = sprintf( ...
'Read the instance library from a copy of the test library at "%s".', ...
fixture.Folder);
fixture.TeardownDescription = ...
'Deleted that library, so the next use reads the default location.';
end
end

methods (Access = protected)
function tf = isCompatible(~, ~)
% isCompatible - Whether a shared fixture can stand in for another
%
% Never: each application makes its own copy, so that what one
% test changes is not what the next one reads.
tf = false;
end
end
end

function folderPath = sourceFolder()
% sourceFolder - The library kept with the tests
testsFolder = fileparts(fileparts(fileparts( mfilename('fullpath') )));
folderPath = fullfile(testsFolder, "fixtures", "instances");
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/contentTypes/text_plain",
"@type": "https://openminds.om-i.org/types/ContentType",
"dataType": null,
"definingSource": null,
"description": null,
"displayLabel": null,
"fileExtension": [
".txt"
],
"isBasedOn": null,
"name": "text/plain",
"specification": null,
"synonym": [
"plain text file format"
]
}
14 changes: 14 additions & 0 deletions tools/tests/fixtures/instances/latest/licenses/MIT.jsonld
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/licenses/MIT",
"@type": "https://openminds.om-i.org/types/License",
"fullName": "MIT License",
"legalCode": "https://spdx.org/licenses/MIT.html",
"shortName": "MIT",
"webpage": [
"https://opensource.org/licenses/MIT",
"https://spdx.org/licenses/MIT.html"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/parcellationEntity/BA-human_BA32",
"@type": "https://openminds.om-i.org/types/ParcellationEntity",
"abbreviation": "BA32",
"alternateName": [
"Brodmann's area 32",
"human BA32",
"human Brodmann area 32",
"human Brodmann's area 32"
],
"definition": null,
"hasParent": [
{
"@id": "https://openminds.om-i.org/instances/parcellationEntity/BA-human_cerebralCortex"
}
],
"lookupLabel": "BA-human_BA32",
"name": "Brodmann area 32",
"ontologyIdentifier": null,
"relatedInterspeciesAnatomy": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/ageCategory/adult",
"@type": "https://openminds.om-i.org/types/AgeCategory",
"definition": "'Adult' categorizes the life cycle stage of an animal or human that reached sexual maturity.",
"description": null,
"name": "adult",
"otherCrossReference": null,
"otherOntologyIdentifier": [
"http://uri.interlex.org/base/ilx_0729043"
],
"preferredCrossReference": null,
"preferredOntologyIdentifier": "http://purl.obolibrary.org/obo/UBERON_0000113",
"synonym": [
"adult stage",
"post-juvenile adult",
"post-juvenile adult stage"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/ageCategory/youngAdult",
"@type": "https://openminds.om-i.org/types/AgeCategory",
"definition": "'Young adult' categorizes the early adult stage of an animal or human when sexual maturity has been reached, but not the cessation of growth.",
"description": null,
"name": "young adult",
"otherCrossReference": null,
"otherOntologyIdentifier": null,
"preferredCrossReference": null,
"preferredOntologyIdentifier": null,
"synonym": [
"early adult",
"early adult stage",
"young adult stage"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"@context": {
"@vocab": "https://openminds.om-i.org/props/"
},
"@id": "https://openminds.om-i.org/instances/biologicalSex/male",
"@type": "https://openminds.om-i.org/types/BiologicalSex",
"definition": "Biological sex that produces sperm cells (spermatozoa).",
"description": "A male organism typically has the capacity to produce relatively small, usually mobile gametes (reproductive cells), called sperm cells (or spermatozoa). In the process of fertilization, these sperm cells fuse with a larger, usually immobile female gamete, called egg cell (or ovum).",
"name": "male",
"otherCrossReference": null,
"otherOntologyIdentifier": [
"http://uri.interlex.org/base/ilx_0106489"
],
"preferredCrossReference": null,
"preferredOntologyIdentifier": "http://purl.obolibrary.org/obo/PATO_0000384",
"synonym": null
}
132 changes: 132 additions & 0 deletions tools/tests/unitTests/InstanceLibraryIntegrationTest.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
classdef InstanceLibraryIntegrationTest < matlab.unittest.TestCase
% InstanceLibraryIntegrationTest - Tests against the downloaded instance library
%
% These read the library the toolbox downloads, so they need it to be
% there and skip when it is not. What they check cannot be checked
% against a library kept with the tests: that every instance upstream
% ships resolves to a type in the model, and that the library follows
% the model version across the versions upstream publishes.

properties
InstanceLibrary
InstanceTable table
end

methods (TestClassSetup)
function setupClass(testCase)
testCase.InstanceLibrary = ...
openminds.internal.InstanceLibrary.getSingleton();
testCase.InstanceTable = testCase.InstanceLibrary.InstanceTable;

testCase.assumeNotEmpty(testCase.InstanceTable, ...
'The instance library is not available locally.')
end
end

methods (Test)
function testEveryInstanceIsTyped(testCase)
% Instances used to be typed by folder name. Folder names are
% pluralized type names that upstream renames whenever a type is
% renamed, so that typing went stale silently and left whole
% modules of the library untyped. Every instance must have a type,
% and only the downloaded library can show that this holds for
% what upstream ships today.

untyped = testCase.InstanceTable(testCase.InstanceTable.Type == "", :);

testCase.verifyEmpty(untyped, sprintf( ...
'Instances of %d folder(s) were left without a type, e.g. "%s".', ...
numel(unique(fileparts(untyped.Filepath))), ...
strjoin(unique(fileparts(untyped.Filepath))', '", "')))
end

function testModuleIsResolvedForEveryInstance(testCase)
% The module used to be derived from the folder name together with
% the type, so it went stale for the same reason.

testCase.verifyEmpty( ...
testCase.InstanceTable(testCase.InstanceTable.Module == "", :))
end

function testSelectingAModelVersionRebuildsTheLibrary(testCase)
% The instance table is typed against the model version on the path
% when it was built, so selecting another version must rebuild the
% library object already in memory. It must be rebuilt in place,
% because other code may hold a reference to the object.

library = testCase.InstanceLibrary;
testCase.assertEqual(library.ModelVersion, openminds.version())

testCase.applyFixture(ommtest.helper.ModelVersionFixture("v3.0"))

testCase.verifyEqual(library.ModelVersion, "v3.0", ...
'Selecting a model version must rebuild the library in memory.')

% ModelVersion is stored in the format openminds.version
% reports, so getSingleton must recognize the rebuilt library
% as current and return the same handle instead of creating a
% new one.
testCase.verifySameHandle( ...
openminds.internal.InstanceLibrary.getSingleton(), library)
end

function testLibraryVersionFollowsTheModelVersion(testCase)
% The library publishes one set of instances per model version, and
% reading one version's instances against another version's classes
% leaves instances untyped, so the library version must equal the
% model version.

library = testCase.InstanceLibrary;
testCase.assumeTrue(ismember("v3.0", library.AvailableVersions))

testCase.applyFixture(ommtest.helper.ModelVersionFixture("v3.0"))

testCase.verifyEqual(library.LibraryVersion, "v3.0")

% The library is downloaded, and a copy may hold no instances
% for this version. That is not a failure of the code under
% test.
testCase.assumeNotEmpty(library.InstanceTable, ...
'The instance library holds no instances for this version.')

% Check that the instances were read from the v3.0 folder, not
% merely labelled v3.0. Whether every instance then resolves to
% a type depends on the model classes having been reloaded,
% which MATLAB cannot do while objects of those classes exist
% in the session, so that is not checked here.
readFromVersion = contains(library.InstanceTable.Filepath, ...
fullfile(filesep, "v3.0", filesep));

testCase.verifyTrue(all(readFromVersion), ...
'The library must read the instances of the selected version.')
end

function testModelVersionWithoutInstancesIsReported(testCase)
% Model versions 1 and 2 predate the type names the instance library
% uses, so no library version can serve them. This must be reported
% as a warning rather than look like a library that happens to be
% empty.
%
% The version is switched directly rather than through
% ModelVersionFixture because verifyWarning has to wrap the call
% that raises the warning.

previousModelVersion = openminds.version();
testCase.addTeardown(@openminds.version, previousModelVersion);

testCase.verifyWarning(@() openminds.version("v1.0"), ...
'OPENMINDS:InstanceLibrary:NoInstancesForModelVersion')

library = testCase.InstanceLibrary;
testCase.verifyTrue(ismissing(library.LibraryVersion), ...
'No library version can be read for this model version.')

% The table keeps its columns, so filtering it returns no rows
% instead of erroring.
testCase.verifyEqual(height(library.InstanceTable), 0)
testCase.verifyEqual( ...
string(library.InstanceTable.Properties.VariableNames), ...
["InstanceName", "Type", "Module", "Subgroup", "Filepath"])
end
end
end
Loading
Loading