diff --git a/tools/tests/+ommtest/+helper/InstanceLibraryFixture.m b/tools/tests/+ommtest/+helper/InstanceLibraryFixture.m new file mode 100644 index 000000000..7601a5acf --- /dev/null +++ b/tools/tests/+ommtest/+helper/InstanceLibraryFixture.m @@ -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 diff --git a/tools/tests/fixtures/instances/latest/contentTypes/text_plain.jsonld b/tools/tests/fixtures/instances/latest/contentTypes/text_plain.jsonld new file mode 100644 index 000000000..e215808a7 --- /dev/null +++ b/tools/tests/fixtures/instances/latest/contentTypes/text_plain.jsonld @@ -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" + ] +} diff --git a/tools/tests/fixtures/instances/latest/licenses/MIT.jsonld b/tools/tests/fixtures/instances/latest/licenses/MIT.jsonld new file mode 100644 index 000000000..96f79a47b --- /dev/null +++ b/tools/tests/fixtures/instances/latest/licenses/MIT.jsonld @@ -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" + ] +} diff --git a/tools/tests/fixtures/instances/latest/parcellationEntities/BA-human/BA-human_BA32.jsonld b/tools/tests/fixtures/instances/latest/parcellationEntities/BA-human/BA-human_BA32.jsonld new file mode 100644 index 000000000..71e2b1951 --- /dev/null +++ b/tools/tests/fixtures/instances/latest/parcellationEntities/BA-human/BA-human_BA32.jsonld @@ -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 +} diff --git a/tools/tests/fixtures/instances/latest/terminologies/ageCategory/adult.jsonld b/tools/tests/fixtures/instances/latest/terminologies/ageCategory/adult.jsonld new file mode 100644 index 000000000..e17d7fc49 --- /dev/null +++ b/tools/tests/fixtures/instances/latest/terminologies/ageCategory/adult.jsonld @@ -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" + ] +} diff --git a/tools/tests/fixtures/instances/latest/terminologies/ageCategory/youngAdult.jsonld b/tools/tests/fixtures/instances/latest/terminologies/ageCategory/youngAdult.jsonld new file mode 100644 index 000000000..ec9fd2275 --- /dev/null +++ b/tools/tests/fixtures/instances/latest/terminologies/ageCategory/youngAdult.jsonld @@ -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" + ] +} diff --git a/tools/tests/fixtures/instances/latest/terminologies/biologicalSex/male.jsonld b/tools/tests/fixtures/instances/latest/terminologies/biologicalSex/male.jsonld new file mode 100644 index 000000000..c48075dc1 --- /dev/null +++ b/tools/tests/fixtures/instances/latest/terminologies/biologicalSex/male.jsonld @@ -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 +} diff --git a/tools/tests/unitTests/InstanceLibraryIntegrationTest.m b/tools/tests/unitTests/InstanceLibraryIntegrationTest.m new file mode 100644 index 000000000..87f3c5a89 --- /dev/null +++ b/tools/tests/unitTests/InstanceLibraryIntegrationTest.m @@ -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 diff --git a/tools/tests/unitTests/InstanceLibraryTest.m b/tools/tests/unitTests/InstanceLibraryTest.m index 7afc1c89d..d0edbf375 100644 --- a/tools/tests/unitTests/InstanceLibraryTest.m +++ b/tools/tests/unitTests/InstanceLibraryTest.m @@ -1,205 +1,147 @@ classdef InstanceLibraryTest < matlab.unittest.TestCase % InstanceLibraryTest - Unit tests for the openMINDS instance library +% +% These read a small library kept with the tests, through +% ommtest.helper.InstanceLibraryFixture, so they need no download and +% check known instances. What can only be checked against the +% downloaded library is in InstanceLibraryIntegrationTest. properties - InstanceLibrary - InstanceTable table + Fixture 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.') + methods (TestMethodSetup) + function readTheTestLibrary(testCase) + testCase.Fixture = ... + testCase.applyFixture(ommtest.helper.InstanceLibraryFixture); 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. - - 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 testInstancesAreTypedByWhatTheyDeclare(testCase) + % Every instance in the test library is typed by the "@type" its + % document declares, and its module follows from the type. The + % folder names differ from the type names, in the plural and in + % case, so nothing here can be read off a folder. - function testTypeIsTakenFromTheInstanceAndNotTheFolderName(testCase) - % License instances are stored in a folder named "licenses". No - % capitalization of that folder name gives the type name "License", - % so the type must come from the "@type" the instance declares. + expected = table( ... + ["adult"; "youngAdult"; "male"; "MIT"; "text_plain"; "BA-human_BA32"], ... + ["AgeCategory"; "AgeCategory"; "BiologicalSex"; "License"; ... + "ContentType"; "ParcellationEntity"], ... + ["controlledTerms"; "controlledTerms"; "controlledTerms"; "core"; ... + "core"; "SANDS"], ... + 'VariableNames', ["InstanceName", "Type", "Module"]); - licenses = testCase.instancesOfType("License"); - testCase.assumeNotEmpty(licenses) + actual = testCase.Fixture.Library.InstanceTable( ... + :, ["InstanceName", "Type", "Module"]); - [~, folderNames] = fileparts(fileparts(licenses.Filepath)); - - testCase.verifyTrue(all(folderNames == "licenses"), ... - 'Expected License instances to be stored in the "licenses" folder.') - 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 == "", :)) + testCase.verifyEqual(sortrows(actual), sortrows(expected)) end function testInstancesGroupedInASubfolderCarryTheSubgroup(testCase) - % Parcellation entities are split into one subfolder per atlas, - % e.g. parcellationEntities/BA-human. The subfolder is the subgroup. + % Parcellation entities are grouped per atlas, here in + % parcellationEntities/BA-human. That grouping is the subgroup. - entities = testCase.instancesOfType("ParcellationEntity"); - testCase.assumeNotEmpty(entities) + entity = testCase.instanceNamed("BA-human_BA32"); - testCase.verifyFalse(any(ismissing(entities.Subgroup)), ... - 'Instances grouped in a subfolder must carry that subgroup.') + testCase.verifyEqual(entity.Subgroup, "BA-human") end function testATypeFolderIsNotReportedAsASubgroup(testCase) - % Controlled terms are stored one type per subfolder under - % terminologies, e.g. terminologies/ageCategory. That subfolder - % names a type, not a subgroup. The two cases are told apart by - % whether sibling folders hold the same type. + % Controlled terms are stored one type per folder under + % terminologies. That folder names the type, not a subgroup, and + % the two are told apart by whether sibling folders share a type: + % ageCategory and biologicalSex do not. - ageCategories = testCase.instancesOfType("AgeCategory"); - testCase.assumeNotEmpty(ageCategories) + term = testCase.instanceNamed("adult"); - testCase.verifyTrue(all(ismissing(ageCategories.Subgroup)), ... + testCase.verifyTrue(ismissing(term.Subgroup), ... 'A folder that names a type must not be read as a subgroup.') end function testPluralIRISegmentResolvesToItsType(testCase) % A few instance IRIs name their type in the plural. openMINDS - % publishes no plural-to-singular mapping, so the library reads the - % mapping from the instance documents. + % publishes no plural to singular mapping, so the segments are + % collected from the instances themselves. - typeEnum = testCase.InstanceLibrary.getTypeFromIRISegment("licenses"); + library = testCase.Fixture.Library; - testCase.verifyEqual(typeEnum, openminds.enum.Types("License")) + testCase.verifyEqual(library.getTypeFromIRISegment("licenses"), ... + openminds.enum.Types("License")) + testCase.verifyEqual(library.getTypeFromIRISegment("contentTypes"), ... + openminds.enum.Types("ContentType")) end function testSingularIRISegmentResolvesToItsType(testCase) - typeEnum = ... - testCase.InstanceLibrary.getTypeFromIRISegment("parcellationEntity"); + library = testCase.Fixture.Library; - testCase.verifyEqual(typeEnum, openminds.enum.Types("ParcellationEntity")) + testCase.verifyEqual(library.getTypeFromIRISegment("ageCategory"), ... + openminds.enum.Types("AgeCategory")) + testCase.verifyEqual(library.getTypeFromIRISegment("parcellationEntity"), ... + openminds.enum.Types("ParcellationEntity")) 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.') + function testUnknownIRISegmentIsRejected(testCase) + % A segment that names no type in the library must raise this + % specific error, so that a caller can tell a bad IRI from a + % missing library. - % 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) + testCase.verifyError( ... + @() testCase.Fixture.Library.getTypeFromIRISegment("notASegment"), ... + 'OPENMINDS:InstanceLibrary:UnknownIRISegment') 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")) + function testADocumentWithoutATypeIsReported(testCase) + % A document whose "@type" cannot be read leaves its folder + % untyped. That is reported, naming the document, rather than + % skipped in silence like a folder that was never there. - testCase.verifyEqual(library.LibraryVersion, "v3.0") + folder = testCase.Fixture.Folder; + damagedFolder = fullfile(folder, "latest", "damaged"); + mkdir(damagedFolder) + fileId = fopen(fullfile(damagedFolder, "broken.jsonld"), "w"); + fprintf(fileId, "{ not a document"); + fclose(fileId); - % 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.') + library = testCase.verifyWarning( ... + @() openminds.internal.InstanceLibrary.getSingleton(folder, "Reset", true), ... + 'OPENMINDS:InstanceLibrary:UnreadableInstance'); - % 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.') + broken = library.InstanceTable(library.InstanceTable.InstanceName == "broken", :); + testCase.verifyEqual(broken.Type, "", ... + 'The damaged document must be listed without a type.') + testCase.verifyEqual(nnz(library.InstanceTable.Type ~= ""), 6, ... + 'The other documents must be typed as before.') 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); + function testAVersionWithoutInstancesIsReported(testCase) + % A version folder that holds no instance files reads as an empty + % library, with its columns in place, and warns rather than + % errors. Selecting a model version rebuilds the library the same + % way, and a library that cannot be read must not stop the + % selection. - testCase.verifyWarning(@() openminds.version("v1.0"), ... - 'OPENMINDS:InstanceLibrary:NoInstancesForModelVersion') + folder = testCase.Fixture.Folder; + rmdir(fullfile(folder, "latest"), "s") + mkdir(fullfile(folder, "latest")) - library = testCase.InstanceLibrary; - testCase.verifyTrue(ismissing(library.LibraryVersion), ... - 'No library version can be read for this model version.') + library = testCase.verifyWarning( ... + @() openminds.internal.InstanceLibrary.getSingleton(folder, "Reset", true), ... + 'OPENMINDS:InstanceLibrary:InstancesNotFound'); - % 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 - function testLibraryLocationSurvivesAWorkingDirectoryChange(testCase) - % The location is built under userpath. On a CI runner userpath is - % empty, because $HOME/Documents does not exist, and a path built - % under an empty userpath is relative. The library is read again on - % every model version change, so the location must stay valid after - % the working directory changes. - - library = testCase.InstanceLibrary; - testCase.assumeTrue(isfolder(library.InstanceLibraryLocation)) - - testCase.applyFixture( ... - matlab.unittest.fixtures.WorkingFolderFixture) - - testCase.verifyTrue(isfolder(library.InstanceLibraryLocation), ... - 'The location must still name the library from another folder.') - end - function testAMissingLocationIsRejectedByName(testCase) % The library is downloaded into its default location only, so a % location a caller names has to exist already. It is rejected % before the library in use is touched, so that library survives % the mistake. - library = testCase.InstanceLibrary; + fixture = testCase.Fixture; testCase.verifyError( ... @() openminds.internal.InstanceLibrary.getSingleton( ... @@ -207,7 +149,8 @@ function testAMissingLocationIsRejectedByName(testCase) 'OPENMINDS:InstanceLibrary:LocationNotFound') testCase.verifySameHandle( ... - openminds.internal.InstanceLibrary.getSingleton(), library) + openminds.internal.InstanceLibrary.getSingleton(fixture.Folder), ... + fixture.Library) end function testAFolderWithoutVersionsIsRejectedByName(testCase) @@ -217,7 +160,7 @@ function testAFolderWithoutVersionsIsRejectedByName(testCase) import matlab.unittest.fixtures.TemporaryFolderFixture - library = testCase.InstanceLibrary; + fixture = testCase.Fixture; emptyFolder = testCase.applyFixture(TemporaryFolderFixture).Folder; testCase.verifyError( ... @@ -225,24 +168,34 @@ function testAFolderWithoutVersionsIsRejectedByName(testCase) 'OPENMINDS:InstanceLibrary:LocationNotFound') testCase.verifySameHandle( ... - openminds.internal.InstanceLibrary.getSingleton(), library) + openminds.internal.InstanceLibrary.getSingleton(fixture.Folder), ... + fixture.Library) end - function testUnknownIRISegmentIsRejected(testCase) - % A segment that names no type in the library must raise this - % specific error, so that a caller can tell a bad IRI from a - % missing library. + function testLibraryLocationSurvivesAWorkingDirectoryChange(testCase) + % The location is built under userpath. On a CI runner userpath is + % empty, because $HOME/Documents does not exist, and a path built + % under an empty userpath is relative. The library is read again on + % every model version change, so the location must stay valid after + % the working directory changes. - testCase.verifyError( ... - @() testCase.InstanceLibrary.getTypeFromIRISegment("notASegment"), ... - 'OPENMINDS:InstanceLibrary:UnknownIRISegment') + library = testCase.Fixture.Library; + testCase.assertTrue(isfolder(library.InstanceLibraryLocation)) + + testCase.applyFixture( ... + matlab.unittest.fixtures.WorkingFolderFixture) + + testCase.verifyTrue(isfolder(library.InstanceLibraryLocation), ... + 'The location must still name the library from another folder.') end end methods (Access = private) - function instances = instancesOfType(testCase, typeName) - instances = testCase.InstanceTable( ... - testCase.InstanceTable.Type == typeName, :); + function row = instanceNamed(testCase, instanceName) + instanceTable = testCase.Fixture.Library.InstanceTable; + row = instanceTable(instanceTable.InstanceName == instanceName, :); + testCase.assertEqual(height(row), 1, ... + sprintf('Expected one instance named "%s" in the test library.', instanceName)) end end end