diff --git a/+tests/+abstract/NwbTestCase.m b/+tests/+abstract/NwbTestCase.m new file mode 100644 index 00000000..f51de524 --- /dev/null +++ b/+tests/+abstract/NwbTestCase.m @@ -0,0 +1,56 @@ +classdef (Abstract, SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + NwbTestCase < matlab.unittest.TestCase +% NwbTestCase - Abstract class providing a shared fixture, and utility +% methods for running tests dependent on generating neurodata type classes + + methods (Access = protected) + function typesOutputFolder = getTypesOutputFolder(testCase) + F = testCase.getSharedTestFixtures(); + isMatch = arrayfun(@(x) isa(x, 'tests.fixtures.GenerateCoreFixture'), F); + F = F(isMatch); + + typesOutputFolder = F.TypesOutputFolder; + end + + function installExtension(testCase, extensionName) + typesOutputFolder = testCase.getTypesOutputFolder(); + + % Use evalc to suppress output while running tests. + matlabExpression = sprintf(... + 'nwbInstallExtension("%s", "savedir", "%s")', ... + extensionName, typesOutputFolder); + evalc(matlabExpression); + end + + function clearExtension(testCase, extensionName) + extensionName = char(extensionName); + namespaceFolderName = strrep(extensionName, '-', '_'); + typesOutputFolder = testCase.getTypesOutputFolder(); + rmdir(fullfile(typesOutputFolder, '+types', ['+', namespaceFolderName]), 's') + delete(fullfile(typesOutputFolder, 'namespaces', [extensionName '.mat'])) + end + end + + methods (Static, Access = protected) + function [nwbFile, nwbFileCleanup] = readNwbFileWithPynwb(nwbFilename) + try + io = py.pynwb.NWBHDF5IO(nwbFilename); + nwbFile = io.read(); + nwbFileCleanup = onCleanup(@(x) closePyNwbObject(io)); + catch ME + error(ME.message) + end + + function closePyNwbObject(io) + io.close() + end + end + + function nwbFilename = getRandomFilename() + % Assumes that this method is called from a test method + functionCallStackTrace = dbstack(); + testName = regexp(functionCallStackTrace(2).name, '\w*$', 'match', 'once'); + nwbFilename = sprintf('%s_%05d.nwb', testName, randi(9999)); + end + end +end \ No newline at end of file diff --git a/+tests/+factory/ImagingPlane.m b/+tests/+factory/ImagingPlane.m new file mode 100644 index 00000000..a7c6e527 --- /dev/null +++ b/+tests/+factory/ImagingPlane.m @@ -0,0 +1,11 @@ +function imaging_plane = ImagingPlane(device) +% ImagingPlane - Create imaging plane with values for all required properties + arguments + device (1,1) types.core.Device % A device is required + end + imaging_plane = types.core.ImagingPlane( ... + 'device', types.untyped.SoftLink(device), ... + 'excitation_lambda', 600., ... + 'indicator', 'GFP', ... + 'location', 'my favorite brain location'); +end diff --git a/+tests/+factory/NWBFile.m b/+tests/+factory/NWBFile.m new file mode 100644 index 00000000..fc1afec6 --- /dev/null +++ b/+tests/+factory/NWBFile.m @@ -0,0 +1,9 @@ +function nwb = NWBFile() + currentTime = datetime("now", 'TimeZone', 'local'); + + nwb = NwbFile( ... + 'session_description', 'NWB File with required properties for unit testing', ... + 'identifier', 'test_file', ... + 'session_start_time', currentTime, ... + 'timestamps_reference_time', currentTime); +end diff --git a/+tests/+factory/TimeSeriesWithTimestamps.m b/+tests/+factory/TimeSeriesWithTimestamps.m new file mode 100644 index 00000000..1c7b71bc --- /dev/null +++ b/+tests/+factory/TimeSeriesWithTimestamps.m @@ -0,0 +1,6 @@ +function timeSeries = TimeSeriesWithTimestamps() + timeSeries = types.core.TimeSeries(... + 'data', rand(1,10), ... + 'timestamps', 1:10, ... + 'data_unit', 'test'); +end diff --git a/+tests/+fixtures/ExtensionGenerationFixture.m b/+tests/+fixtures/ExtensionGenerationFixture.m new file mode 100644 index 00000000..a538de19 --- /dev/null +++ b/+tests/+fixtures/ExtensionGenerationFixture.m @@ -0,0 +1,55 @@ +classdef ExtensionGenerationFixture < matlab.unittest.fixtures.Fixture +%EXTENSIONGENERATIONFIXTURE - Fixture for generating an NWB extension. +% +% EXTENSIONGENERATIONFIXTURE provides a fixture for generating extension code +% from an NWB specification's namespace file. When the testing framework +% sets up the fixture, it calls the generateExtension function to produce the +% necessary code in the specified output folder. When the framework tears down +% the fixture, it removes the generated files and associated cache data, +% ensuring that no artifacts remain from the test generation process. +% +% See also matlab.unittest.fixtures.Fixture generateExtension nwbClearGenerated + + properties + % TypesOutputFolder - Folder to output generated types for test + % classes that share this fixture + TypesOutputFolder (1,1) string + + % NamespaceFilepath - Path name for extension's namespace file + NamespaceFilepath (1,1) string + end + + methods + function fixture = ExtensionGenerationFixture(namespaceFilepath, outputFolder) + fixture.NamespaceFilepath = namespaceFilepath; + fixture.TypesOutputFolder = outputFolder; + end + end + + methods + function setup(fixture) + generateExtension(fixture.NamespaceFilepath, 'savedir', fixture.TypesOutputFolder); + fixture.addTeardown(@fixture.clearGenerated) + end + end + + methods (Access = protected) + function tf = isCompatible(fixtureA, fixtureB) + tf = strcmp(fixtureA.NamespaceFilepath, fixtureB.NamespaceFilepath) ... + && strcmp(fixtureA.TypesOutputFolder, fixtureB.TypesOutputFolder); + end + end + + methods (Access = private) + function clearGenerated(fixture) + [~, namespaceFilename] = fileparts(fixture.NamespaceFilepath); + namespaceName = extractBefore(namespaceFilename, '.'); + + generatedTypesDirectory = fullfile(fixture.TypesOutputFolder, "+types", "+"+namespaceName); + rmdir(generatedTypesDirectory, 's'); + + cacheFile = fullfile(fixture.TypesOutputFolder, "namespaces", namespaceName+".mat"); + delete(cacheFile) + end + end +end diff --git a/+tests/+fixtures/GenerateCoreFixture.m b/+tests/+fixtures/GenerateCoreFixture.m new file mode 100644 index 00000000..3f0190d6 --- /dev/null +++ b/+tests/+fixtures/GenerateCoreFixture.m @@ -0,0 +1,44 @@ +classdef GenerateCoreFixture < matlab.unittest.fixtures.Fixture +% GENERATECOREFIXTURE - Fixture for creating classes for NWB types in a temporary folder. +% +% GENERATECOREFIXTURE provides a fixture for generating classes for neurodata +% types from the from the core NWB specifications. When the testing framework +% sets up the fixture, it calls the generateCore function to produce the +% necessary code in a temporary output folder and add it to MATLAB's path. When +% the framework tears down the fixture, it clears all the classes and deletes +% the temporary folder, ensuring that no artifacts remain from the test process. +% +% See also matlab.unittest.fixtures.Fixture generateCore + properties + % TypesOutputFolder - Folder to output generated types for test + % classes that share this fixture + TypesOutputFolder (1,1) string + end + + methods + function setup(fixture) + import matlab.unittest.fixtures.PathFixture + import matlab.unittest.fixtures.TemporaryFolderFixture + import tests.fixtures.NwbClearGeneratedFixture + + % Use the NwbClearGeneratedFixture to clear all generated types + % from the MatNWB root directory in order to preventing path + % conflicts when generating new types in a temporary directory + fixture.applyFixture( NwbClearGeneratedFixture ) + + % Use a fixture to add the MatNWB folder to the search path + fixture.applyFixture( PathFixture( misc.getMatnwbDir() ) ); + + % Use a fixture to create a temporary working directory + F = fixture.applyFixture( TemporaryFolderFixture ); + + % Generate core types in the temporary folder and add to path + generateCore('savedir', F.Folder) + fixture.applyFixture( PathFixture(F.Folder) ); + + % Save the folder containing cached namespaces and NWB type classes + % on the fixture object + fixture.TypesOutputFolder = F.Folder; + end + end +end diff --git a/+tests/+fixtures/NwbClearGeneratedFixture.m b/+tests/+fixtures/NwbClearGeneratedFixture.m new file mode 100644 index 00000000..bb868a13 --- /dev/null +++ b/+tests/+fixtures/NwbClearGeneratedFixture.m @@ -0,0 +1,38 @@ +classdef NwbClearGeneratedFixture < matlab.unittest.fixtures.Fixture +% NwbClearGeneratedFixture - Fixture for clearing generated NWB classes. +% +% NwbClearGeneratedFixture provides a fixture for clearing all the +% generated classes for NWB types from the matnwb folder. When the fixture is +% set up, all generated class files for NWB types are deleted. When the +% fixture is torn down, generateCore is called to regenerate the classes for +% NWB types of the latest NWB version +% +% See also matlab.unittest.fixtures.Fixture generateCore nwbClearGenerated + + properties + TypesOutputFolder (1,1) string {mustBeFolder} = misc.getMatnwbDir + end + + methods + function fixture = NwbClearGeneratedFixture(outputFolder) + arguments + outputFolder (1,1) string {mustBeFolder} = misc.getMatnwbDir + end + fixture.TypesOutputFolder = outputFolder; + end + end + + methods + function setup(fixture) + fixture.addTeardown( ... + @() generateCore('savedir', fixture.TypesOutputFolder) ) + nwbClearGenerated(fixture.TypesOutputFolder) + end + end + + methods (Access = protected) + function tf = isCompatible(fixtureA, fixtureB) + tf = strcmp(fixtureA.TypesOutputFolder, fixtureB.TypesOutputFolder); + end + end +end diff --git a/+tests/+fixtures/ResetGeneratedTypesFixture.m b/+tests/+fixtures/ResetGeneratedTypesFixture.m deleted file mode 100644 index 312881c7..00000000 --- a/+tests/+fixtures/ResetGeneratedTypesFixture.m +++ /dev/null @@ -1,16 +0,0 @@ -classdef ResetGeneratedTypesFixture < matlab.unittest.fixtures.Fixture - % ResetGeneratedTypesFixture - Fixture for resetting generated NWB classes. - % - % ResetGeneratedTypesFixture clears all the generated classes for NWB - % types from the matnwb folder. When the fixture is set up, all generated - % class files for NWB types are deleted. When the fixture is torn down, - % generateCore is called to regenerate the classes for NWB types of the - % latest NWB version - - methods - function setup(fixture) - fixture.addTeardown( @generateCore ) - nwbClearGenerated() - end - end -end diff --git a/+tests/+fixtures/SetEnvironmentVariableFixture.m b/+tests/+fixtures/SetEnvironmentVariableFixture.m new file mode 100644 index 00000000..85af47ae --- /dev/null +++ b/+tests/+fixtures/SetEnvironmentVariableFixture.m @@ -0,0 +1,83 @@ +classdef SetEnvironmentVariableFixture < matlab.unittest.fixtures.Fixture +% UsesEnvironmentVariable Fixture for setting environment variables in tests. +% +% This fixture reads an environment file containing key-value pairs and +% sets the corresponding system environment variables prior to executing +% tests. The expected format for the environment file is: +% +% VARIABLE_NAME=VALUE +% +% Lines that are empty or start with '#' (comments) are ignored. +% +% The fixture first attempts to load environment variables from the file +% "nwbtest.env" located in the "+tests" folder. If "nwbtest.env" is not +% found, it falls back to "nwbtest.default.env". When using the default file, +% the fixture only applies environment variables if they are not present +% in the current list of environment variables. + + methods + function setup(fixture) %#ok + + applyFromFile = true; + envFilePath = fullfile(misc.getMatnwbDir, '+tests', 'nwbtest.env'); + + if ~isfile(envFilePath) + envFilePath = fullfile(misc.getMatnwbDir, '+tests', 'nwbtest.default.env'); + applyFromFile = false; + end + + if exist("loadenv", "file") == 2 + envVariables = loadenv(envFilePath); + else + envVariables = readEnvFile(envFilePath); + end + + envVariableNames = string( envVariables.keys() ); + if ~isrow(envVariableNames); envVariableNames = envVariableNames'; end + + for varName = envVariableNames + varValue = envVariables(varName); + if ~isenv(varName) + setenv(varName, varValue) + elseif applyFromFile && ~isempty(char(varValue)) + setenv(varName, varValue) + end + end + end + end +end + +function envMap = readEnvFile(filename) +% readEnvFile Reads an environment file into a containers.Map. +% +% envMap = readEnvFile(filename) reads the file specified by 'filename' +% and returns a containers.Map where each key is a variable name and each +% value is the corresponding value from the file. +% +% Lines starting with '#' or empty lines are ignored. + + envMap = containers.Map; + + fileContent = fileread(filename); + lines = strsplit(fileContent, newline); + + for i = 1:numel(lines) + line = lines{i}; + if isempty(line) || startsWith(line, '#') + continue; + end + + % Find the first occurrence of '=' + idx = strfind(line, '='); + if isempty(idx) + continue; % ignore line + end + + % Use the first '=' as the delimiter + key = strtrim(line(1:idx(1)-1)); + value = strtrim(line(idx(1)+1:end)); + + % Insert the key-value pair into the map + envMap(key) = value; + end +end diff --git a/+tests/+sanity/GenerationTest.m b/+tests/+sanity/GenerationTest.m index bf6442b9..52c43184 100644 --- a/+tests/+sanity/GenerationTest.m +++ b/+tests/+sanity/GenerationTest.m @@ -1,17 +1,21 @@ classdef GenerationTest < matlab.unittest.TestCase +% Note: Sometimes this test does not work for the first two schema versions. +% Restarting MATLAB can fix this. + properties (MethodSetupParameter) schemaVersion = listSchemaVersions() end methods (TestClassSetup) - function setupClass(testCase) + function setupMatNWBPathFixture(testCase) import matlab.unittest.fixtures.PathFixture - import tests.fixtures.ResetGeneratedTypesFixture - - rootPath = tests.util.getProjectDirectory(); - testCase.applyFixture( PathFixture(rootPath) ); + matNwbRootPath = tests.util.getProjectDirectory(); + testCase.applyFixture( PathFixture(matNwbRootPath) ); + end - testCase.applyFixture( ResetGeneratedTypesFixture ); + function setupNwbClearGeneratedFixture(testCase) + import tests.fixtures.NwbClearGeneratedFixture + testCase.applyFixture( NwbClearGeneratedFixture ); end end diff --git a/+tests/+unit/PynwbTutorialTest.m b/+tests/+system/+tutorial/PynwbTutorialTest.m similarity index 90% rename from +tests/+unit/PynwbTutorialTest.m rename to +tests/+system/+tutorial/PynwbTutorialTest.m index ee6e9807..26407504 100644 --- a/+tests/+unit/PynwbTutorialTest.m +++ b/+tests/+system/+tutorial/PynwbTutorialTest.m @@ -1,11 +1,10 @@ -classdef PynwbTutorialTest < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.SetEnvironmentVariableFixture}) ... + PynwbTutorialTest < matlab.unittest.TestCase % PynwbTutorialTest - Unit test for testing the pynwb tutorials. % % This test will test most pynwb tutorial files (while skipping tutorials with % dependencies) If the tutorial creates nwb file(s), the test will also try % to open these with matnwb. -% -% See also tests.util.getPythonPath properties MatNwbDirectory @@ -40,11 +39,16 @@ PythonEnvironment % Stores the value of the environment variable % "PYTHONPATH" to restore when test is finished. - Debug (1,1) logical = false + Debug (1,1) logical end methods (TestClassSetup) function setupClass(testCase) + + import tests.fixtures.NwbClearGeneratedFixture + + testCase.Debug = strcmp(getenv('NWB_TEST_DEBUG'), '1'); + % Get the root path of the matnwb repository rootPath = getMatNwbRootDirectory(); testCase.MatNwbDirectory = rootPath; @@ -52,7 +56,8 @@ function setupClass(testCase) % Use a fixture to add the folder to the search path testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - nwbClearGenerated() % Clear the generated schema classes + % Clear the generated schema classes + testCase.applyFixture(NwbClearGeneratedFixture) % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); @@ -69,11 +74,10 @@ function setupClass(testCase) L = dir('temp_venv/lib/python*/site-*'); % Find the site-packages folder pythonPath = fullfile(L.folder, L.name); setenv('PYTHONPATH', pythonPath) - - pythonPath = tests.util.getPythonPath(); if testCase.Debug - [~, m] = system(sprintf('%s -m pip list', pythonPath)); disp(m) + pythonExecutable = getenv("PYTHON_EXECUTABLE"); + [~, m] = system(sprintf('%s -m pip list', pythonExecutable)); disp(m) end end end @@ -106,12 +110,8 @@ function teardownMethod(testCase) %#ok methods (Test) function testTutorial(testCase, tutorialFile) - %S = pyenv(); - %pythonPath = S.Executable; - - pythonPath = tests.util.getPythonPath(); - - cmd = sprintf('%s %s', pythonPath, tutorialFile); + pythonExecutable = getenv("PYTHON_EXECUTABLE"); + cmd = sprintf('%s %s', pythonExecutable, tutorialFile); [status, cmdout] = system(cmd); if status == 1 @@ -136,7 +136,7 @@ function testReadTutorialNwbFileWithMatNwb(testCase) for i = 1:numel(nwbListing) nwbFilename = nwbListing(i).name; - if any(strcmp(nwbFilename, tests.unit.PynwbTutorialTest.SkippedFiles)) + if any(strcmp(nwbFilename, tests.system.tutorial.PynwbTutorialTest.SkippedFiles)) continue end @@ -148,19 +148,17 @@ function testReadTutorialNwbFileWithMatNwb(testCase) nwbFile = nwbRead(nwbFilename, 'savedir', '.'); %#ok catch ME error(ME.message) - %testCase.verifyFail(sprintf('Failed to read file %s with error: %s', nwbListing(i).name, ME.message)); end end end end methods (Access = private) % Utility functions - function createVirtualPythonEnvironment(testCase) - % Todo: Consider to use py.* - %py.venv.create('.', with_pip=true) + function createVirtualPythonEnvironment(testCase) %#ok + + pythonExecutable = getenv("PYTHON_EXECUTABLE"); - pythonPath = tests.util.getPythonPath(); - cmd = sprintf("%s -m venv ./temp_venv", pythonPath ); + cmd = sprintf("%s -m venv ./temp_venv", pythonExecutable ); [status, cmdout] = system(cmd); if ~status == 0 @@ -202,7 +200,7 @@ function installPythonDependencies(testCase) else token = ''; end - + allFilePaths = listFilesInRepo(... 'NeurodataWithoutBorders', 'pynwb', 'docs/gallery/', token); @@ -213,12 +211,12 @@ function installPythonDependencies(testCase) % Exclude skipped files. fileNames = strcat(fileNames(keep), '.py'); - [~, iA] = setdiff(fileNames, tests.unit.PynwbTutorialTest.SkippedTutorials, 'stable'); + [~, iA] = setdiff(fileNames, tests.system.tutorial.PynwbTutorialTest.SkippedTutorials, 'stable'); tutorialNames = allFilePaths(iA); end function folderPath = getMatNwbRootDirectory() - folderPath = fileparts(fileparts(fileparts(mfilename('fullpath')))); + folderPath = fileparts(fileparts(fileparts(fileparts(mfilename('fullpath'))))); end function pynwbFolder = downloadPynwb() diff --git a/+tests/+unit/TutorialTest.m b/+tests/+system/+tutorial/TutorialTest.m similarity index 93% rename from +tests/+unit/TutorialTest.m rename to +tests/+system/+tutorial/TutorialTest.m index 554c0cb2..a2882e8a 100644 --- a/+tests/+unit/TutorialTest.m +++ b/+tests/+system/+tutorial/TutorialTest.m @@ -1,4 +1,5 @@ -classdef TutorialTest < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture, tests.fixtures.SetEnvironmentVariableFixture}) ... + TutorialTest < matlab.unittest.TestCase % TutorialTest - Unit test for testing the matnwb tutorials. % % This test will test most tutorial files (while skipping tutorials with @@ -52,8 +53,6 @@ methods (TestClassSetup) function setupClass(testCase) - import tests.fixtures.ResetGeneratedTypesFixture - % Get the root path of the matnwb repository rootPath = tests.util.getProjectDirectory(); tutorialsFolder = fullfile(rootPath, 'tutorials'); @@ -61,7 +60,7 @@ function setupClass(testCase) testCase.MatNwbDirectory = rootPath; % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); + %testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); testCase.applyFixture(matlab.unittest.fixtures.PathFixture(tutorialsFolder)); % Check if it is possible to call py.nwbinspector.* functions. @@ -72,15 +71,13 @@ function setupClass(testCase) catch testCase.NWBInspectorMode = "CLI"; end - - testCase.applyFixture( ResetGeneratedTypesFixture ); end end methods (TestMethodSetup) function setupMethod(testCase) testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); + %generateCore('savedir', '.'); end end @@ -120,7 +117,8 @@ function inspectTutorialFileWithNwbInspector(testCase) results = py.list(py.nwbinspector.inspect_nwbfile(nwbfile_path=nwbFilename)); results = testCase.convertNwbInspectorResultsToStruct(results); elseif testCase.NWBInspectorMode == "CLI" - [s, m] = system(sprintf('nwbinspector %s --levels importance', nwbFilename)); + nwbInspectorExecutable = getenv("NWBINSPECTOR_EXECUTABLE"); + [s, m] = system(sprintf('%s %s --levels importance', nwbInspectorExecutable, nwbFilename)); testCase.assertEqual(s,0, 'Failed to run NWB Inspector using system command.') results = testCase.parseNWBInspectorTextOutput(m); end @@ -158,7 +156,7 @@ function inspectTutorialFileWithNwbInspector(testCase) methods (Static) function resultsOut = convertNwbInspectorResultsToStruct(resultsIn) - resultsOut = tests.unit.TutorialTest.getEmptyNwbInspectorResultStruct(); + resultsOut = tests.system.tutorial.TutorialTest.getEmptyNwbInspectorResultStruct(); C = cell(resultsIn); for i = 1:numel(C) @@ -178,7 +176,7 @@ function inspectTutorialFileWithNwbInspector(testCase) end function resultsOut = parseNWBInspectorTextOutput(systemCommandOutput) - resultsOut = tests.unit.TutorialTest.getEmptyNwbInspectorResultStruct(); + resultsOut = tests.system.tutorial.TutorialTest.getEmptyNwbInspectorResultStruct(); importanceLevels = containers.Map(... ["BEST_PRACTICE_SUGGESTION", ... @@ -264,5 +262,5 @@ function inspectTutorialFileWithNwbInspector(testCase) ); L( [L.isdir] ) = []; % Ignore folders - tutorialNames = setdiff({L.name}, tests.unit.TutorialTest.SkippedTutorials); + tutorialNames = setdiff({L.name}, tests.system.tutorial.TutorialTest.SkippedTutorials); end diff --git a/+tests/+system/AlignedSpikeTimesUtilityTest.m b/+tests/+system/AlignedSpikeTimesUtilityTest.m index 7f885c85..221e4fb7 100644 --- a/+tests/+system/AlignedSpikeTimesUtilityTest.m +++ b/+tests/+system/AlignedSpikeTimesUtilityTest.m @@ -1,8 +1,9 @@ -classdef AlignedSpikeTimesUtilityTest < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + AlignedSpikeTimesUtilityTest < matlab.unittest.TestCase + methods (TestMethodSetup) function setupMethod(testCase) testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); end end methods (Test) diff --git a/+tests/+system/DynamicTableTest.m b/+tests/+system/DynamicTableTest.m index 43dbe6f4..d4fd2099 100644 --- a/+tests/+system/DynamicTableTest.m +++ b/+tests/+system/DynamicTableTest.m @@ -206,13 +206,17 @@ function getRowTest(testCase) end function getRowRoundtripTest(testCase) + import matlab.unittest.fixtures.SuppressedWarningsFixture + suppressedWarningId = 'NWB:DynamicTable:VectorDataAmbiguousSize'; + testCase.applyFixture(SuppressedWarningsFixture(suppressedWarningId)) + filename = ['MatNWB.' testCase.className() '.testGetRow.nwb']; nwbExport(testCase.file, filename); ActualFile = nwbRead(filename, 'ignorecache'); ActualTable = ActualFile.intervals_trials; ExpectedTable = testCase.file.intervals_trials; - % even if struct is passed in. It is still read back as a + % Even if struct is passed in. It is still read back as a % table. So we cheat a bit here since this is expected a2a. CompoundStructVector = ExpectedTable.vectordata.get('compound_struct'); ExpectedCompoundStruct = CompoundStructVector.data; diff --git a/+tests/+system/NWBFileIOTest.m b/+tests/+system/NWBFileIOTest.m index 2772a485..f0954188 100644 --- a/+tests/+system/NWBFileIOTest.m +++ b/+tests/+system/NWBFileIOTest.m @@ -1,4 +1,5 @@ classdef NWBFileIOTest < tests.system.PyNWBIOTest + methods function addContainer(testCase, file) %#ok ts = types.core.TimeSeries(... @@ -44,7 +45,7 @@ function testLoadAll(testCase) fileName = ['MatNWB.' testCase.className() '.testLoadAll.nwb']; nwbExport(testCase.file, fileName) nwb = nwbRead(fileName, "ignorecache"); - nwb.loadAll() + nwb.loadAll(); end function readWithStringArg(testCase) @@ -60,7 +61,7 @@ function readFileWithoutSpec(testCase) io.internal.h5.deleteGroup(fileName, 'specifications') - nwbRead(fileName); + nwbRead(fileName, "ignorecache"); end function readFileWithoutSpecLoc(testCase) @@ -72,7 +73,7 @@ function readFileWithoutSpecLoc(testCase) % When specloc is missing, the specifications are not added to % the blacklist, so it will get passed as an input to NwbFile. - testCase.verifyError(@(fn) nwbRead(fileName), 'MATLAB:TooManyInputs'); + testCase.verifyError(@(fn) nwbRead(fileName, "ignorecache"), 'MATLAB:TooManyInputs'); end function readFileWithUnsupportedVersion(testCase) @@ -85,7 +86,7 @@ function readFileWithUnsupportedVersion(testCase) io.writeAttribute(file_id, '/nwb_version', '1.0.0') H5F.close(file_id); - testCase.verifyWarning(@(fn) nwbRead(fileName), 'NWB:Read:UnsupportedSchema') + testCase.verifyWarning(@(fn) nwbRead(fileName, "ignorecache"), 'NWB:Read:UnsupportedSchema') end function readFileWithUnsupportedVersionAndNoSpecloc(testCase) @@ -104,7 +105,7 @@ function readFileWithUnsupportedVersionAndNoSpecloc(testCase) % When specloc is missing, the specifications are not added to % the blacklist, so it will get passed as an input to NwbFile. - testCase.verifyError(@(fn) nwbRead(fileName), 'MATLAB:TooManyInputs'); + testCase.verifyError(@(fn) nwbRead(fileName, "ignorecache"), 'MATLAB:TooManyInputs'); end end end diff --git a/+tests/+system/NwbTestInterface.m b/+tests/+system/NwbTestInterface.m index 5ffa9510..c961ab34 100644 --- a/+tests/+system/NwbTestInterface.m +++ b/+tests/+system/NwbTestInterface.m @@ -1,4 +1,6 @@ -classdef NwbTestInterface < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + NwbTestInterface < matlab.unittest.TestCase + properties % registry file @@ -7,16 +9,12 @@ methods (TestClassSetup) function setupClass(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - testCase.root = rootPath; end end methods (TestMethodSetup) function setupMethod(testCase) testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); testCase.file = NwbFile( ... 'session_description', 'a test NWB File', ... 'identifier', 'TEST123', ... diff --git a/+tests/+system/PhotonSeriesIOTest.m b/+tests/+system/PhotonSeriesIOTest.m index 96bc4bae..7bedc9b5 100644 --- a/+tests/+system/PhotonSeriesIOTest.m +++ b/+tests/+system/PhotonSeriesIOTest.m @@ -16,7 +16,7 @@ function addContainer(testCase, file) %#ok 'location', 'somewhere in the brain'); tps = types.core.TwoPhotonSeries( ... - 'data', ones(3,3,3), ... + 'data', ones(3,3,10), ... 'imaging_plane', types.untyped.SoftLink(ip), ... 'data_unit', 'image_unit', ... 'format', 'raw', ... diff --git a/+tests/+system/PyNWBIOTest.m b/+tests/+system/PyNWBIOTest.m index fc3ceb32..1a36e6a1 100644 --- a/+tests/+system/PyNWBIOTest.m +++ b/+tests/+system/PyNWBIOTest.m @@ -1,4 +1,5 @@ -classdef PyNWBIOTest < tests.system.RoundTripTest +classdef (SharedTestFixtures = {tests.fixtures.SetEnvironmentVariableFixture}) ... + PyNWBIOTest < tests.system.RoundTripTest % Assumes PyNWB and unittest2 has been installed on the system. % % To install PyNWB, execute: @@ -36,22 +37,11 @@ function testInFromPyNWB(testCase) function [status, cmdout] = runPyTest(testCase, testName) tests.util.addFolderToPythonPath( fileparts(mfilename('fullpath')) ) - envPath = fullfile('+tests', 'env.mat'); - if isfile(envPath) - Env = load(envPath, '-mat'); - if isfield(Env, 'pythonPath') - pythonPath = Env.pythonPath; - else - pythonPath = fullfile(Env.pythonDir, 'python'); - end - else - pythonPath = 'python'; - end - + pythonExecutable = getenv("PYTHON_EXECUTABLE"); cmd = sprintf('"%s" -B -m unittest %s.%s.%s',... - pythonPath,... + pythonExecutable,... 'PyNWBIOTest', testCase.className(), testName); [status, cmdout] = system(cmd); end end -end \ No newline at end of file +end diff --git a/+tests/+system/PyNWBIOTest.py b/+tests/+system/PyNWBIOTest.py index 212a6b79..27645f59 100644 --- a/+tests/+system/PyNWBIOTest.py +++ b/+tests/+system/PyNWBIOTest.py @@ -183,7 +183,7 @@ def addContainer(self, file): indicator = 'GFP', location = 'somewhere in the brain', imaging_rate = 2.718) - data = np.ones((3, 3, 3)) + data = np.ones((10, 3, 3)) timestamps = list(range(10)) fov = [2.0, 2.0, 5.0] tps = TwoPhotonSeries('test_2ps', ip, data, 'image_unit', 'raw', diff --git a/+tests/+system/SmokeTest.m b/+tests/+system/SmokeTest.m new file mode 100644 index 00000000..ebc294f5 --- /dev/null +++ b/+tests/+system/SmokeTest.m @@ -0,0 +1,49 @@ +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + SmokeTest < matlab.unittest.TestCase + + methods (TestMethodSetup) + function setup(testCase) + % This method runs before each test method. + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end + + methods (Test) + %TODO rewrite namespace instantiation check + function testSmokeInstantiateCore(testCase) + % classes = fieldnames(testCase.TestData.registry); + % for i = 1:numel(classes) + % c = classes{i}; + % try + % types.(c); + % catch e + % testCase.verifyFail(['Could not instantiate types.' c ' : ' e.message]); + % end + % end + end + + function testSmokeReadWrite(testCase) + % Create a TimeIntervals object + epochs = types.core.TimeIntervals( ... + 'colnames', {'start_time'; 'stop_time'} , ... + 'id', types.hdmf_common.ElementIdentifiers('data', 1), ... + 'description', 'test TimeIntervals', ... + 'start_time', types.hdmf_common.VectorData('data', 0, 'description', 'start time'), ... + 'stop_time', types.hdmf_common.VectorData('data', 1, 'description', 'stop time')); + + % Create an NwbFile and export + file = NwbFile( ... + 'identifier', 'st', ... + 'session_description', 'smokeTest', ... + 'session_start_time', datetime, ... + 'intervals_epochs', epochs, ... + 'timestamps_reference_time', datetime); + nwbExport(file, 'epoch.nwb'); + + % Read the file back + readFile = nwbRead('epoch.nwb', 'ignorecache'); + + tests.util.verifyContainerEqual(testCase, readFile, file); + end + end +end diff --git a/+tests/+system/smokeTest.m b/+tests/+system/smokeTest.m deleted file mode 100644 index 9366ea09..00000000 --- a/+tests/+system/smokeTest.m +++ /dev/null @@ -1,45 +0,0 @@ -function tests = smokeTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -rehash(); -end - -%TODO rewrite namespace instantiation check -function testSmokeInstantiateCore(testCase) -% classes = fieldnames(testCase.TestData.registry); -% for i = 1:numel(classes) -% c = classes{i}; -% try -% types.(c); -% catch e -% testCase.verifyFail(['Could not instantiate types.' c ' : ' e.message]); -% end -% end -end - -function testSmokeReadWrite(testCase) -epochs = types.core.TimeIntervals(... - 'colnames', {'start_time' 'stop_time'} .',... - 'id', types.hdmf_common.ElementIdentifiers('data', 1),... - 'description', 'test TimeIntervals',... - 'start_time', types.hdmf_common.VectorData('data', 0, 'description', 'start time'),... - 'stop_time', types.hdmf_common.VectorData('data', 1, 'description', 'stop time')); -file = NwbFile('identifier', 'st', 'session_description', 'smokeTest', ... - 'session_start_time', datetime, 'intervals_epochs', epochs,... - 'timestamps_reference_time', datetime); - -nwbExport(file, 'epoch.nwb'); -readFile = nwbRead('epoch.nwb', 'ignorecache'); -% testCase.verifyEqual(testCase, readFile, file, ... -% 'Could not write and then read a simple file'); -tests.util.verifyContainerEqual(testCase, readFile, file); -end \ No newline at end of file diff --git a/+tests/+unit/+abstract/SchemaTest.m b/+tests/+unit/+abstract/SchemaTest.m new file mode 100644 index 00000000..abf085bf --- /dev/null +++ b/+tests/+unit/+abstract/SchemaTest.m @@ -0,0 +1,52 @@ +classdef (Abstract, SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + SchemaTest < matlab.unittest.TestCase +% SCHEMATEST - Abstract class for generating and testing test-schemas. +% +% Subclasses must implement the abstract properties: +% - SchemaFolder +% - SchemaNamespaceFileName +% +% Example subclasses are found in the "tests.unit.schema" namespace +% +% See also: matlab.unittest.TestCase, tests.fixtures.GenerateCoreFixture + + properties (Constant, Abstract) + SchemaFolder % Name of folder containing the schema definition files + SchemaNamespaceFileName % The filename of the specification's namespace file + end + + properties (Constant) + % SchemaRootDirectory - Root directory for test schemas + SchemaRootDirectory = fullfile(misc.getMatnwbDir(), '+tests', 'test-schema') + end + + methods (TestClassSetup) + function setup(testCase) + % SETUP Performs fixture setup at the class level + + import tests.fixtures.ExtensionGenerationFixture + + F = testCase.getSharedTestFixtures(); + isMatch = arrayfun(@(x) isa(x, 'tests.fixtures.GenerateCoreFixture'), F); + F = F(isMatch); + + typesOutputFolder = F.TypesOutputFolder; + + namespaceFilePath = fullfile( ... + testCase.SchemaRootDirectory, ... + testCase.SchemaFolder, ... + testCase.SchemaNamespaceFileName); + + testCase.applyFixture( ... + ExtensionGenerationFixture(namespaceFilePath, typesOutputFolder) ) + end + end + + methods (TestMethodSetup) + function setupMethod(testCase) + % SETUPMETHOD Applies a WorkingFolderFixture before each test + % Ensures every test method runs in its own temporary working folder + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end +end diff --git a/+tests/+unit/+file/CloneNwbTest.m b/+tests/+unit/+file/CloneNwbTest.m index 1aeb11ab..93c27698 100644 --- a/+tests/+unit/+file/CloneNwbTest.m +++ b/+tests/+unit/+file/CloneNwbTest.m @@ -1,17 +1,10 @@ -classdef CloneNwbTest < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + CloneNwbTest < matlab.unittest.TestCase methods (TestClassSetup) function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - - generateCore('savedir', '.') end end @@ -36,7 +29,7 @@ function testCloneNwbFile(testCase) file.cloneNwbFileClass(fullfile('NwbFile'), 'MyCustomNwbFile') testCase.verifyTrue( isfile(fullfile(misc.getMatnwbDir(), 'NwbFile.m')) ) - + nwbFile = NwbFile(); nwbFile.general_experimenter = "Mouse McMouse"; C = evalc('nwbFile.sayHello()'); diff --git a/+tests/+unit/+io/+internal/+h5/MustBeH5FileTest.m b/+tests/+unit/+io/+internal/+h5/MustBeH5FileTest.m index be660682..1f18ab70 100644 --- a/+tests/+unit/+io/+internal/+h5/MustBeH5FileTest.m +++ b/+tests/+unit/+io/+internal/+h5/MustBeH5FileTest.m @@ -1,4 +1,5 @@ -classdef MustBeH5FileTest < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + MustBeH5FileTest < matlab.unittest.TestCase properties (TestParameter) ValidFileName = {'test_file.h5', 'test_file.nwb'} @@ -23,7 +24,8 @@ function createTestFiles(testCase) testCase.createNwbFile(nwbFileName) % Create file which is not h5 - system( sprintf("touch %s", testCase.InvalidFileName{1}) ) + s = system( sprintf("touch %s", testCase.InvalidFileName{1}) ); + assert(s==0) end end @@ -80,10 +82,7 @@ function createH5File(filePath) end function createNwbFile(filePath) - nwbFile = NwbFile( ... - 'session_description', 'Test file for nwb export', ... - 'identifier', 'export_test', ... - 'session_start_time', datetime("now", 'TimeZone', 'local') ); + nwbFile = tests.factory.NWBFile(); nwbExport(nwbFile, filePath) end end diff --git a/+tests/+unit/+io/WriteTest.m b/+tests/+unit/+io/WriteTest.m index ead733b0..f6a1cacc 100644 --- a/+tests/+unit/+io/WriteTest.m +++ b/+tests/+unit/+io/WriteTest.m @@ -16,7 +16,7 @@ function testWriteBooleanAttribute(testCase) fileCleanupObj = onCleanup(@(id) H5F.close(fid)); targetPath = '/'; - io.writeGroup(fid, targetPath) + io.writeGroup(fid, targetPath); % Define target dataset path and create it in the HDF5 file io.writeAttribute(fid, '/test', true); % First write to create the dataset diff --git a/+tests/+unit/+io/testCreateParsedType.m b/+tests/+unit/+io/testCreateParsedType.m index 7980162f..f66a183a 100644 --- a/+tests/+unit/+io/testCreateParsedType.m +++ b/+tests/+unit/+io/testCreateParsedType.m @@ -1,35 +1,36 @@ -function tests = testCreateParsedType() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.') -end +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + testCreateParsedType < matlab.unittest.TestCase -function testCreateTypeWithValidInputs(testCase) - testPath = 'some/dataset/path'; - testType = 'types.hdmf_common.VectorIndex'; - kwargs = {'description', 'this is a test'}; + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end - type = io.createParsedType(testPath, testType, kwargs{:}); - testCase.verifyClass(type, testType) + methods (Test) + function testCreateTypeWithValidInputs(testCase) + testPath = 'some/dataset/path'; + testType = 'types.hdmf_common.VectorIndex'; + kwargs = {'description', 'this is a test'}; + + type = io.createParsedType(testPath, testType, kwargs{:}); + testCase.verifyClass(type, testType) + + testCase.verifyWarningFree(... + @(varargin)io.createParsedType(testPath, testType, kwargs{:})) + end + + function testCreateTypeWithInvalidInputs(testCase) + testPath = 'some/dataset/path'; + testType = 'types.hdmf_common.VectorIndex'; + kwargs = {'description', 'this is a test', 'comment', 'this is another test'}; + + type = testCase.verifyWarning(... + @(varargin) io.createParsedType(testPath, testType, kwargs{:}), ... + 'NWB:CheckUnset:InvalidProperties'); - testCase.verifyWarningFree(... - @(varargin)io.createParsedType(testPath, testType, kwargs{:})) + testCase.verifyClass(type, testType) + end + end end - -function testCreateTypeWithInvalidInputs(testCase) - testPath = 'some/dataset/path'; - testType = 'types.hdmf_common.VectorIndex'; - kwargs = {'description', 'this is a test', 'comment', 'this is another test'}; - type = io.createParsedType(testPath, testType, kwargs{:}); - testCase.verifyClass(type, testType) - - testCase.verifyWarning(... - @(varargin)io.createParsedType(testPath, testType, kwargs{:}), ... - 'NWB:CheckUnset:InvalidProperties') -end \ No newline at end of file diff --git a/+tests/+unit/+schema/AnonTest.m b/+tests/+unit/+schema/AnonTest.m new file mode 100644 index 00000000..2eac2b12 --- /dev/null +++ b/+tests/+unit/+schema/AnonTest.m @@ -0,0 +1,27 @@ +classdef AnonTest < tests.unit.abstract.SchemaTest + + properties (Constant) + SchemaFolder = "anonSchema" + SchemaNamespaceFileName = "anon.namespace.yaml" + end + + methods (Test) + function testAnonDataset(testCase) + ag = types.anon.AnonGroup('ad', types.anon.AnonData('data', 0)); + nwbExpected = NwbFile(... + 'identifier', 'ANON',... + 'session_description', 'anonymous class schema testing',... + 'session_start_time', datetime()); + nwbExpected.acquisition.set('ag', ag); + nwbExport(nwbExpected, 'testanon.nwb'); + + tests.util.verifyContainerEqual(testCase, nwbRead('testanon.nwb', 'ignorecache'), nwbExpected); + end + + function testAnonTypeWithNameValueInput(testCase) + anon = types.untyped.Anon('a', 1); + testCase.verifyEqual(anon.name, 'a') + testCase.verifyEqual(anon.value, 1) + end + end +end diff --git a/+tests/+unit/+schema/BoolTest.m b/+tests/+unit/+schema/BoolTest.m new file mode 100644 index 00000000..33cd3934 --- /dev/null +++ b/+tests/+unit/+schema/BoolTest.m @@ -0,0 +1,27 @@ +classdef BoolTest < tests.unit.abstract.SchemaTest + + properties (Constant) + SchemaFolder = "boolSchema" + SchemaNamespaceFileName = "bool.namespace.yaml" + end + + methods (Test) + function testIo(testCase) + nwb = NwbFile(... + 'identifier', 'BOOL',... + 'session_description', 'test bool',... + 'session_start_time', datetime()); + boolContainer = types.bool.BoolContainer(... + 'data', logical(randi([0,1], 100, 1)), ... + 'attribute', false); + scalarBoolContainer = types.bool.BoolContainer(... + 'data', false, ... + 'attribute', true); + nwb.acquisition.set('bool', boolContainer); + nwb.acquisition.set('scalarbool', scalarBoolContainer); + nwb.export('test.nwb'); + nwbActual = nwbRead('test.nwb', 'ignorecache'); + tests.util.verifyContainerEqual(testCase, nwbActual, nwb); + end + end +end diff --git a/+tests/+unit/+schema/MultipleConstrainedTest.m b/+tests/+unit/+schema/MultipleConstrainedTest.m new file mode 100644 index 00000000..c6ef568f --- /dev/null +++ b/+tests/+unit/+schema/MultipleConstrainedTest.m @@ -0,0 +1,24 @@ +classdef MultipleConstrainedTest < tests.unit.abstract.SchemaTest + + properties (Constant) + SchemaFolder = "multipleConstrainedSchema" + SchemaNamespaceFileName = "mcs.namespace.yaml" + end + + methods (Test) + function testRoundabout(testCase) + multiSet = types.mcs.MultiSetContainer(); + multiSet.something.set('A', types.mcs.ArbitraryTypeA()); + multiSet.something.set('B', types.mcs.ArbitraryTypeB()); + multiSet.something.set('Data', types.mcs.DatasetType('data', ones(3,3))); + nwbExpected = NwbFile(... + 'identifier', 'MCS', ... + 'session_description', 'multiple constrained schema testing', ... + 'session_start_time', datetime()); + nwbExpected.acquisition.set('multiset', multiSet); + nwbExport(nwbExpected, 'testmcs.nwb'); + + tests.util.verifyContainerEqual(testCase, nwbRead('testmcs.nwb', 'ignorecache'), nwbExpected); + end + end +end diff --git a/+tests/+unit/+schema/MultipleShapesTest.m b/+tests/+unit/+schema/MultipleShapesTest.m new file mode 100644 index 00000000..e9df8033 --- /dev/null +++ b/+tests/+unit/+schema/MultipleShapesTest.m @@ -0,0 +1,58 @@ +classdef MultipleShapesTest < tests.unit.abstract.SchemaTest + + properties (Constant) + SchemaFolder = "multipleShapesSchema" + SchemaNamespaceFileName = "mss.namespace.yaml" + end + + methods (Test) + function testMultipleShapesDataset(testCase) + msd = types.mss.MultiShapeDataset('data', rand(3, 1)); + msd.data = rand(1, 5, 7); + testCase.roundabout(msd); + end + + function testNullShapeDataset(testCase) + nsd = types.mss.NullShapeDataset; + randiMax = intmax('int8') - 1; + for i=1:100 + %test validation + nsd.data = rand(randi(randiMax) + 1, 3); + end + testCase.roundabout(nsd); + end + + function testMultipleNullShapesDataset(testCase) + mnsd = types.mss.MultiNullShapeDataset; + randiMax = intmax('int8'); + for i=1:100 + if rand() > 0.5 + mnsd.data = rand(randi(randiMax), 1); + else + mnsd.data = rand(randi(randiMax), randi(randiMax)); + end + end + testCase.roundabout(mnsd); + end + + function testInheritedDtypeDataset(testCase) + nid = types.mss.NarrowInheritedDataset; + nid.data = 'Inherited Dtype Dataset'; + testCase.roundabout(nid); + end + end + + methods (Access = private) + %% Convenience + function roundabout(testCase, dataset) + nwb = NwbFile('identifier', 'MSS', 'session_description', 'test',... + 'session_start_time', '2017-04-15T12:00:00.000000-08:00',... + 'timestamps_reference_time', '2017-04-15T12:00:00.000000-08:00'); + wrapper = types.mss.MultiShapeWrapper('shaped_data', dataset); + nwb.acquisition.set('wrapper', wrapper); + filename = 'multipleShapesTest.nwb'; + nwbExport(nwb, filename); + tests.util.verifyContainerEqual(testCase, nwbRead(filename, 'ignorecache'), nwb); + end + end +end diff --git a/+tests/+unit/+schema/RegionViewTest.m b/+tests/+unit/+schema/RegionViewTest.m new file mode 100644 index 00000000..5048268b --- /dev/null +++ b/+tests/+unit/+schema/RegionViewTest.m @@ -0,0 +1,55 @@ +classdef RegionViewTest < tests.unit.abstract.SchemaTest + + properties (Constant) + SchemaFolder = "regionReferenceSchema" + SchemaNamespaceFileName = "rrs.namespace.yaml" + end + + properties (Constant, Access = private) + NumRegionViewsToTest = 5 + end + + methods (Test) + function testRegionViewIo(testCase) + nwb = NwbFile(... + 'identifier', 'REGIONREF',... + 'session_description', 'region ref test',... + 'session_start_time', datetime()); + + rcContainer = types.rrs.RefContainer(... + 'data', types.rrs.RefData('data', rand(10, 10, 10, 10, 10))); + nwb.acquisition.set('refdata', rcContainer); + + for i = 1:testCase.NumRegionViewsToTest + rcAttrRef = types.untyped.RegionView(... + rcContainer.data,... + randi(10),... + randi(10),... + randi(10),... + randi(10),... + randi(10)); + rcDataRef = types.untyped.RegionView(... + rcContainer.data,... + 1:randi(10),... + 1:randi(10),... + 1:randi(10),... + 1:randi(10),... + 1:randi(10)); + nwb.acquisition.set(sprintf('ref%d', i),... + types.rrs.ContainerReference(... + 'attribute_regref', rcAttrRef,... + 'data_regref', rcDataRef)); + end + nwb.export('test.nwb'); + nwbActual = nwbRead('test.nwb', 'ignorecache'); + tests.util.verifyContainerEqual(testCase, nwbActual, nwb); + + for i = 1:testCase.NumRegionViewsToTest + refName = sprintf('ref%d', i); + reference = nwb.acquisition.get(refName); + testCase.verifyEqual(reference.attribute_regref.refresh(nwb),... + reference.attribute_regref.refresh(nwbActual)); + end + end + end +end diff --git a/+tests/+unit/+types/FunctionTests.m b/+tests/+unit/+types/FunctionTests.m index 2adce77a..3b1cde12 100644 --- a/+tests/+unit/+types/FunctionTests.m +++ b/+tests/+unit/+types/FunctionTests.m @@ -1,17 +1,11 @@ -classdef FunctionTests < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + FunctionTests < matlab.unittest.TestCase % FunctionTests - Unit test for functions in +types namespace. methods (TestClassSetup) function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - - generateCore('savedir', '.') end end methods (Test) @@ -80,7 +74,7 @@ function testCheckDtype(testCase) ); testCase.verifyClass(stimuli, 'types.core.IntracellularStimuliTable') end - + function testParseConstrainedAppendMode(testCase) columnA = types.hdmf_common.VectorData( ... diff --git a/+tests/+unit/FunctionTests.m b/+tests/+unit/FunctionTests.m index 3a77bc27..55e528ee 100644 --- a/+tests/+unit/FunctionTests.m +++ b/+tests/+unit/FunctionTests.m @@ -1,17 +1,11 @@ -classdef FunctionTests < matlab.unittest.TestCase +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + FunctionTests < matlab.unittest.TestCase % FunctionTests - Unit test for functions. methods (TestClassSetup) function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); end end diff --git a/+tests/+unit/InstallExtensionTest.m b/+tests/+unit/InstallExtensionTest.m index db7e1826..fb58e772 100644 --- a/+tests/+unit/InstallExtensionTest.m +++ b/+tests/+unit/InstallExtensionTest.m @@ -1,16 +1,10 @@ -classdef InstallExtensionTest < matlab.unittest.TestCase - +classdef InstallExtensionTest < tests.abstract.NwbTestCase + methods (TestClassSetup) function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); + testCase.addTeardown(@() testCase.clearExtension("ndx-miniscope")) end end @@ -22,14 +16,16 @@ function testInstallExtensionFailsWithNoInputArgument(testCase) end function testInstallExtension(testCase) - nwbInstallExtension("ndx-miniscope", 'savedir', '.') + testCase.installExtension("ndx-miniscope"); - testCase.verifyTrue(isfolder('./+types/+ndx_miniscope'), ... + typesOutputFolder = testCase.getTypesOutputFolder(); + extensionTypesFolder = fullfile(typesOutputFolder, "+types", "+ndx_miniscope"); + testCase.verifyTrue(isfolder(extensionTypesFolder), ... 'Folder with extension types does not exist') end function testUseInstalledExtension(testCase) - nwbObject = testCase.initNwbFile(); + nwbObject = tests.factory.NWBFile(); miniscopeDevice = types.ndx_miniscope.Miniscope(... 'deviceType', 'test_device', ... @@ -75,13 +71,4 @@ function testBuildRepoDownloadUrl(testCase) 'NWB:BuildRepoDownloadUrl:UnsupportedRepository') end end - - methods (Static) - function nwb = initNwbFile() - nwb = NwbFile( ... - 'session_description', 'test file for nwb extension', ... - 'identifier', 'export_test', ... - 'session_start_time', datetime("now", 'TimeZone', 'local') ); - end - end end diff --git a/+tests/+unit/WarningsTest.m b/+tests/+unit/WarningsTest.m index 683e286a..393ea17f 100644 --- a/+tests/+unit/WarningsTest.m +++ b/+tests/+unit/WarningsTest.m @@ -1,18 +1,5 @@ -classdef WarningsTest < matlab.unittest.TestCase - - methods (TestClassSetup) - function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - - % Use a fixture to create a temporary working directory - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - end - end +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + WarningsTest < matlab.unittest.TestCase methods (Test) function testWarningIfAttributeDependencyMissing(testCase) diff --git a/+tests/+unit/aberrantValuesTest.m b/+tests/+unit/aberrantValuesTest.m index 5018ce0a..ec303f78 100644 --- a/+tests/+unit/aberrantValuesTest.m +++ b/+tests/+unit/aberrantValuesTest.m @@ -1,58 +1,57 @@ -function tests = aberrantValuesTest() - tests = functiontests(localfunctions); -end +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + aberrantValuesTest < matlab.unittest.TestCase +% aberrantValuesTest - Unit test aberrant values -function setup(TestCase) - TestCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - rehash(); - TestCase.TestData.Filename = 'extra.nwb'; - ExpectedFile = NwbFile(... - 'identifier', 'EXTRAVALUES' ... - , 'session_description', 'test extra values/fields/datasets' ... - , 'session_start_time', datetime() ... - ); - ExpectedFile.acquisition.set('timeseries', types.core.TimeSeries('data', 1:100 ... - , 'data_unit', 'unit' ... - , 'starting_time', 0 ... - , 'starting_time_rate', 1)); - nwbExport(ExpectedFile, TestCase.TestData.Filename); -end + properties + TestFileName = 'extra.nwb'; + end -function testExtraAttribute(TestCase) - warning('off', 'NWB:Debug:ErrorStub'); - warning('NWB:Debug:ErrorStub', ''); % ensures `lastwarn` returns this id if called - warning('on', 'NWB:Debug:ErrorStub'); - - fid = H5F.open(TestCase.TestData.Filename, 'H5F_ACC_RDWR', 'H5P_DEFAULT'); - io.writeAttribute(fid, '/acquisition/timeseries/__expected_extra_attrib', 'extra_data'); - H5F.close(fid); - nwbRead(TestCase.TestData.Filename, 'ignorecache'); - [~,warnId] = lastwarn(); - TestCase.verifyEqual(warnId, 'NWB:CheckUnset:InvalidProperties'); -end + methods (TestClassSetup) + function setup(TestCase) + TestCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + + ExpectedFile = NwbFile(... + 'identifier', 'EXTRAVALUES' ... + , 'session_description', 'test extra values/fields/datasets' ... + , 'session_start_time', datetime() ... + ); + ExpectedFile.acquisition.set('timeseries', types.core.TimeSeries('data', 1:100 ... + , 'data_unit', 'unit' ... + , 'starting_time', 0 ... + , 'starting_time_rate', 1)); + nwbExport(ExpectedFile, TestCase.TestFileName); + end + end -function testInvalidConstraint(TestCase) - warning('off', 'NWB:Debug:ErrorStub'); - warning('NWB:Debug:ErrorStub', ''); % ensures `lastwarn` returns this id if called - warning('on', 'NWB:Debug:ErrorStub'); - - fid = H5F.open(TestCase.TestData.Filename, 'H5F_ACC_RDWR', 'H5P_DEFAULT'); - % add a fake valid dataset to force the constrained validation to fail. - wrongData = types.hdmf_common.VectorData('data', rand(3,1), 'description', 'fake data'); - refs = wrongData.export(fid, '/acquisition/fakedata', {}); - TestCase.assertEmpty(refs); - H5F.close(fid); - file = nwbRead(TestCase.TestData.Filename, 'ignorecache'); - [~,warnId] = lastwarn(); - TestCase.verifyEqual(warnId, 'NWB:Set:FailedValidation'); - - warning('off', 'NWB:Debug:ErrorStub'); - warning('NWB:Debug:ErrorStub', ''); % ensures `lastwarn` returns this id if called - warning('on', 'NWB:Debug:ErrorStub'); - - file.acquisition.set('wrong', wrongData); - [~,warnId] = lastwarn(); - TestCase.verifyEqual(warnId, 'NWB:Set:FailedValidation'); - TestCase.verifyTrue(~file.acquisition.isKey('wrong')); -end \ No newline at end of file + methods (Test) + function testExtraAttribute(TestCase) + fid = H5F.open(TestCase.TestFileName, 'H5F_ACC_RDWR', 'H5P_DEFAULT'); + io.writeAttribute(fid, '/acquisition/timeseries/__expected_extra_attrib', 'extra_data'); + H5F.close(fid); + + TestCase.verifyWarning(... + @() nwbRead(TestCase.TestFileName, 'ignorecache'), ... + 'NWB:CheckUnset:InvalidProperties') + end + + function testInvalidConstraint(TestCase) + % Add a fake valid dataset to force the constrained validation to fail. + fid = H5F.open(TestCase.TestFileName, 'H5F_ACC_RDWR', 'H5P_DEFAULT'); + % add a fake valid dataset to force the constrained validation to fail. + wrongData = types.hdmf_common.VectorData('data', rand(3,1), 'description', 'fake data'); + refs = wrongData.export(fid, '/acquisition/fakedata', {}); + TestCase.assertEmpty(refs); + H5F.close(fid); + + file = TestCase.verifyWarning( ... + @() nwbRead(TestCase.TestFileName, 'ignorecache'), ... + 'NWB:Set:FailedValidation'); + + TestCase.verifyWarning( ... + @() file.acquisition.set('wrong', wrongData), ... + 'NWB:Set:FailedValidation') + + TestCase.verifyTrue(~file.acquisition.isKey('wrong')); + end + end +end diff --git a/+tests/+unit/anonTest.m b/+tests/+unit/anonTest.m deleted file mode 100644 index c3ac5718..00000000 --- a/+tests/+unit/anonTest.m +++ /dev/null @@ -1,35 +0,0 @@ -function tests = anonTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -schemaPath = fullfile(misc.getMatnwbDir(),... - '+tests', '+unit', 'anonSchema', 'anon.namespace.yaml'); -generateExtension(schemaPath, 'savedir', '.'); -rehash(); -end - -function testAnonDataset(testCase) -ag = types.anon.AnonGroup('ad', types.anon.AnonData('data', 0)); -nwbExpected = NwbFile(... - 'identifier', 'ANON',... - 'session_description', 'anonymous class schema testing',... - 'session_start_time', datetime()); -nwbExpected.acquisition.set('ag', ag); -nwbExport(nwbExpected, 'testanon.nwb'); - -tests.util.verifyContainerEqual(testCase, nwbRead('testanon.nwb', 'ignorecache'), nwbExpected); -end - -function testAnonTypeWithNameValueInput(testCase) - anon = types.untyped.Anon('a', 1); - testCase.verifyEqual(anon.name, 'a') - testCase.verifyEqual(anon.value, 1) -end diff --git a/+tests/+unit/boolTest.m b/+tests/+unit/boolTest.m deleted file mode 100644 index 0248ed09..00000000 --- a/+tests/+unit/boolTest.m +++ /dev/null @@ -1,35 +0,0 @@ -function tests = boolTest() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - schemaPath = fullfile(misc.getMatnwbDir(),... - '+tests', '+unit', 'boolSchema', 'bool.namespace.yaml'); - generateExtension(schemaPath, 'savedir', '.'); - rehash(); -end - -function testIo(testCase) - nwb = NwbFile(... - 'identifier', 'BOOL',... - 'session_description', 'test bool',... - 'session_start_time', datetime()); - boolContainer = types.bool.BoolContainer(... - 'data', logical(randi([0,1], 100, 1)), ... - 'attribute', false); - scalarBoolContainer = types.bool.BoolContainer(... - 'data', false, ... - 'attribute', true); - nwb.acquisition.set('bool', boolContainer); - nwb.acquisition.set('scalarbool', scalarBoolContainer); - nwb.export('test.nwb'); - nwbActual = nwbRead('test.nwb', 'ignorecache'); - tests.util.verifyContainerEqual(testCase, nwbActual, nwb); -end diff --git a/+tests/+unit/dataPipeTest.m b/+tests/+unit/dataPipeTest.m index 724e4937..7463328c 100644 --- a/+tests/+unit/dataPipeTest.m +++ b/+tests/+unit/dataPipeTest.m @@ -1,386 +1,371 @@ -function tests = dataPipeTest() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - rehash(); -end - -function testInit(testCase) - import types.untyped.datapipe.*; - import matlab.unittest.fixtures.SuppressedWarningsFixture - %testCase.applyFixture(SuppressedWarningsFixture('NWB:DataPipeTest:Debug')) - - warnDebugId = 'NWB:DataPipeTest:Debug'; - warning('off', warnDebugId); - warning(warnDebugId, ''); - - %% extra data type - data = rand(100, 1); - % types.untyped.DataPipe('data', data, 'dataType', 'double'); - % [~,lastId] = lastwarn(); - % testCase.verifyEqual(lastId, 'NWB:DataPipe:RedundantDataType'); - - testCase.verifyWarning(... - @(varargin) types.untyped.DataPipe('data', data, 'dataType', 'double'), ... - 'NWB:DataPipe:RedundantDataType') - - warning(warnDebugId, ''); - - %% compressionLevel and hasShuffle ignored if filters is provided - pipe = types.untyped.DataPipe('data', data ... - , 'compressionLevel', 3 ... - , 'hasShuffle', true ... - , 'filters', [properties.Compression(4)]); - [~,lastId] = lastwarn(); - testCase.verifyEqual(lastId, 'NWB:DataPipe:FilterOverride'); - testCase.verifyEqual(pipe.compressionLevel, 4); - testCase.verifyTrue(~pipe.hasShuffle); - pipe.compressionLevel = 2; - testCase.verifyEqual(pipe.compressionLevel, 2); - pipe.hasShuffle = true; - testCase.verifyTrue(pipe.hasShuffle); - - warning(warnDebugId, ''); - - %% extraneous properties from file - filename = 'testInit.h5'; - datasetName = '/test_data'; - fid = H5F.create(filename); - pipe.export(fid, datasetName, {}); - H5F.close(fid); - - testCase.verifyWarning(... - @(varargin) types.untyped.DataPipe('filename', filename, 'path', datasetName, 'dataType', 'double'), ... - 'NWB:DataPipe:UnusedArguments') - - testCase.applyFixture(SuppressedWarningsFixture('NWB:DataPipe:UnusedArguments')) - pipe = types.untyped.DataPipe('filename', filename, 'path', datasetName, 'dataType', 'double'); - % [~,lastId] = lastwarn(); - % testCase.verifyEqual(lastId, 'NWB:DataPipe:UnusedArguments'); - testCase.verifyEqual(pipe.compressionLevel, 2); - testCase.verifyTrue(pipe.hasShuffle); - - % cleanup - warning('on', warnDebugId); -end - -function testIndex(testCase) - filename = 'testIndexing.h5'; - name = '/test_data'; - - data = rand(100, 100, 100); - Pipe = types.untyped.DataPipe('data', data); - - testCase.verifyEqual(Pipe(:), data(:)); - testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); - - fid = H5F.create(filename); - Pipe.export(fid, name, {}); % bind the pipe. - H5F.close(fid); - - testCase.verifyEqual(Pipe(:), data(:)); - testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); -end - -function testAppend(testCase) - filename = 'testIterativeWrite.h5'; - - Pipe = types.untyped.DataPipe(... - 'maxSize', [10 13 15],... - 'axis', 3,... - 'chunkSize', [10 13 1],... - 'dataType', 'uint8',... - 'compressionLevel', 5); - - OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); - - %% create test file - fid = H5F.create(filename); - - initialData = createData(Pipe.dataType, [10 13 10]); - Pipe.internal.data = initialData; - Pipe.export(fid, '/test_data', {}); % bind - OneDimensionPipe.export(fid, '/test_one_dim_data', {}); - - H5F.close(fid); - - %% append data - totalLength = 3; - appendData = zeros([10 13 totalLength], Pipe.dataType); - for i = 1:totalLength - appendData(:,:,i) = createData(Pipe.dataType, Pipe.chunkSize); - Pipe.append(appendData(:,:,i)); - end - - for i = 1:totalLength - OneDimensionPipe.append(rand()); - end - - %% verify data - Pipe = types.untyped.DataPipe('filename', filename, 'path', '/test_data'); - readData = Pipe.load(); - testCase.verifyEqual(readData(:,:,1:10), initialData); - testCase.verifyEqual(readData(:,:,11:end), appendData); - - OneDimensionPipe = types.untyped.DataPipe('filename', filename, 'path', '/test_one_dim_data'); - readData = OneDimensionPipe.load(); - testCase.verifyTrue(isvector(readData)); - testCase.verifyEqual(length(readData), 6); - testCase.verifyEqual(readData(1:3), [7, 8, 9] .'); -end - -function testExternalFilters(testCase) - import types.untyped.datapipe.dynamic.Filter; - import types.untyped.datapipe.properties.DynamicFilter; - import types.untyped.datapipe.properties.Shuffle; - - % TODO: Why is Filter.LZ4 not part of the exported Pipe, i.e when the - % Pipe.internal goes from Blueprint to Bound - - testCase.assumeTrue(logical(H5Z.filter_avail(uint32(Filter.LZ4)))); - - filename = 'testExternalWrite.h5'; - - Pipe = types.untyped.DataPipe(... - 'maxSize', [10 13 15],... - 'axis', 3,... - 'chunkSize', [10 13 1],... - 'dataType', 'uint8',... - 'filters', [Shuffle() DynamicFilter(Filter.LZ4)]); - - OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); - - %% create test file - fid = H5F.create(filename); - - initialData = createData(Pipe.dataType, [10 13 10]); - Pipe.internal.data = initialData; - Pipe.export(fid, '/test_data', {}); % bind - OneDimensionPipe.export(fid, '/test_one_dim_data', {}); - - H5F.close(fid); - - %% append data - totalLength = 3; - appendData = zeros([10 13 totalLength], Pipe.dataType); - for i = 1:totalLength - appendData(:,:,i) = createData(Pipe.dataType, Pipe.chunkSize); - Pipe.append(appendData(:,:,i)); - end - - for i = 1:totalLength - OneDimensionPipe.append(rand()); - end - - %% verify data - Pipe = types.untyped.DataPipe('filename', filename, 'path', '/test_data'); - readData = Pipe.load(); - testCase.verifyEqual(readData(:,:,1:10), initialData); - testCase.verifyEqual(readData(:,:,11:end), appendData); - - OneDimensionPipe = types.untyped.DataPipe('filename', filename, 'path', '/test_one_dim_data'); - readData = OneDimensionPipe.load(); - testCase.verifyTrue(isvector(readData)); - testCase.verifyEqual(length(readData), 6); - testCase.verifyEqual(readData(1:3), [7, 8, 9] .'); -end - -function testBoundPipe(testCase) - import types.untyped.*; - filename = 'bound.h5'; - dsName = '/test_data'; - debugId = 'NWB:DataPipe:Debug'; - warning('off', debugId); - - %% full pipe case - fullpipe = DataPipe('data', rand(100, 1)); - - fid = H5F.create(filename); - fullpipe.export(fid, dsName, {}); - H5F.close(fid); - DataPipe('filename', filename, 'path', dsName); - delete(filename); - - %% multi-axis case - data = rand(100, 1); - maxSize = [200, 2]; - multipipe = DataPipe('data', data, 'maxSize', maxSize); - fid = H5F.create(filename); - try - % this should be impossible normally. - multipipe.export(fid, dsName, {}); - catch ME - testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:InvalidSize'); - end - H5F.close(fid); - delete(filename); - - fid = H5F.create(filename); - rank = length(maxSize); - dcpl = H5P.create('H5P_DATASET_CREATE'); - H5P.set_chunk(dcpl, datapipe.guessChunkSize(class(data), maxSize)); - did = H5D.create( ... - fid, dsName ... - , io.getBaseType(class(data)) ... - , H5S.create_simple(rank, fliplr(size(data)), fliplr(maxSize)) ... - , 'H5P_DEFAULT', dcpl, 'H5P_DEFAULT'); - H5D.write(did, 'H5ML_DEFAULT', 'H5S_ALL', 'H5S_ALL', 'H5P_DEFAULT', data); - H5D.close(did); - H5F.close(fid); - - warning(debugId, ''); - multipipe = DataPipe('filename', filename, 'path', dsName); - [~,lastId] = lastwarn(); - testCase.verifyEqual(lastId, 'NWB:BoundPipe:InvalidPipeShape'); - - try - multipipe.append(rand(10, 2, 10)); - catch ME - testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:InvalidDataShape'); +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + dataPipeTest < matlab.unittest.TestCase + + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end end - - delete(filename); - - %% not chunked behavior - fid = H5F.create(filename); - did = H5D.create( ... - fid, dsName ... - , io.getBaseType(class(data)) ... - , H5S.create_simple(rank, fliplr(size(data)), fliplr(size(data))) ... - , 'H5P_DEFAULT', 'H5P_DEFAULT', 'H5P_DEFAULT'); - H5D.write(did, 'H5ML_DEFAULT', 'H5S_ALL', 'H5S_ALL', 'H5P_DEFAULT', data); - H5D.close(did); - H5F.close(fid); - warning(debugId, ''); - nochunk = DataPipe('filename', filename, 'path', dsName); - [~,lastId] = lastwarn(); - testCase.verifyEqual(lastId, 'NWB:BoundPipe:NotChunked'); - nochunk.load(); % test still loadable. - - %% cleanup - warning('on', debugId); -end - -function testConfigurationFromData(testCase) - conf = types.untyped.datapipe.Configuration.fromData(zeros(10,10), 1); - testCase.verifyClass(conf, 'types.untyped.datapipe.Configuration') -end - -function testPropertySetGet(testCase) - data = rand(100, 1); - pipe = types.untyped.DataPipe('data', data); - - pipe.axis = 1; - testCase.verifyEqual(pipe.axis, 1) - - pipe.offset = 4; - testCase.verifyEqual(pipe.offset, 4) - - pipe.dataType = 'double'; - testCase.verifyEqual(pipe.dataType, 'double') - - pipe.chunkSize = 10; - testCase.verifyEqual(pipe.chunkSize, 10) - - pipe.compressionLevel = -1; - % Todo: make verification - - pipe.hasShuffle = false; - testCase.verifyFalse(pipe.hasShuffle) - - pipe.hasShuffle = true; - testCase.verifyTrue(pipe.hasShuffle) -end - -function testAppendVectorToBlueprintPipe(testCase) - % Column vector: - data = rand(10, 1); - pipe = types.untyped.DataPipe('data', data); - - pipe.append([1;2]); - newData = pipe.load(); - testCase.verifyEqual(newData, cat(1, data, [1;2])) - - testCase.verifyError(@(X) pipe.append([1,2]), 'MATLAB:catenate:dimensionMismatch') - - % Row vector: - data = rand(1, 10); - pipe = types.untyped.DataPipe('data', data); - - pipe.append([1,2]); - newData = pipe.load(); - testCase.verifyEqual(newData, cat(2, data, [1,2])) - - testCase.verifyError(@(X) pipe.append([1;2]), 'MATLAB:catenate:dimensionMismatch') -end - -function testSubsrefWithNonScalarSubs(testCase) - data = rand(100, 1); - pipe = types.untyped.DataPipe('data', data); - - % This syntax should not be supported. Not clear what a valid - % non-scalar subsref would be... - subData = pipe{1:10}(1:5); - testCase.verifyEqual(subData, data(1:5)) -end -function testOverrideBoundPipeProperties(testCase) - import matlab.unittest.fixtures.SuppressedWarningsFixture - testCase.applyFixture(SuppressedWarningsFixture('NWB:DataPipe:UnusedArguments')) - - data = rand(10, 1); - pipe = types.untyped.DataPipe('data', data); - - filename = 'testInit.h5'; - datasetName = '/test_data'; - fid = H5F.create(filename); - pipe.export(fid, datasetName, {}); - H5F.close(fid); - - loadedPipe = types.untyped.DataPipe('filename', filename, 'path', datasetName, 'dataType', 'double'); - - % Using verifyError did not work for the following statements, i.e this: - % testCase.verifyError(@(x) eval('loadedPipe.chunkSize = 2'), 'NWB:BoundPipe:CannotSetPipeProperty') %#ok - % fails with the following error: Attempt to add "loadedPipe" to a static workspace. - try - loadedPipe.chunkSize = 2; - catch ME - testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:CannotSetPipeProperty') - end - - try - loadedPipe.hasShuffle = false; - catch ME - testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:CannotSetPipeProperty') + methods (Test) + function testInit(testCase) + import types.untyped.datapipe.*; + import matlab.unittest.fixtures.SuppressedWarningsFixture + + %% Providing data and dataType should issue warning + data = rand(100, 1); + pipe = testCase.verifyWarning(... + @(varargin) types.untyped.DataPipe('data', data, 'dataType', 'double'), ... + 'NWB:DataPipe:RedundantDataType'); + + pipe.compressionLevel = 2; + pipe.hasShuffle = true; + + %% Extraneous properties from file + filename = 'testInit.h5'; + datasetName = '/test_data'; + fid = H5F.create(filename); + pipe.export(fid, datasetName, {}); + H5F.close(fid); + + pipe = testCase.verifyWarning(... + @(varargin) types.untyped.DataPipe('filename', filename, 'path', datasetName, 'dataType', 'double'), ... + 'NWB:DataPipe:UnusedArguments'); + + % Verify that proprerty values from file are present in object. + testCase.verifyEqual(pipe.compressionLevel, 2); + testCase.verifyTrue(pipe.hasShuffle); + end + + function testFilterOverride(testCase) + import types.untyped.datapipe.*; + + constructorArgs = { ... + 'data', rand(100, 1), ... + 'compressionLevel', 3, ... + 'hasShuffle', true, ... + 'filters', [properties.Compression(4)] ... + }; + + pipe = testCase.verifyWarning(... + @(varargin) types.untyped.DataPipe(constructorArgs{:}), ... + 'NWB:DataPipe:FilterOverride'); + + % Verify that compressionLevel and hasShuffle is ignored if filters is provided + testCase.verifyEqual(pipe.compressionLevel, 4); + testCase.verifyFalse(pipe.hasShuffle); + + % Explicitly set property values and verify that they are updated + pipe.compressionLevel = 2; + testCase.verifyEqual(pipe.compressionLevel, 2); + pipe.hasShuffle = true; + testCase.verifyTrue(pipe.hasShuffle); + end + + function testIndex(testCase) + filename = 'testIndexing.h5'; + name = '/test_data'; + + data = rand(100, 100, 100); + Pipe = types.untyped.DataPipe('data', data); + + testCase.verifyEqual(Pipe(:), data(:)); + testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); + + fid = H5F.create(filename); + Pipe.export(fid, name, {}); % bind the pipe. + H5F.close(fid); + + testCase.verifyEqual(Pipe(:), data(:)); + testCase.verifyEqual(Pipe(:,:,1), data(:,:,1)); + end + + function testAppend(testCase) + filename = 'testIterativeWrite.h5'; + + Pipe = types.untyped.DataPipe(... + 'maxSize', [10 13 15],... + 'axis', 3,... + 'chunkSize', [10 13 1],... + 'dataType', 'uint8',... + 'compressionLevel', 5); + + OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); + + %% create test file + fid = H5F.create(filename); + + initialData = createData(Pipe.dataType, [10 13 10]); + Pipe.internal.data = initialData; + Pipe.export(fid, '/test_data', {}); % bind + OneDimensionPipe.export(fid, '/test_one_dim_data', {}); + + H5F.close(fid); + + %% append data + totalLength = 3; + appendData = zeros([10 13 totalLength], Pipe.dataType); + for i = 1:totalLength + appendData(:,:,i) = createData(Pipe.dataType, Pipe.chunkSize); + Pipe.append(appendData(:,:,i)); + end + + for i = 1:totalLength + OneDimensionPipe.append(rand()); + end + + %% verify data + Pipe = types.untyped.DataPipe('filename', filename, 'path', '/test_data'); + readData = Pipe.load(); + testCase.verifyEqual(readData(:,:,1:10), initialData); + testCase.verifyEqual(readData(:,:,11:end), appendData); + + OneDimensionPipe = types.untyped.DataPipe('filename', filename, 'path', '/test_one_dim_data'); + readData = OneDimensionPipe.load(); + testCase.verifyTrue(isvector(readData)); + testCase.verifyEqual(length(readData), 6); + testCase.verifyEqual(readData(1:3), [7, 8, 9] .'); + end + + function testExternalFilters(testCase) + import types.untyped.datapipe.dynamic.Filter; + import types.untyped.datapipe.properties.DynamicFilter; + import types.untyped.datapipe.properties.Shuffle; + + % TODO: Why is Filter.LZ4 not part of the exported Pipe, i.e when the + % Pipe.internal goes from Blueprint to Bound + + testCase.assumeTrue(logical(H5Z.filter_avail(uint32(Filter.LZ4)))); + + filename = 'testExternalWrite.h5'; + + Pipe = types.untyped.DataPipe(... + 'maxSize', [10 13 15],... + 'axis', 3,... + 'chunkSize', [10 13 1],... + 'dataType', 'uint8',... + 'filters', [Shuffle() DynamicFilter(Filter.LZ4)]); + + OneDimensionPipe = types.untyped.DataPipe('maxSize', Inf, 'data', [7, 8, 9]); + + %% create test file + fid = H5F.create(filename); + + initialData = createData(Pipe.dataType, [10 13 10]); + Pipe.internal.data = initialData; + Pipe.export(fid, '/test_data', {}); % bind + OneDimensionPipe.export(fid, '/test_one_dim_data', {}); + + H5F.close(fid); + + %% append data + totalLength = 3; + appendData = zeros([10 13 totalLength], Pipe.dataType); + for i = 1:totalLength + appendData(:,:,i) = createData(Pipe.dataType, Pipe.chunkSize); + Pipe.append(appendData(:,:,i)); + end + + for i = 1:totalLength + OneDimensionPipe.append(rand()); + end + + %% verify data + Pipe = types.untyped.DataPipe('filename', filename, 'path', '/test_data'); + readData = Pipe.load(); + testCase.verifyEqual(readData(:,:,1:10), initialData); + testCase.verifyEqual(readData(:,:,11:end), appendData); + + OneDimensionPipe = types.untyped.DataPipe('filename', filename, 'path', '/test_one_dim_data'); + readData = OneDimensionPipe.load(); + testCase.verifyTrue(isvector(readData)); + testCase.verifyEqual(length(readData), 6); + testCase.verifyEqual(readData(1:3), [7, 8, 9] .'); + end + + function testBoundPipe(testCase) + import types.untyped.*; + filename = 'bound.h5'; + dsName = '/test_data'; + + %% full pipe case + fullpipe = DataPipe('data', rand(100, 1)); + + fid = H5F.create(filename); + fullpipe.export(fid, dsName, {}); + H5F.close(fid); + DataPipe('filename', filename, 'path', dsName); + delete(filename); + + %% multi-axis case + data = rand(100, 1); + maxSize = [200, 2]; + multipipe = DataPipe('data', data, 'maxSize', maxSize); + fid = H5F.create(filename); + try + % this should be impossible normally. + multipipe.export(fid, dsName, {}); + catch ME + testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:InvalidSize'); + end + H5F.close(fid); + delete(filename); + + fid = H5F.create(filename); + rank = length(maxSize); + dcpl = H5P.create('H5P_DATASET_CREATE'); + H5P.set_chunk(dcpl, datapipe.guessChunkSize(class(data), maxSize)); + did = H5D.create( ... + fid, dsName ... + , io.getBaseType(class(data)) ... + , H5S.create_simple(rank, fliplr(size(data)), fliplr(maxSize)) ... + , 'H5P_DEFAULT', dcpl, 'H5P_DEFAULT'); + H5D.write(did, 'H5ML_DEFAULT', 'H5S_ALL', 'H5S_ALL', 'H5P_DEFAULT', data); + H5D.close(did); + H5F.close(fid); + + multipipe = testCase.verifyWarning(... + @(varargin) DataPipe('filename', filename, 'path', dsName), ... + 'NWB:BoundPipe:InvalidPipeShape'); + + testCase.verifyError(... + @(varargin) multipipe.append(rand(10, 2, 10)), ... + 'NWB:BoundPipe:InvalidDataShape') + + delete(filename); + + %% not chunked behavior + fid = H5F.create(filename); + did = H5D.create( ... + fid, dsName ... + , io.getBaseType(class(data)) ... + , H5S.create_simple(rank, fliplr(size(data)), fliplr(size(data))) ... + , 'H5P_DEFAULT', 'H5P_DEFAULT', 'H5P_DEFAULT'); + H5D.write(did, 'H5ML_DEFAULT', 'H5S_ALL', 'H5S_ALL', 'H5P_DEFAULT', data); + H5D.close(did); + H5F.close(fid); + + nochunk = testCase.verifyWarning(... + @(varargin) DataPipe('filename', filename, 'path', dsName), ... + 'NWB:BoundPipe:NotChunked'); + + nochunk.load(); % test still loadable. + end + + function testConfigurationFromData(testCase) + conf = types.untyped.datapipe.Configuration.fromData(zeros(10,10), 1); + testCase.verifyClass(conf, 'types.untyped.datapipe.Configuration') + end + + function testPropertySetGet(testCase) + data = rand(100, 1); + pipe = types.untyped.DataPipe('data', data); + + pipe.axis = 1; + testCase.verifyEqual(pipe.axis, 1) + + pipe.offset = 4; + testCase.verifyEqual(pipe.offset, 4) + + pipe.dataType = 'double'; + testCase.verifyEqual(pipe.dataType, 'double') + + pipe.chunkSize = 10; + testCase.verifyEqual(pipe.chunkSize, 10) + + pipe.compressionLevel = -1; + % Todo: make verification + + pipe.hasShuffle = false; + testCase.verifyFalse(pipe.hasShuffle) + + pipe.hasShuffle = true; + testCase.verifyTrue(pipe.hasShuffle) + end + + function testAppendVectorToBlueprintPipe(testCase) + % Column vector: + data = rand(10, 1); + pipe = types.untyped.DataPipe('data', data); + + pipe.append([1;2]); + newData = pipe.load(); + testCase.verifyEqual(newData, cat(1, data, [1;2])) + + testCase.verifyError(@(X) pipe.append([1,2]), 'MATLAB:catenate:dimensionMismatch') + + % Row vector: + data = rand(1, 10); + pipe = types.untyped.DataPipe('data', data); + + pipe.append([1,2]); + newData = pipe.load(); + testCase.verifyEqual(newData, cat(2, data, [1,2])) + + testCase.verifyError(@(X) pipe.append([1;2]), 'MATLAB:catenate:dimensionMismatch') + end + + function testSubsrefWithNonScalarSubs(testCase) + data = rand(100, 1); + pipe = types.untyped.DataPipe('data', data); + + % This syntax should not be supported. Not clear what a valid + % non-scalar subsref would be... + subData = pipe{1:10}(1:5); + testCase.verifyEqual(subData, data(1:5)) + end + + function testOverrideBoundPipeProperties(testCase) + import matlab.unittest.fixtures.SuppressedWarningsFixture + testCase.applyFixture(SuppressedWarningsFixture('NWB:DataPipe:UnusedArguments')) + + data = rand(10, 1); + pipe = types.untyped.DataPipe('data', data); + + filename = 'testInit.h5'; + datasetName = '/test_data'; + fid = H5F.create(filename); + pipe.export(fid, datasetName, {}); + H5F.close(fid); + + loadedPipe = types.untyped.DataPipe('filename', filename, 'path', datasetName, 'dataType', 'double'); + + % Using verifyError did not work for the following statements, i.e this: + % testCase.verifyError(@(x) eval('loadedPipe.chunkSize = 2'), 'NWB:BoundPipe:CannotSetPipeProperty') %#ok + % fails with the following error: Attempt to add "loadedPipe" to a static workspace. + try + loadedPipe.chunkSize = 2; + catch ME + testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:CannotSetPipeProperty') + end + + try + loadedPipe.hasShuffle = false; + catch ME + testCase.verifyEqual(ME.identifier, 'NWB:BoundPipe:CannotSetPipeProperty') + end + + end + + function testDynamicFilterIsInDatasetCreationPropertyList(testCase) + import types.untyped.datapipe.dynamic.Filter; + import types.untyped.datapipe.properties.DynamicFilter; + + dcpl = H5P.create('H5P_DATASET_CREATE'); + dynamicFilter = DynamicFilter(Filter.LZ4); + + tf = dynamicFilter.isInDcpl(dcpl); + testCase.verifyFalse(tf) + + % Add filter + dynamicFilter.addTo(dcpl) + tf = dynamicFilter.isInDcpl(dcpl); + testCase.verifyTrue(tf) + end end - -end - -function testDynamicFilterIsInDatasetCreationPropertyList(testCase) - import types.untyped.datapipe.dynamic.Filter; - import types.untyped.datapipe.properties.DynamicFilter; - - dcpl = H5P.create('H5P_DATASET_CREATE'); - dynamicFilter = DynamicFilter(Filter.LZ4); - - tf = dynamicFilter.isInDcpl(dcpl); - testCase.verifyFalse(tf) - - % Add filter - dynamicFilter.addTo(dcpl) - tf = dynamicFilter.isInDcpl(dcpl); - testCase.verifyTrue(tf) end function data = createData(dataType, size) data = randi(intmax(dataType), size, dataType); end - diff --git a/+tests/+unit/dataStubTest.m b/+tests/+unit/dataStubTest.m index d2a38d76..84d69350 100644 --- a/+tests/+unit/dataStubTest.m +++ b/+tests/+unit/dataStubTest.m @@ -1,145 +1,147 @@ -function tests = dataStubTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -rehash(); -end - -function testRegionRead(testCase) - -nwb = NwbFile(... - 'session_description', 'a test NWB File', ... - 'identifier', 'mouse004_day4', ... - 'session_start_time', datetime(2018, 3, 1, 12, 0, 0, 'TimeZone', 'local')); - -data = reshape(1:5000, 25, 5, 4, 2, 5); - -timeseries = types.core.TimeSeries(... - 'starting_time', 0.0, ... % seconds - 'starting_time_rate', 200., ... % Hz - 'data', data,... - 'data_unit','na'); - -nwb.acquisition.set('data', timeseries); -%% - -nwbExport(nwb, 'test_stub_read.nwb'); -nwb2 = nwbRead('test_stub_read.nwb', 'ignorecache'); - -stub = nwb2.acquisition.get('data').data; - -%% -% test subset/missing dimensions -stubData = stub(2:4, 2:4, 2:4); -testCase.verifyEqual(stubData, data(2:4, 2:4, 2:4)); -% test legacy load style -testCase.verifyEqual(stubData, stub.load([2, 2, 2], [1, 1, 1], [4, 4, 4])); -testCase.verifyEqual(stubData, stub.load([2, 2, 2], [4, 4, 4])); - -% test Inf -testCase.verifyEqual(stub(2:end, 2:end, 2:end, :), data(2:end, 2:end, 2:end, :)); - -% test stride -testCase.verifyEqual(stub(1:2:25, 1:2:4, :, :), data(1:2:25, 1:2:4, :, :)); - -% test flatten -testCase.verifyEqual(stub(1, 1, :), data(1, 1, :)); - -% test non-dangling `:` -testCase.verifyEqual(stub(:, 1), data(:, 1)); - -% test arbitrary indices -primeInd = primes(25); -testCase.verifyEqual(stub(primeInd), data(primeInd)); -% multidim scalar indices outputs data according to selection orientation. -testCase.verifyEqual(stub(primeInd .'), data(primeInd .')); -testCase.verifyEqual(stub(primeInd, 2:4, :), data(primeInd, 2:4, :)); -testCase.verifyEqual(stub(primeInd, :, 1), data(primeInd, :, 1)); -testCase.verifyEqual(stub(primeInd, [1 2 5]), data(primeInd, [1 2 5])); -testCase.verifyEqual(stub([1 25], [1 5], [1 4], [1 2], [1 5]), data([1 25], [1 5], [1 4], [1 2], [1 5])); -overflowPrimeInd = primes(31); -testCase.verifyEqual(stub(overflowPrimeInd), stub(ind2sub(stub.dims, overflowPrimeInd))); -testCase.verifyEqual(stub(overflowPrimeInd), data(overflowPrimeInd)); - -% test duplicate indices -testCase.verifyEqual(stub([1 1 1 1]), data([1 1 1 1])); - -% test out of order indices -testCase.verifyEqual(stub([5 4 3 2 2]), data([5 4 3 2 2])); -end - -function testObjectCopy(testCase) -rootDir = misc.getMatnwbDir(); -unitTestLocation = fullfile(rootDir, '+tests', '+unit'); -generateExtension(fullfile(unitTestLocation, 'regionReferenceSchema', 'rrs.namespace.yaml'), 'savedir', '.'); -generateExtension(fullfile(unitTestLocation, 'compoundSchema', 'cs.namespace.yaml'), 'savedir', '.'); -rehash(); -nwb = NwbFile(... - 'identifier', 'DATASTUB',... - 'session_description', 'test datastub object copy',... - 'session_start_time', datetime()); -rc = types.rrs.RefContainer('data', types.rrs.RefData('data', rand(100, 100))); -rcPath = '/acquisition/rc'; -rcDataPath = [rcPath '/data']; -rcRef = types.cs.CompoundRefData('data', table(... - rand(2, 1),... - rand(2, 1),... - [types.untyped.ObjectView(rcPath); types.untyped.ObjectView(rcPath)],... - [types.untyped.RegionView(rcDataPath, 1:2, 99:100); types.untyped.RegionView(rcDataPath, 5:6, 88:89)],... - 'VariableNames', {'a', 'b', 'objref', 'regref'})); - -nwb.acquisition.set('rc', rc); -nwb.analysis.set('rcRef', rcRef); -nwbExport(nwb, 'original.nwb'); -nwbNew = nwbRead('original.nwb', 'ignorecache'); -tests.util.verifyContainerEqual(testCase, nwbNew, nwb); -nwbExport(nwbNew, 'new.nwb'); -end - -function testLoadWithEmptyIndices(testCase) - nwb = NwbFile(... - 'identifier', 'DATASTUB',... - 'session_description', 'test datastub object copy',... - 'session_start_time', datetime()); - - % Add different datatypes to a table, and try to read them in later - % using empty indexing on a DataStub representation - tableToExport = table( ... - {'test'}, ... % Cell - 0, ... % Double - false, ... % Logical - struct('x', 1, 'y', 1, 'z', 1) ... % Struct (compound) - ); - dynamicTable = util.table2nwb(tableToExport); - nwb.acquisition.set('Test', dynamicTable); - - nwbExport(nwb, 'testLoadWithEmptyIndices.nwb') - - nwbIn = nwbRead('testLoadWithEmptyIndices.nwb', 'ignorecache'); - - importedTable = nwbIn.acquisition.get('Test'); - varNames = transpose( string(importedTable.colnames) ); - - for iVarName = varNames - iDataStub = importedTable.vectordata.get(iVarName).data; +classdef dataStubTest < tests.abstract.NwbTestCase + + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end - testCase.assertClass(iDataStub, 'types.untyped.DataStub') - value = iDataStub([]); - testCase.assertEmpty(value) + methods (Test) + function testRegionRead(testCase) + + nwb = tests.factory.NWBFile(); + + data = reshape(1:5000, 25, 5, 4, 2, 5); + + timeseries = types.core.TimeSeries(... + 'starting_time', 0.0, ... % seconds + 'starting_time_rate', 200., ... % Hz + 'data', data,... + 'data_unit','na'); + + nwb.acquisition.set('data', timeseries); + %% + + nwbExport(nwb, 'test_stub_read.nwb'); + nwb2 = nwbRead('test_stub_read.nwb', 'ignorecache'); + + stub = nwb2.acquisition.get('data').data; + + %% + % test subset/missing dimensions + stubData = stub(2:4, 2:4, 2:4); + testCase.verifyEqual(stubData, data(2:4, 2:4, 2:4)); + % test legacy load style + testCase.verifyEqual(stubData, stub.load([2, 2, 2], [1, 1, 1], [4, 4, 4])); + testCase.verifyEqual(stubData, stub.load([2, 2, 2], [4, 4, 4])); + + % test Inf + testCase.verifyEqual(stub(2:end, 2:end, 2:end, :), data(2:end, 2:end, 2:end, :)); + + % test stride + testCase.verifyEqual(stub(1:2:25, 1:2:4, :, :), data(1:2:25, 1:2:4, :, :)); + + % test flatten + testCase.verifyEqual(stub(1, 1, :), data(1, 1, :)); + + % test non-dangling `:` + testCase.verifyEqual(stub(:, 1), data(:, 1)); + + % test arbitrary indices + primeInd = primes(25); + testCase.verifyEqual(stub(primeInd), data(primeInd)); + % multidim scalar indices outputs data according to selection orientation. + testCase.verifyEqual(stub(primeInd .'), data(primeInd .')); + testCase.verifyEqual(stub(primeInd, 2:4, :), data(primeInd, 2:4, :)); + testCase.verifyEqual(stub(primeInd, :, 1), data(primeInd, :, 1)); + testCase.verifyEqual(stub(primeInd, [1 2 5]), data(primeInd, [1 2 5])); + testCase.verifyEqual(stub([1 25], [1 5], [1 4], [1 2], [1 5]), data([1 25], [1 5], [1 4], [1 2], [1 5])); + overflowPrimeInd = primes(31); + testCase.verifyEqual(stub(overflowPrimeInd), stub(ind2sub(stub.dims, overflowPrimeInd))); + testCase.verifyEqual(stub(overflowPrimeInd), data(overflowPrimeInd)); + + % test duplicate indices + testCase.verifyEqual(stub([1 1 1 1]), data([1 1 1 1])); + + % test out of order indices + testCase.verifyEqual(stub([5 4 3 2 2]), data([5 4 3 2 2])); + end + + function testObjectCopy(testCase) + import tests.fixtures.ExtensionGenerationFixture + + rootDir = misc.getMatnwbDir(); + + testSchemaLocation = fullfile(rootDir, '+tests', 'test-schema'); + typesOutputFolder = testCase.getTypesOutputFolder(); + + extensionNamespaceFile = fullfile(testSchemaLocation, 'regionReferenceSchema', 'rrs.namespace.yaml'); + testCase.applyFixture(... + ExtensionGenerationFixture(extensionNamespaceFile, typesOutputFolder)) + + extensionNamespaceFile = fullfile(testSchemaLocation, 'compoundSchema', 'cs.namespace.yaml'); + testCase.applyFixture(... + ExtensionGenerationFixture(extensionNamespaceFile, typesOutputFolder)) + + nwb = NwbFile(... + 'identifier', 'DATASTUB',... + 'session_description', 'test datastub object copy',... + 'session_start_time', datetime()); + rc = types.rrs.RefContainer('data', types.rrs.RefData('data', rand(100, 100))); + rcPath = '/acquisition/rc'; + rcDataPath = [rcPath '/data']; + rcRef = types.cs.CompoundRefData('data', table(... + rand(2, 1),... + rand(2, 1),... + [types.untyped.ObjectView(rcPath); types.untyped.ObjectView(rcPath)],... + [types.untyped.RegionView(rcDataPath, 1:2, 99:100); types.untyped.RegionView(rcDataPath, 5:6, 88:89)],... + 'VariableNames', {'a', 'b', 'objref', 'regref'})); + + nwb.acquisition.set('rc', rc); + nwb.analysis.set('rcRef', rcRef); + nwbExport(nwb, 'original.nwb'); + nwbNew = nwbRead('original.nwb', 'ignorecache'); + tests.util.verifyContainerEqual(testCase, nwbNew, nwb); + nwbExport(nwbNew, 'new.nwb'); + end + + function testLoadWithEmptyIndices(testCase) + nwb = tests.factory.NWBFile(); + + % Add different datatypes to a table, and try to read them in later + % using empty indexing on a DataStub representation + tableToExport = table( ... + {'test'}, ... % Cell + 0, ... % Double + false, ... % Logical + struct('x', 1, 'y', 1, 'z', 1) ... % Struct (compound) + ); + dynamicTable = util.table2nwb(tableToExport); + nwb.acquisition.set('Test', dynamicTable); + + nwbFilePath = testCase.getRandomFilename(); + nwbExport(nwb, nwbFilePath) + + nwbIn = nwbRead(nwbFilePath, 'ignorecache'); + + importedTable = nwbIn.acquisition.get('Test'); + varNames = transpose( string(importedTable.colnames) ); + + for iVarName = varNames + iDataStub = importedTable.vectordata.get(iVarName).data; - if isstruct(tableToExport.(iVarName)) - expectedClass = 'table'; - else - expectedClass = class(tableToExport.(iVarName)); + testCase.assertClass(iDataStub, 'types.untyped.DataStub') + value = iDataStub([]); + testCase.assertEmpty(value) + + if isstruct(tableToExport.(iVarName)) + expectedClass = 'table'; + else + expectedClass = class(tableToExport.(iVarName)); + end + testCase.assertClass(value, expectedClass) + end end - testCase.assertClass(value, expectedClass) end end diff --git a/+tests/+unit/dynamicTableTest.m b/+tests/+unit/dynamicTableTest.m index 54eaa0ee..681a247c 100644 --- a/+tests/+unit/dynamicTableTest.m +++ b/+tests/+unit/dynamicTableTest.m @@ -1,161 +1,165 @@ -function tests = dynamicTableTest() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - rehash(); -end - -function setup(testCase) %#ok - % pass -end - -function testNwbToTableWithReferencedTablesAsRowIndices(testCase) - % The default mode for the toTable() method is to return the row indices - % for dynamic table regions. This test verifies that the data type of - % the converted table columns is int64, the default type for indices. - dtr_table = createDynamicTableWithTableRegionReferences(); - convertedTable = dtr_table.toTable(); - - testCase.verifyClass(convertedTable.dtr_col_a(1), 'int64') - testCase.verifyClass(convertedTable.dtr_col_b(1), 'int64') -end - -function testNwbToTableWithReferencedTablesAsTableRows(testCase) - % An alternative mode for the toTable() method is to return the referenced - % table rows for dynamic table regions as subtables. This test verifies that - % the data type of the converted table columns is table. - dtr_table = createDynamicTableWithTableRegionReferences(); - convertedTable = dtr_table.toTable(false); % Return - - row1colA = convertedTable.dtr_col_a(1); - row1colB = convertedTable.dtr_col_b(1); - if iscell(row1colA); row1colA = row1colA{1}; end - if iscell(row1colB); row1colB = row1colB{1}; end - - testCase.verifyClass(row1colA, 'table') - testCase.verifyClass(row1colB, 'table') -end - -function testClearDynamicTable(testCase) - dtr_table = createDynamicTableWithTableRegionReferences(); - types.util.dynamictable.clear(dtr_table) - - % testCase.verifyEmpty(dtr_table.vectordata) %todo when PR merged - testCase.verifyEqual(size(dtr_table.vectordata), uint64([0,1])) -end - -function testClearDynamicTableV2_1(testCase) - - import matlab.unittest.fixtures.SuppressedWarningsFixture - testCase.applyFixture(SuppressedWarningsFixture('NWB:CheckUnset:InvalidProperties')) - - nwbClearGenerated('.', 'ClearCache', true) - generateCore("2.1.0", "savedir", '.') - rehash(); - table = types.core.DynamicTable( ... - 'description', 'test table with DynamicTableRegion', ... - 'colnames', {'dtr_col_a', 'dtr_col_b'}, ... - 'dtr_col_a', 1:4, ... - 'dtr_col_b', 5:8, ... - 'id', types.core.ElementIdentifiers('data', [0; 1; 2; 3]) ); - - types.util.dynamictable.clear(table) - - % testCase.verifyEmpty(dtr_table.vectordata) %todo when PR merged - testCase.verifyEqual(size(table.vectordata), uint64([0,1])) - - nwbClearGenerated('.','ClearCache',true) - generateCore('savedir', '.'); - rehash(); -end +classdef dynamicTableTest < tests.abstract.NwbTestCase -function testToTableForNdVectorData(testCase) - import matlab.unittest.fixtures.SuppressedWarningsFixture - testCase.applyFixture(... - SuppressedWarningsFixture('NWB:DynamicTable:VectorDataAmbiguousSize')) + methods (TestClassSetup) + function setupClass(testCase) + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end - arrayLength = 5; - numTableRows = 3; - nDimsToTest = [2,3,4]; - - for nDims = nDimsToTest - vectorDataShape = repmat(arrayLength, 1, nDims-1); + methods (Test) + function testNwbToTableWithReferencedTablesAsRowIndices(testCase) + % The default mode for the toTable() method is to return the row indices + % for dynamic table regions. This test verifies that the data type of + % the converted table columns is int64, the default type for indices. + dtr_table = testCase.createDynamicTableWithTableRegionReferences(); + convertedTable = dtr_table.toTable(); + + testCase.verifyClass(convertedTable.dtr_col_a(1), 'int64') + testCase.verifyClass(convertedTable.dtr_col_b(1), 'int64') + end + + function testNwbToTableWithReferencedTablesAsTableRows(testCase) + % An alternative mode for the toTable() method is to return the referenced + % table rows for dynamic table regions as subtables. This test verifies that + % the data type of the converted table columns is table. + dtr_table = testCase.createDynamicTableWithTableRegionReferences(); + convertedTable = dtr_table.toTable(false); % Return + + row1colA = convertedTable.dtr_col_a(1); + row1colB = convertedTable.dtr_col_b(1); + if iscell(row1colA); row1colA = row1colA{1}; end + if iscell(row1colB); row1colB = row1colB{1}; end + + testCase.verifyClass(row1colA, 'table') + testCase.verifyClass(row1colB, 'table') + end + + function testClearDynamicTable(testCase) + dtr_table = testCase.createDynamicTableWithTableRegionReferences(); + types.util.dynamictable.clear(dtr_table) + + % testCase.verifyEmpty(dtr_table.vectordata) %todo when PR merged + testCase.verifyEqual(size(dtr_table.vectordata), uint64([0,1])) + end + + function testClearDynamicTableV2_1(testCase) + + import matlab.unittest.fixtures.SuppressedWarningsFixture + testCase.applyFixture(... + SuppressedWarningsFixture('NWB:CheckUnset:InvalidProperties')) + + typesOutputFolder = testCase.getTypesOutputFolder(); + + nwbClearGenerated(typesOutputFolder, 'ClearCache', true) + generateCore("2.1.0", "savedir", typesOutputFolder) + + table = types.core.DynamicTable( ... + 'description', 'test table with DynamicTableRegion', ... + 'colnames', {'dtr_col_a', 'dtr_col_b'}, ... + 'dtr_col_a', 1:4, ... + 'dtr_col_b', 5:8, ... + 'id', types.core.ElementIdentifiers('data', [0; 1; 2; 3]) ); + + types.util.dynamictable.clear(table) + + testCase.verifyEqual(size(table.vectordata), uint64([0,1])) + + nwbClearGenerated(typesOutputFolder, 'ClearCache',true) + generateCore('savedir', typesOutputFolder); + end + + function testToTableForNdVectorData(testCase) + import matlab.unittest.fixtures.SuppressedWarningsFixture + testCase.applyFixture(... + SuppressedWarningsFixture('NWB:DynamicTable:VectorDataAmbiguousSize')) + + arrayLength = 5; + numTableRows = 3; + nDimsToTest = [2,3,4]; + + for nDims = nDimsToTest + vectorDataShape = repmat(arrayLength, 1, nDims-1); + + dynamicTable = types.hdmf_common.DynamicTable( ... + 'description', 'test table with n-dimensional VectorData', ... + 'colnames', {'columnA', 'columnB'}, ... + 'columnA', types.hdmf_common.VectorData('data', randi(10, [vectorDataShape, numTableRows])), ... + 'columnB', types.hdmf_common.VectorData('data', randi(10, [vectorDataShape, numTableRows])), ... + 'id', types.hdmf_common.ElementIdentifiers('data', (0:numTableRows-1)' ) ); + + T = dynamicTable.toTable(); + testCase.verifyClass(T, 'table'); + testCase.verifySize(T.columnA, [numTableRows, vectorDataShape]) + end + end + + function testToTableForTableImportedFromFile(testCase) + fileName = testCase.getRandomFilename(); + + nwb = tests.factory.NWBFile(); + + dynamicTable = testCase.createDynamicTable(); + nwb.acquisition.set('DynamicTable', dynamicTable); - dynamicTable = types.hdmf_common.DynamicTable( ... - 'description', 'test table with n-dimensional VectorData', ... - 'colnames', {'columnA', 'columnB'}, ... - 'columnA', types.hdmf_common.VectorData('data', randi(10, [vectorDataShape, numTableRows])), ... - 'columnB', types.hdmf_common.VectorData('data', randi(10, [vectorDataShape, numTableRows])), ... - 'id', types.hdmf_common.ElementIdentifiers('data', (0:numTableRows-1)' ) ); - - T = dynamicTable.toTable(); - testCase.verifyClass(T, 'table'); - testCase.verifySize(T.columnA, [numTableRows, vectorDataShape]) + nwbExport(nwb, fileName) + + nwbIn = nwbRead(fileName); + + T = nwbIn.acquisition.get('DynamicTable').toTable(); + testCase.verifyClass(T, 'table') + end end -end - -function testToTableForTableImportedFromFile(testCase) - fileName = "testToTableForTableImportedFromFile.nwb"; - nwb = NwbFile( ... - 'session_description', 'test file for nwb export', ... - 'identifier', 'export_test', ... - 'session_start_time', datetime("now", 'TimeZone', 'local') ); - - numTableRows = 10; - - dynamicTable = types.hdmf_common.DynamicTable( ... - 'description', 'test table with n-dimensional VectorData', ... - 'colnames', {'columnA', 'columnB'}, ... - 'columnA', types.hdmf_common.VectorData(... + methods (Static, Access=private) + + % Non-test functions + function dtr_table = createDynamicTableWithTableRegionReferences() + % Create a dynamic table with two columns, where the data of each column is + % a dynamic table region referencing another dynamic table. + T = table([1;2;3], {'a';'b';'c'}, 'VariableNames', {'col1', 'col2'}); + T.Properties.VariableDescriptions = {'column #1', 'column #2'}; + + T = util.table2nwb(T); + + dtr_col_a = types.hdmf_common.DynamicTableRegion( ... + 'description', 'references multiple rows of earlier table', ... + 'data', [0; 1; 1; 0], ... # 0-indexed + 'table',types.untyped.ObjectView(T) ... % object view of target table + ); + + dtr_col_b = types.hdmf_common.DynamicTableRegion( ... + 'description', 'references multiple rows of earlier table', ... + 'data', [1; 2; 2; 1], ... # 0-indexed + 'table',types.untyped.ObjectView(T) ... % object view of target table + ); + + dtr_table = types.hdmf_common.DynamicTable( ... + 'description', 'test table with DynamicTableRegion', ... + 'colnames', {'dtr_col_a', 'dtr_col_b'}, ... + 'dtr_col_a', dtr_col_a, ... + 'dtr_col_b', dtr_col_b, ... + 'id',types.hdmf_common.ElementIdentifiers('data', [0; 1; 2; 3]) ... + ); + end + + function dynamicTable = createDynamicTable() + numTableRows = 10; + + columnA = types.hdmf_common.VectorData(... 'description', 'first_column', ... - 'data', randi(10, [1, numTableRows])), ... - 'columnB', types.hdmf_common.VectorData(... + 'data', randi(10, [1, numTableRows])); + columnB = types.hdmf_common.VectorData(... 'description', 'second_column', ... - 'data', randi(10, [1, numTableRows])), ... - 'id', types.hdmf_common.ElementIdentifiers('data', (0:numTableRows-1)' ) ); - - nwb.acquisition.set('DynamicTable', dynamicTable); - nwbExport(nwb, fileName) - - nwbIn = nwbRead(fileName); - - T = nwbIn.acquisition.get('DynamicTable').toTable(); - testCase.verifyClass(T, 'table') -end - - -% Non-test functions -function dtr_table = createDynamicTableWithTableRegionReferences() - % Create a dynamic table with two columns, where the data of each column is - % a dynamic table region referencing another dynamic table. - T = table([1;2;3], {'a';'b';'c'}, 'VariableNames', {'col1', 'col2'}); - T.Properties.VariableDescriptions = {'column #1', 'column #2'}; - - T = util.table2nwb(T); - - dtr_col_a = types.hdmf_common.DynamicTableRegion( ... - 'description', 'references multiple rows of earlier table', ... - 'data', [0; 1; 1; 0], ... # 0-indexed - 'table',types.untyped.ObjectView(T) ... % object view of target table - ); - - dtr_col_b = types.hdmf_common.DynamicTableRegion( ... - 'description', 'references multiple rows of earlier table', ... - 'data', [1; 2; 2; 1], ... # 0-indexed - 'table',types.untyped.ObjectView(T) ... % object view of target table - ); - - dtr_table = types.hdmf_common.DynamicTable( ... - 'description', 'test table with DynamicTableRegion', ... - 'colnames', {'dtr_col_a', 'dtr_col_b'}, ... - 'dtr_col_a', dtr_col_a, ... - 'dtr_col_b', dtr_col_b, ... - 'id',types.hdmf_common.ElementIdentifiers('data', [0; 1; 2; 3]) ... - ); + 'data', randi(10, [1, numTableRows])); + idColumn = types.hdmf_common.ElementIdentifiers(... + 'data', (0:numTableRows-1)' ); + + dynamicTable = types.hdmf_common.DynamicTable( ... + 'description', 'test table with n-dimensional VectorData', ... + 'colnames', {'columnA', 'columnB'}, ... + 'columnA', columnA, ... + 'columnB', columnB, ... + 'id', idColumn); + end + end end diff --git a/+tests/+unit/linkTest.m b/+tests/+unit/linkTest.m index 986f9665..5a76d057 100644 --- a/+tests/+unit/linkTest.m +++ b/+tests/+unit/linkTest.m @@ -1,112 +1,110 @@ -function tests = linkTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -rehash(); -end - -function testExternLinkConstructor(testCase) -l = types.untyped.ExternalLink('myfile.nwb', '/mypath'); -testCase.verifyEqual(l.path, '/mypath'); -testCase.verifyEqual(l.filename, 'myfile.nwb'); -end - -function testSoftLinkConstructor(testCase) -l = types.untyped.SoftLink('/mypath'); -testCase.verifyEqual(l.path, '/mypath'); -end - -function testLinkExportSoft(testCase) -fid = H5F.create('test.nwb'); -close = onCleanup(@()H5F.close(fid)); -l = types.untyped.SoftLink('/mypath'); -l.export(fid, 'l1'); -info = h5info('test.nwb'); -testCase.verifyEqual(info.Links.Name, 'l1'); -testCase.verifyEqual(info.Links.Type, 'soft link'); -testCase.verifyEqual(info.Links.Value, {'/mypath'}); -end - -function testLinkExportExternal(testCase) -fid = H5F.create('test.nwb'); -close = onCleanup(@()H5F.close(fid)); -l = types.untyped.ExternalLink('extern.nwb', '/mypath'); -l.export(fid, 'l1'); -info = h5info('test.nwb'); -testCase.verifyEqual(info.Links.Name, 'l1'); -testCase.verifyEqual(info.Links.Type, 'external link'); -testCase.verifyEqual(info.Links.Value, {'extern.nwb';'/mypath'}); -end - -function testSoftResolution(testCase) -nwb = NwbFile; -dev = types.core.Device; -nwb.general_devices.set('testDevice', dev); -nwb.general_extracellular_ephys.set('testEphys',... - types.core.ElectrodeGroup('device',... - types.untyped.SoftLink('/general/devices/testDevice'))); -testCase.verifyEqual(dev,... - nwb.general_extracellular_ephys.get('testEphys').device.deref(nwb)); -end - -function testExternalResolution(testCase) -nwb = NwbFile('identifier', 'EXTERNAL',... - 'session_description', 'external link test',... - 'session_start_time', datetime()); - -expectedData = rand(100,1); -stubDtr = types.hdmf_common.DynamicTableRegion(... - 'table', types.untyped.ObjectView('/acquisition/es1'),... - 'data', 1, ... - 'description', 'dtr stub that points to electrical series illegally'); % do not do this at home. -expected = types.core.ElectricalSeries('data', expectedData,... - 'data_unit', 'volts', ... - 'timestamps', (1:100)', ... - 'electrodes', stubDtr); -nwb.acquisition.set('es1', expected); -nwb.export('test1.nwb'); - -externalLink = types.untyped.ExternalLink('test1.nwb', '/acquisition/es1'); -tests.util.verifyContainerEqual(testCase, externalLink.deref(), expected); -externalDataLink = types.untyped.ExternalLink('test1.nwb', '/acquisition/es1/data'); -% for datasets, a Datastub is returned. -testCase.verifyEqual(externalDataLink.deref().load(), expectedData); +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + linkTest < matlab.unittest.TestCase -nwb.acquisition.clear(); -nwb.acquisition.set('lfp', types.core.LFP('eslink', externalLink)); -nwb.export('test2.nwb'); - -metaExternalLink = types.untyped.ExternalLink('test2.nwb', '/acquisition/lfp/eslink'); -% for links, deref() should return its own link. -tests.util.verifyContainerEqual(testCase, metaExternalLink.deref().deref(), expected); -end - -function testDirectTypeAssignmentToSoftLinkProperty(testCase) - device = types.core.Device('description', 'test_device'); - electrodeGroup = types.core.ElectrodeGroup(... - 'description', 'test_group', ... - 'device', device); - - testCase.verifyClass(electrodeGroup.device, 'types.untyped.SoftLink') - testCase.verifyClass(electrodeGroup.device.target, 'types.core.Device') -end + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end -function testWrongTypeInSoftLinkAssignment(testCase) - % Adding an OpticalChannel as device for ElectrodeGroup should fail. - function createElectrodeGroupWithWrongDeviceType() - not_a_device = types.core.OpticalChannel('description', 'test_channel'); - electrodeGroup = types.core.ElectrodeGroup(... - 'description', 'test_group', ... - 'device', not_a_device); %#ok + methods (Test) + function testExternLinkConstructor(testCase) + l = types.untyped.ExternalLink('myfile.nwb', '/mypath'); + testCase.verifyEqual(l.path, '/mypath'); + testCase.verifyEqual(l.filename, 'myfile.nwb'); + end + + function testSoftLinkConstructor(testCase) + l = types.untyped.SoftLink('/mypath'); + testCase.verifyEqual(l.path, '/mypath'); + end + + function testLinkExportSoft(testCase) + fid = H5F.create('test.nwb'); + close = onCleanup(@()H5F.close(fid)); + l = types.untyped.SoftLink('/mypath'); + l.export(fid, 'l1'); + info = h5info('test.nwb'); + testCase.verifyEqual(info.Links.Name, 'l1'); + testCase.verifyEqual(info.Links.Type, 'soft link'); + testCase.verifyEqual(info.Links.Value, {'/mypath'}); + end + + function testLinkExportExternal(testCase) + fid = H5F.create('test.nwb'); + close = onCleanup(@()H5F.close(fid)); + l = types.untyped.ExternalLink('extern.nwb', '/mypath'); + l.export(fid, 'l1'); + info = h5info('test.nwb'); + testCase.verifyEqual(info.Links.Name, 'l1'); + testCase.verifyEqual(info.Links.Type, 'external link'); + testCase.verifyEqual(info.Links.Value, {'extern.nwb';'/mypath'}); + end + + function testSoftResolution(testCase) + nwb = NwbFile; + dev = types.core.Device; + nwb.general_devices.set('testDevice', dev); + nwb.general_extracellular_ephys.set('testEphys',... + types.core.ElectrodeGroup('device',... + types.untyped.SoftLink('/general/devices/testDevice'))); + testCase.verifyEqual(dev,... + nwb.general_extracellular_ephys.get('testEphys').device.deref(nwb)); + end + + function testExternalResolution(testCase) + nwb = NwbFile('identifier', 'EXTERNAL',... + 'session_description', 'external link test',... + 'session_start_time', datetime()); + + expectedData = rand(100,1); + stubDtr = types.hdmf_common.DynamicTableRegion(... + 'table', types.untyped.ObjectView('/acquisition/es1'),... + 'data', 1, ... + 'description', 'dtr stub that points to electrical series illegally'); % do not do this at home. + expected = types.core.ElectricalSeries('data', expectedData,... + 'data_unit', 'volts', ... + 'timestamps', (1:100)', ... + 'electrodes', stubDtr); + nwb.acquisition.set('es1', expected); + nwb.export('test1.nwb'); + + externalLink = types.untyped.ExternalLink('test1.nwb', '/acquisition/es1'); + tests.util.verifyContainerEqual(testCase, externalLink.deref(), expected); + externalDataLink = types.untyped.ExternalLink('test1.nwb', '/acquisition/es1/data'); + % for datasets, a Datastub is returned. + testCase.verifyEqual(externalDataLink.deref().load(), expectedData); + + nwb.acquisition.clear(); + nwb.acquisition.set('lfp', types.core.LFP('eslink', externalLink)); + nwb.export('test2.nwb'); + + metaExternalLink = types.untyped.ExternalLink('test2.nwb', '/acquisition/lfp/eslink'); + % for links, deref() should return its own link. + tests.util.verifyContainerEqual(testCase, metaExternalLink.deref().deref(), expected); + end + + function testDirectTypeAssignmentToSoftLinkProperty(testCase) + device = types.core.Device('description', 'test_device'); + electrodeGroup = types.core.ElectrodeGroup(... + 'description', 'test_group', ... + 'device', device); + + testCase.verifyClass(electrodeGroup.device, 'types.untyped.SoftLink') + testCase.verifyClass(electrodeGroup.device.target, 'types.core.Device') + end + + function testWrongTypeInSoftLinkAssignment(testCase) + % Adding an OpticalChannel as device for ElectrodeGroup should fail. + function createElectrodeGroupWithWrongDeviceType() + not_a_device = types.core.OpticalChannel('description', 'test_channel'); + electrodeGroup = types.core.ElectrodeGroup(... + 'description', 'test_group', ... + 'device', not_a_device); %#ok + end + testCase.verifyError(@createElectrodeGroupWithWrongDeviceType, ... + 'NWB:CheckDType:InvalidNeurodataType') + end end - testCase.verifyError(@createElectrodeGroupWithWrongDeviceType, ... - 'NWB:CheckDType:InvalidNeurodataType') end diff --git a/+tests/+unit/multipleConstrainedTest.m b/+tests/+unit/multipleConstrainedTest.m deleted file mode 100644 index bcbb5318..00000000 --- a/+tests/+unit/multipleConstrainedTest.m +++ /dev/null @@ -1,32 +0,0 @@ -function tests = multipleConstrainedTest() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); - schemaPath = fullfile(misc.getMatnwbDir(),... - '+tests', '+unit', 'multipleConstrainedSchema', 'mcs.namespace.yaml'); - generateExtension(schemaPath, 'savedir', '.'); - rehash(); -end - -function testRoundabout(testCase) - MultiSet = types.mcs.MultiSetContainer(); - MultiSet.something.set('A', types.mcs.ArbitraryTypeA()); - MultiSet.something.set('B', types.mcs.ArbitraryTypeB()); - MultiSet.something.set('Data', types.mcs.DatasetType('data', ones(3,3))); - nwbExpected = NwbFile(... - 'identifier', 'MCS', ... - 'session_description', 'multiple constrained schema testing', ... - 'session_start_time', datetime()); - nwbExpected.acquisition.set('multiset', MultiSet); - nwbExport(nwbExpected, 'testmcs.nwb'); - - tests.util.verifyContainerEqual(testCase, nwbRead('testmcs.nwb', 'ignorecache'), nwbExpected); -end \ No newline at end of file diff --git a/+tests/+unit/multipleShapesTest.m b/+tests/+unit/multipleShapesTest.m deleted file mode 100644 index d09af025..00000000 --- a/+tests/+unit/multipleShapesTest.m +++ /dev/null @@ -1,64 +0,0 @@ -function tests = multipleShapesTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -schemaPath = fullfile(misc.getMatnwbDir(),... - '+tests', '+unit', 'multipleShapesSchema', 'mss.namespace.yaml'); -generateExtension(schemaPath, 'savedir', '.'); -rehash(); -end - -function testMultipleShapesDataset(testCase) -msd = types.mss.MultiShapeDataset('data', rand(3, 1)); -msd.data = rand(1, 5, 7); -roundabout(testCase, msd); -end - -function testNullShapeDataset(testCase) -nsd = types.mss.NullShapeDataset; -randiMax = intmax('int8') - 1; -for i=1:100 - %test validation - nsd.data = rand(randi(randiMax) + 1, 3); -end -roundabout(testCase, nsd); -end - -function testMultipleNullShapesDataset(testCase) -mnsd = types.mss.MultiNullShapeDataset; -randiMax = intmax('int8'); -for i=1:100 - if rand() > 0.5 - mnsd.data = rand(randi(randiMax), 1); - else - mnsd.data = rand(randi(randiMax), randi(randiMax)); - end -end -roundabout(testCase, mnsd); -end - -function testInheritedDtypeDataset(testCase) -nid = types.mss.NarrowInheritedDataset; -nid.data = 'Inherited Dtype Dataset'; -roundabout(testCase, nid); -end - -%% Convenience -function roundabout(testCase, dataset) -nwb = NwbFile('identifier', 'MSS', 'session_description', 'test',... - 'session_start_time', '2017-04-15T12:00:00.000000-08:00',... - 'timestamps_reference_time', '2017-04-15T12:00:00.000000-08:00'); -wrapper = types.mss.MultiShapeWrapper('shaped_data', dataset); -nwb.acquisition.set('wrapper', wrapper); -filename = 'multipleShapesTest.nwb'; -nwbExport(nwb, filename); -tests.util.verifyContainerEqual(testCase, nwbRead(filename, 'ignorecache'), nwb); -end \ No newline at end of file diff --git a/+tests/+unit/nwbExportTest.m b/+tests/+unit/nwbExportTest.m index 5ef8eaa5..a1d91e7b 100644 --- a/+tests/+unit/nwbExportTest.m +++ b/+tests/+unit/nwbExportTest.m @@ -1,40 +1,24 @@ -classdef nwbExportTest < matlab.unittest.TestCase - - properties - NwbObject - OutputFolder = "out" - end +classdef nwbExportTest < tests.abstract.NwbTestCase +% nwbExportTest - Unit tests for testing various aspects of exporting to an NWB file. methods (TestClassSetup) - function setupClass(testCase) - % Get the root path of the matnwb repository - rootPath = misc.getMatnwbDir(); - - % Use a fixture to add the folder to the search path - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); - + function setupTemporaryWorkingFolder(testCase) % Use a fixture to create a temporary working directory testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); - generateCore('savedir', '.'); end end methods (TestMethodSetup) - function setupMethod(testCase) - testCase.NwbObject = testCase.initNwbFile(); - - if isfolder( testCase.OutputFolder ) - rmdir(testCase.OutputFolder, "s") - end - mkdir(testCase.OutputFolder) - end + % No method setup end methods (Test) function testExportNwbFileWithMissingRequiredProperties(testCase) nwb = NwbFile(); - nwbFilePath = fullfile(testCase.OutputFolder, 'testfile.nwb'); - testCase.verifyError(@(file, path) nwbExport(nwb, nwbFilePath), ... + nwbFilePath = testCase.getRandomFilename(); + + testCase.verifyError(... + @() nwbExport(nwb, nwbFilePath), ... 'NWB:RequiredPropertyMissing') end @@ -43,14 +27,15 @@ function testExportNwbFileWithMissingRequiredAttribute(testCase) % 'description' property to be set (description is a required % attribute of ProcessingModule). + nwbFile = tests.factory.NWBFile(); processingModule = types.core.ProcessingModule(); - testCase.NwbObject.processing.set('TestModule', processingModule); + nwbFile.processing.set('TestModule', processingModule); + + nwbFilePath = testCase.getRandomFilename(); - nwbFilePath = 'testExportNwbFileWithMissingRequiredAttribute.nwb'; - testCase.verifyError(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath), ... + testCase.verifyError(... + @() nwbExport(nwbFile, nwbFilePath), ... 'NWB:RequiredPropertyMissing') - - testCase.NwbObject.processing.remove('TestModule'); end function testExportNwbFileWithMissingRequiredLink(testCase) @@ -59,23 +44,21 @@ function testExportNwbFileWithMissingRequiredLink(testCase) % IntracellularElectrode and exporting the object should throw % an error. + nwbFile = tests.factory.NWBFile(); electrode = types.core.IntracellularElectrode('description', 'test'); - testCase.NwbObject.general_intracellular_ephys.set('Electrode', electrode); + nwbFile.general_intracellular_ephys.set('Electrode', electrode); - nwbFilePath = 'testExportNwbFileWithMissingRequiredLink.nwb'; - testCase.verifyError(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath), ... + nwbFilePath = testCase.getRandomFilename(); + testCase.verifyError(... + @() nwbExport(nwbFile, nwbFilePath), ... 'NWB:RequiredPropertyMissing') - - % Clean up: the NwbObject is reused by other tests. - testCase.NwbObject.general_intracellular_ephys.remove('Electrode'); end function testExportWithMissingRequiredDependentProperty(testCase) - nwbFile = testCase.initNwbFile(); - fileName = "testExportWithMissingRequiredDependentProperty"; - - % Should work without error - nwbExport(nwbFile, fileName + "_1.nwb") + nwbFile = tests.factory.NWBFile(); + + nwbFilePath = testCase.getRandomFilename(); + nwbExport(nwbFile, nwbFilePath) % Should work without error % Now we add a value to the "general_source_script" property. This % is a dataset with a required attribute called "file_name". @@ -86,95 +69,115 @@ function testExportWithMissingRequiredDependentProperty(testCase) % Verify that exporting the file throws an error, stating that a % required property (i.e general_source_script_file_name) is missing + nwbFilePath = testCase.getRandomFilename(); testCase.verifyError( ... - @(nwbObj, filePath) nwbExport(nwbFile, fileName + "_2.nwb"), ... + @() nwbExport(nwbFile, nwbFilePath), ... 'NWB:DependentRequiredPropertyMissing') end - function testExportFileWithAttributeOfEmptyDataset(testCase) + function testExportDependentAttributeWithMissingParent(testCase) + import matlab.unittest.fixtures.SuppressedWarningsFixture + testCase.applyFixture(... + SuppressedWarningsFixture('NWB:AttributeDependencyNotSet')) + + nwbFile = tests.factory.NWBFile(); + nwbFile.general_source_script_file_name = 'my_test_script.m'; + nwbFilePath = testCase.getRandomFilename(); + + testCase.verifyWarning(... + @() nwbExport(nwbFile, nwbFilePath), ... + 'NWB:DependentAttributeNotExported') + + % Add value for dataset which attribute depends on and export again + nwbFile.general_source_script = 'my test'; + nwbFilePath = testCase.getRandomFilename(); - nwbFile = testCase.initNwbFile(); + testCase.verifyWarningFree(@() nwbExport(nwbFile, nwbFilePath)) + end - % Add device to nwb object + function testExportFileWithAttributeOfEmptyDataset(testCase) + import matlab.unittest.fixtures.SuppressedWarningsFixture + + nwbFile = tests.factory.NWBFile(); + device = types.core.Device(); nwbFile.general_devices.set('Device', device); - imaging_plane = types.core.ImagingPlane( ... - 'device', types.untyped.SoftLink(device), ... - 'excitation_lambda', 600., ... - 'indicator', 'GFP', ... - 'location', 'my favorite brain location'); - nwbFile.general_optophysiology.set('ImagingPlane', imaging_plane); - + imagingPlane = tests.factory.ImagingPlane(device); + nwbFile.general_optophysiology.set('ImagingPlane', imagingPlane); + + nwbFilePath = testCase.getRandomFilename(); testCase.verifyWarningFree(... - @() nwbExport(nwbFile, 'test_1.nwb')) + @() nwbExport(nwbFile, nwbFilePath)) % Change value for attribute of the grid_spacing dataset. - % Because grid_spacing is not set, this attribute value is not - % exported to the file. Verify that warning is issued. - imaging_plane.grid_spacing_unit = "microns"; + testCase.applyFixture(... + SuppressedWarningsFixture('NWB:AttributeDependencyNotSet')) + imagingPlane.grid_spacing_unit = "microns"; + % Because grid_spacing is not set, this attribute value is not + % exported to the file. Verify that warning is issued on export. + nwbFilePath = testCase.getRandomFilename(); testCase.verifyWarning(... - @() nwbExport(nwbFile, 'test_2.nwb'), ... + @() nwbExport(nwbFile, nwbFilePath), ... 'NWB:DependentAttributeNotExported') end function testExportTimeseriesWithMissingTimestampsAndStartingTime(testCase) + nwbFile = tests.factory.NWBFile(); + time_series = types.core.TimeSeries( ... 'data', linspace(0, 0.4, 50), ... 'description', 'a test series', ... 'data_unit', 'n/a' ... - ); - - testCase.NwbObject.acquisition.set('time_series', time_series); - nwbFilePath = fullfile(testCase.OutputFolder, 'testfile.nwb'); - testCase.verifyError(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath), ... - 'NWB:CustomConstraintUnfulfilled') - end - - function testExportDependentAttributeWithMissingParentA(testCase) - testCase.NwbObject.general_source_script_file_name = 'my_test_script.m'; - nwbFilePath = fullfile(testCase.OutputFolder, 'test_part1.nwb'); - testCase.verifyWarning(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath), 'NWB:DependentAttributeNotExported') + ); + nwbFile.acquisition.set('time_series', time_series); - % Add value for dataset which attribute depends on and export again - testCase.NwbObject.general_source_script = 'my test'; - nwbFilePath = fullfile(testCase.OutputFolder, 'test_part2.nwb'); - testCase.verifyWarningFree(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath)) + nwbFilePath = testCase.getRandomFilename(); + testCase.verifyError(... + @() nwbExport(nwbFile, nwbFilePath), ... + 'NWB:CustomConstraintUnfulfilled') end - + function testExportTimeseriesWithoutStartingTimeRate(testCase) + nwbFile = tests.factory.NWBFile(); + time_series = types.core.TimeSeries( ... 'data', linspace(0, 0.4, 50), ... 'starting_time', 1, ... 'description', 'a test series', ... 'data_unit', 'n/a' ... ); - testCase.NwbObject.acquisition.set('time_series', time_series); - nwbFilePath = fullfile(testCase.OutputFolder, 'test_part1.nwb'); - testCase.verifyError(@(f, fn) nwbExport(testCase.NwbObject, nwbFilePath), 'NWB:CustomConstraintUnfulfilled') + nwbFile.acquisition.set('time_series', time_series); + + nwbFilePath = testCase.getRandomFilename(); + testCase.verifyError(... + @() nwbExport(nwbFile, nwbFilePath), ... + 'NWB:CustomConstraintUnfulfilled') end function testEmbeddedSpecs(testCase) - nwbFileName = 'testEmbeddedSpecs.nwb'; - - % Install extension. - nwbInstallExtension(["ndx-miniscope", "ndx-photostim"], 'savedir', '.') + % Install extensions, one will be used, the other will not. + testCase.installExtension("ndx-miniscope") + testCase.addTeardown(@() testCase.clearExtension("ndx-miniscope")) + testCase.installExtension("ndx-photostim") + testCase.addTeardown(@() testCase.clearExtension("ndx-photostim")) % Export a file not using a type from an extension - nwb = testCase.initNwbFile(); - - nwbExport(nwb, nwbFileName); - embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFileName); + nwb = tests.factory.NWBFile(); + nwbFilePath = testCase.getRandomFilename(); + nwbExport(nwb, nwbFilePath); + + % Verify that no namespaces were embedded in file + embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFilePath); testCase.verifyEmpty(embeddedNamespaces) - ts = types.core.TimeSeries(... - 'data', rand(1,10), 'timestamps', 1:10, 'data_unit', 'test'); + ts = tests.factory.TimeSeriesWithTimestamps(); nwb.acquisition.set('test', ts); - nwbExport(nwb, nwbFileName); - embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFileName); + nwbExport(nwb, nwbFilePath); + embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFilePath); % Verify that extension namespace is not part of embedded specs testCase.verifyEqual(sort(embeddedNamespaces), {'core', 'hdmf-common'}) @@ -183,8 +186,8 @@ function testEmbeddedSpecs(testCase) testDevice = types.ndx_photostim.Laser('model', 'Spectra-Physics'); nwb.general_devices.set('TestDevice', testDevice); - nwbExport(nwb, nwbFileName); - embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFileName); + nwbExport(nwb, nwbFilePath); + embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFilePath); % Verify that extension namespace is part of embedded specs. testCase.verifyEqual(sort(embeddedNamespaces), {'core', 'hdmf-common', 'ndx-photostim'}) @@ -200,9 +203,9 @@ function testEmbeddedSpecs(testCase) % See matnwb issue #649: % https://github.com/NeurodataWithoutBorders/matnwb/issues/649 nwb.general_devices.remove('TestDevice'); - nwbExport(nwb, nwbFileName); + nwbExport(nwb, nwbFilePath); - embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFileName); + embeddedNamespaces = io.spec.listEmbeddedSpecNamespaces(nwbFilePath); testCase.verifyEqual(sort(embeddedNamespaces), {'core', 'hdmf-common'}) end @@ -211,17 +214,23 @@ function testWarnIfMissingNamespaceSpecification(testCase) % deleted from disk before an nwb object containing types from % that namespace is exported to file. - nwbFileName = 'testWarnIfMissingNamespaceSpecification.nwb'; + % A cached namespace is manually deleted in this test, so will + % use a fixture to ignore the warning for a missing file when + % the installed extension is cleared. + import matlab.unittest.fixtures.SuppressedWarningsFixture + testCase.applyFixture(SuppressedWarningsFixture('MATLAB:DELETE:FileNotFound')) + + nwbFilePath = testCase.getRandomFilename(); - % Install extension. - nwbInstallExtension("ndx-photostim", 'savedir', '.') + % Install extension. + testCase.installExtension("ndx-photostim"); + testCase.addTeardown(@() testCase.clearExtension("ndx-photostim")) % Export a file not using a type from an extension - nwb = testCase.initNwbFile(); + nwb = tests.factory.NWBFile(); % Add a timeseries object - ts = types.core.TimeSeries(... - 'data', rand(1,10), 'timestamps', 1:10, 'data_unit', 'test'); + ts = tests.factory.TimeSeriesWithTimestamps(); nwb.acquisition.set('test', ts); % Add type from ndx-photostim extension. @@ -230,22 +239,15 @@ function testWarnIfMissingNamespaceSpecification(testCase) % Simulate the rare case where a user might delete the cached % namespace specification before exporting a file - cachedNamespaceSpec = fullfile("namespaces/ndx-photostim.mat"); + generatedTypesOutputFolder = testCase.getTypesOutputFolder(); + cachedNamespaceSpec = fullfile(generatedTypesOutputFolder, ... + "namespaces", "ndx-photostim.mat"); delete(cachedNamespaceSpec) % Test that warning for missing namespace works testCase.verifyWarning(... - @() nwbExport(nwb, nwbFileName), ... + @() nwbExport(nwb, nwbFilePath), ... 'NWB:validators:MissingEmbeddedNamespace') end end - - methods (Static) - function nwb = initNwbFile() - nwb = NwbFile( ... - 'session_description', 'test file for nwb export', ... - 'identifier', 'export_test', ... - 'session_start_time', datetime("now", 'TimeZone', 'local') ); - end - end end diff --git a/+tests/+unit/read_nwbfile_with_pynwb.py b/+tests/+unit/read_nwbfile_with_pynwb.py deleted file mode 100644 index 546e353d..00000000 --- a/+tests/+unit/read_nwbfile_with_pynwb.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys -from pynwb import NWBHDF5IO - -def pynwbread(): - if len(sys.argv) > 1: - # Take the first input argument - nwb_file_path = sys.argv[1] - print(f"Reading file '{nwb_file_path}' with pynwb.") - - with NWBHDF5IO(nwb_file_path, "r") as io: - read_nwbfile = io.read() - - else: - raise Exception("No filepath was provided") - -if __name__ == "__main__": - pynwbread() \ No newline at end of file diff --git a/+tests/+unit/regionViewTest.m b/+tests/+unit/regionViewTest.m deleted file mode 100644 index 1ca92898..00000000 --- a/+tests/+unit/regionViewTest.m +++ /dev/null @@ -1,62 +0,0 @@ -function tests = regionViewTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -schemaPath = fullfile(misc.getMatnwbDir(),... - '+tests', '+unit', 'regionReferenceSchema', 'rrs.namespace.yaml'); -generateExtension(schemaPath, 'savedir', '.'); -rehash(); -end - -function testRegionViewIo(testCase) -nwb = NwbFile(... - 'identifier', 'REGIONREF',... - 'session_description', 'region ref test',... - 'session_start_time', datetime()); - -rcContainer = types.rrs.RefContainer('data', types.rrs.RefData('data', rand(10, 10, 10, 10, 10))); -nwb.acquisition.set('refdata', rcContainer); - -for i = 1:100 - rcAttrRef = types.untyped.RegionView(... - rcContainer.data,... - getRandInd(10),... - getRandInd(10),... - getRandInd(10),... - getRandInd(10),... - getRandInd(10)); - rcDataRef = types.untyped.RegionView(... - rcContainer.data,... - 1:getRandInd(10),... - 1:getRandInd(10),... - 1:getRandInd(10),... - 1:getRandInd(10),... - 1:getRandInd(10)); - nwb.acquisition.set(sprintf('ref%d', i),... - types.rrs.ContainerReference(... - 'attribute_regref', rcAttrRef,... - 'data_regref', rcDataRef)); -end -nwb.export('test.nwb'); -nwbActual = nwbRead('test.nwb', 'ignorecache'); -tests.util.verifyContainerEqual(testCase, nwbActual, nwb); - -for i = 1:100 - refName = sprintf('ref%d', i); - Reference = nwb.acquisition.get(refName); - testCase.verifyEqual(Reference.attribute_regref.refresh(nwb),... - Reference.attribute_regref.refresh(nwbActual)); -end -end - -function ind = getRandInd(indEnd) -ind = round(rand() * (indEnd - 1)) + 1; -end \ No newline at end of file diff --git a/+tests/+unit/searchTest.m b/+tests/+unit/searchTest.m index 7e4d5421..0ba80bac 100644 --- a/+tests/+unit/searchTest.m +++ b/+tests/+unit/searchTest.m @@ -1,30 +1,28 @@ -function tests = searchTest() -tests = functiontests(localfunctions); -end - -function setupOnce(testCase) -rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); -testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + searchTest < matlab.unittest.TestCase + + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end -function setup(testCase) -testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -generateCore('savedir', '.'); -rehash(); + methods (Test) + function testSearch(testCase) + nwb = NwbFile(); + testCase.assertEmpty(nwb.searchFor('types.core.TimeSeries')); + + nwb.acquisition.set('ts1', types.core.TimeSeries()); + testCase.assertNotEmpty(nwb.searchFor('types.core.TimeSeries')); + testCase.assertNotEmpty(nwb.searchFor('types.core.timeseries')); + nwb.acquisition.set('pc1', types.core.PatchClampSeries()); + + % default search does NOT include subclasses + testCase.assertLength(nwb.searchFor('types.core.TimeSeries'), 1); + + % use includeSubClasses keyword + testCase.assertLength(nwb.searchFor('types.core.TimeSeries', 'includeSubClasses'), 2); + end + end end - -function testSearch(testCase) -nwb = NwbFile(); -testCase.assertEmpty(nwb.searchFor('types.core.TimeSeries')); - -nwb.acquisition.set('ts1', types.core.TimeSeries()); -testCase.assertNotEmpty(nwb.searchFor('types.core.TimeSeries')); -testCase.assertNotEmpty(nwb.searchFor('types.core.timeseries')); -nwb.acquisition.set('pc1', types.core.PatchClampSeries()); - -% default search does NOT include subclasses -testCase.assertLength(nwb.searchFor('types.core.TimeSeries'), 1); - -% use includeSubClasses keyword -testCase.assertLength(nwb.searchFor('types.core.TimeSeries', 'includeSubClasses'), 2); -end \ No newline at end of file diff --git a/+tests/+unit/untypedSetTest.m b/+tests/+unit/untypedSetTest.m index 588a8667..2631cc45 100644 --- a/+tests/+unit/untypedSetTest.m +++ b/+tests/+unit/untypedSetTest.m @@ -1,76 +1,76 @@ -function tests = untypedSetTest() - tests = functiontests(localfunctions); -end - -function setupOnce(testCase) - rootPath = fullfile(fileparts(mfilename('fullpath')), '..', '..'); - testCase.applyFixture(matlab.unittest.fixtures.PathFixture(rootPath)); -end - -function setup(testCase) - testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); -end - -function testCreateSetWithFunctionInput(testCase) - set = types.untyped.Set(@(key, value) true); - testCase.verifyNotEmpty(set.ValidationFcn) -end - -function testCreateSetFromStruct(testCase) - untypedSet = types.untyped.Set( struct('a',1, 'b', 2) ); - testCase.verifyEqual(untypedSet.get('a'), 1) -end - -function testCreateSetFromNvPairs(testCase) - untypedSet = types.untyped.Set( 'a',1, 'b', 2 ); - testCase.verifyEqual(untypedSet.get('a'), 1) -end - -function testCreateSetFromNvPairsPlusFunctionHandle(testCase) - untypedSet = types.untyped.Set( 'a',1, 'b', 2, @(key, value) disp('Hello World')); - testCase.verifyEqual(untypedSet.get('a'), 1) -end - -function testDisplayEmptyObject(testCase) - emptyUntypedSet = types.untyped.Set(); %#ok - C = evalc( 'disp(emptyUntypedSet)' ); - testCase.verifyClass(C, 'char') -end - -function testDisplayScalarObject(testCase) - scalarSet = types.untyped.Set('a', 1); %#ok - C = evalc( 'disp(scalarSet)' ); - testCase.verifyClass(C, 'char') -end - -function testGetSetSize(testCase) - untypedSet = types.untyped.Set( 'a',1, 'b', 2 ); +classdef (SharedTestFixtures = {tests.fixtures.GenerateCoreFixture}) ... + untypedSetTest < matlab.unittest.TestCase - [nRowsA, nColsA] = size(untypedSet); - - nRowsB = size(untypedSet, 1); - nColsB = size(untypedSet, 2); - - testCase.verifyEqual(nRowsA, nRowsB); - testCase.verifyEqual(nColsA, nColsB); -end - -function testHorizontalConcatenation(testCase) - untypedSetA = types.untyped.Set( struct('a',1, 'b', 2) ); - untypedSetB = types.untyped.Set( struct('c',3, 'd', 3) ); - - testCase.verifyError(@() [untypedSetA, untypedSetB], 'NWB:Set:Unsupported') -end - -function testVerticalConcatenation(testCase) - untypedSetA = types.untyped.Set( struct('a',1, 'b', 2) ); - untypedSetB = types.untyped.Set( struct('c',3, 'd', 3) ); - - testCase.verifyError(@() [untypedSetA; untypedSetB], 'NWB:Set:Unsupported') + methods (TestMethodSetup) + function setupMethod(testCase) + % Use a fixture to create a temporary working directory + testCase.applyFixture(matlab.unittest.fixtures.WorkingFolderFixture); + end + end + + methods (Test) + function testCreateSetWithFunctionInput(testCase) + set = types.untyped.Set(@(key, value) true); + testCase.verifyNotEmpty(set.ValidationFcn) + end + + function testCreateSetFromStruct(testCase) + untypedSet = types.untyped.Set( struct('a',1, 'b', 2) ); + testCase.verifyEqual(untypedSet.get('a'), 1) + end + + function testCreateSetFromNvPairs(testCase) + untypedSet = types.untyped.Set( 'a',1, 'b', 2 ); + testCase.verifyEqual(untypedSet.get('a'), 1) + end + + function testCreateSetFromNvPairsPlusFunctionHandle(testCase) + untypedSet = types.untyped.Set( 'a',1, 'b', 2, @(key, value) disp('Hello World')); + testCase.verifyEqual(untypedSet.get('a'), 1) + end + + function testDisplayEmptyObject(testCase) + emptyUntypedSet = types.untyped.Set(); %#ok + C = evalc( 'disp(emptyUntypedSet)' ); + testCase.verifyClass(C, 'char') + end + + function testDisplayScalarObject(testCase) + scalarSet = types.untyped.Set('a', 1); %#ok + C = evalc( 'disp(scalarSet)' ); + testCase.verifyClass(C, 'char') + end + + function testGetSetSize(testCase) + untypedSet = types.untyped.Set( 'a',1, 'b', 2 ); + + [nRowsA, nColsA] = size(untypedSet); + + nRowsB = size(untypedSet, 1); + nColsB = size(untypedSet, 2); + + testCase.verifyEqual(nRowsA, nRowsB); + testCase.verifyEqual(nColsA, nColsB); + end + + function testHorizontalConcatenation(testCase) + untypedSetA = types.untyped.Set( struct('a',1, 'b', 2) ); + untypedSetB = types.untyped.Set( struct('c',3, 'd', 3) ); + + testCase.verifyError(@() [untypedSetA, untypedSetB], 'NWB:Set:Unsupported') + end + + function testVerticalConcatenation(testCase) + untypedSetA = types.untyped.Set( struct('a',1, 'b', 2) ); + untypedSetB = types.untyped.Set( struct('c',3, 'd', 3) ); + + testCase.verifyError(@() [untypedSetA; untypedSetB], 'NWB:Set:Unsupported') + end + + function testSetCharValue(testCase) + untypedSet = types.untyped.Set( struct('a', 'a', 'b', 'b') ); + untypedSet.set('c', 'c'); + testCase.verifyEqual(untypedSet.get('c'), 'c') + end + end end - -function testSetCharValue(testCase) - untypedSet = types.untyped.Set( struct('a', 'a', 'b', 'b') ); - untypedSet.set('c', 'c') - testCase.verifyEqual(untypedSet.get('c'), 'c') -end \ No newline at end of file diff --git a/+tests/+util/getPythonPath.m b/+tests/+util/getPythonPath.m deleted file mode 100644 index 304cdd10..00000000 --- a/+tests/+util/getPythonPath.m +++ /dev/null @@ -1,14 +0,0 @@ -function pythonPath = getPythonPath() - envPath = fullfile('+tests', 'env.mat'); - - if isfile(fullfile(misc.getMatnwbDir, envPath)) - Env = load(envPath, '-mat'); - if isfield(Env, 'pythonPath') - pythonPath = Env.pythonPath; - else - pythonPath = fullfile(Env.pythonDir, 'python'); - end - else - pythonPath = 'python'; - end -end diff --git a/+tests/.coverageignore b/+tests/.coverageignore new file mode 100644 index 00000000..9cfad2e1 --- /dev/null +++ b/+tests/.coverageignore @@ -0,0 +1,12 @@ ++contrib ++tests ++types/+core ++types/+hdmf_common ++types/+hdmf_experimental ++util +external_packages +tools +tutorials ++matnwb/+extension/installAll.m +nwbtest.m +nwbClearGenerated.m diff --git a/+tests/nwbtest.default.env b/+tests/nwbtest.default.env new file mode 100644 index 00000000..83b2db21 --- /dev/null +++ b/+tests/nwbtest.default.env @@ -0,0 +1,19 @@ +# Default settings for the environment variables used in the NWB test suite. +# +# To customize these settings for your local environment, make a copy of this +$ file, update the values as needed (for example, to change the paths for the +# NWBInspector and Python executables), and save the copy as "nwbtest.env" in +# the same directory. +# +# When running tests, the fixture "test.fixtures.UsesEnvironmentVariable" will +# first look for "nwbtest.env". If found, it applies all the specified values +# (unless they are empty). If not, it falls back to this default file +# ("nwbtest.default.env") and only sets variables that are not already defined +# in your system. +# +# Update the values below as necessary for your environment: + +NWBINSPECTOR_EXECUTABLE=nwbinspector +PYTHON_EXECUTABLE=python +NWB_TEST_DEBUG=0 +GITHUB_TOKEN= diff --git a/+tests/+unit/anonSchema/anon.anonymous.yaml b/+tests/test-schema/anonSchema/anon.anonymous.yaml similarity index 100% rename from +tests/+unit/anonSchema/anon.anonymous.yaml rename to +tests/test-schema/anonSchema/anon.anonymous.yaml diff --git a/+tests/+unit/anonSchema/anon.namespace.yaml b/+tests/test-schema/anonSchema/anon.namespace.yaml similarity index 100% rename from +tests/+unit/anonSchema/anon.namespace.yaml rename to +tests/test-schema/anonSchema/anon.namespace.yaml diff --git a/+tests/+unit/boolSchema/bool.bools.yaml b/+tests/test-schema/boolSchema/bool.bools.yaml similarity index 100% rename from +tests/+unit/boolSchema/bool.bools.yaml rename to +tests/test-schema/boolSchema/bool.bools.yaml diff --git a/+tests/+unit/boolSchema/bool.namespace.yaml b/+tests/test-schema/boolSchema/bool.namespace.yaml similarity index 100% rename from +tests/+unit/boolSchema/bool.namespace.yaml rename to +tests/test-schema/boolSchema/bool.namespace.yaml diff --git a/+tests/+unit/compoundSchema/cs.compoundtypes.yaml b/+tests/test-schema/compoundSchema/cs.compoundtypes.yaml similarity index 100% rename from +tests/+unit/compoundSchema/cs.compoundtypes.yaml rename to +tests/test-schema/compoundSchema/cs.compoundtypes.yaml diff --git a/+tests/+unit/compoundSchema/cs.namespace.yaml b/+tests/test-schema/compoundSchema/cs.namespace.yaml similarity index 100% rename from +tests/+unit/compoundSchema/cs.namespace.yaml rename to +tests/test-schema/compoundSchema/cs.namespace.yaml diff --git a/+tests/+unit/multipleConstrainedSchema/mcs.manyset.yaml b/+tests/test-schema/multipleConstrainedSchema/mcs.manyset.yaml similarity index 100% rename from +tests/+unit/multipleConstrainedSchema/mcs.manyset.yaml rename to +tests/test-schema/multipleConstrainedSchema/mcs.manyset.yaml diff --git a/+tests/+unit/multipleConstrainedSchema/mcs.namespace.yaml b/+tests/test-schema/multipleConstrainedSchema/mcs.namespace.yaml similarity index 100% rename from +tests/+unit/multipleConstrainedSchema/mcs.namespace.yaml rename to +tests/test-schema/multipleConstrainedSchema/mcs.namespace.yaml diff --git a/+tests/+unit/multipleShapesSchema/mss.multishapes.yaml b/+tests/test-schema/multipleShapesSchema/mss.multishapes.yaml similarity index 100% rename from +tests/+unit/multipleShapesSchema/mss.multishapes.yaml rename to +tests/test-schema/multipleShapesSchema/mss.multishapes.yaml diff --git a/+tests/+unit/multipleShapesSchema/mss.namespace.yaml b/+tests/test-schema/multipleShapesSchema/mss.namespace.yaml similarity index 100% rename from +tests/+unit/multipleShapesSchema/mss.namespace.yaml rename to +tests/test-schema/multipleShapesSchema/mss.namespace.yaml diff --git a/+tests/+unit/regionReferenceSchema/rrs.namespace.yaml b/+tests/test-schema/regionReferenceSchema/rrs.namespace.yaml similarity index 100% rename from +tests/+unit/regionReferenceSchema/rrs.namespace.yaml rename to +tests/test-schema/regionReferenceSchema/rrs.namespace.yaml diff --git a/+tests/+unit/regionReferenceSchema/rrs.regref.yaml b/+tests/test-schema/regionReferenceSchema/rrs.regref.yaml similarity index 100% rename from +tests/+unit/regionReferenceSchema/rrs.regref.yaml rename to +tests/test-schema/regionReferenceSchema/rrs.regref.yaml diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 0c1fb722..706f45c3 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -40,7 +40,7 @@ jobs: with: command: | pyenv("ExecutionMode", "OutOfProcess"); - results = assertSuccess(nwbtest); + results = assertSuccess(nwbtest('ReportOutputFolder', '.')); assert(~isempty(results), 'No tests ran'); - name: Upload JUnit results if: always() diff --git a/.gitignore b/.gitignore index 0511f62e..4dd70e8c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,9 @@ workspace/ *.asv *.swp .DS_Store -+tests/env.mat + ++tests/env.mat # Deprecated +nwbtest.env # Ignore everything in the +types/ folder +types/* @@ -19,3 +21,4 @@ workspace/ !+types/+util/ docs/build +docs/reports diff --git a/nwbtest.m b/nwbtest.m index c15c4ec0..224818bf 100644 --- a/nwbtest.m +++ b/nwbtest.m @@ -30,53 +30,62 @@ % nwbtest('ProcedureName', 'testSmoke*') % % See also: matlab.unittest.TestSuite.fromPackage + import matlab.unittest.TestSuite; import matlab.unittest.TestRunner; + import matlab.unittest.plugins.XMLPlugin; - import matlab.unittest.plugins.CodeCoveragePlugin; import matlab.unittest.plugins.codecoverage.CoberturaFormat; + try parser = inputParser; parser.KeepUnmatched = true; parser.addParameter('Verbosity', 1); parser.addParameter('Selector', []) + parser.addParameter('Namespace', 'tests') + parser.addParameter('ProduceCodeCoverage', true) + parser.addParameter('ReportOutputFolder', '') + parser.parse(varargin{:}); - ws = pwd; - - nwbClearGenerated(); % Clear default files if any. - cleanupObj = onCleanup(@() generateCore); - cleaner = onCleanup(@generateCore); % Regenerate core when finished + if isempty(parser.Results.ReportOutputFolder) + numReports = 1 + parser.Results.ProduceCodeCoverage; + [reportOutputFolder, folderCleanupObject] = createReportsFolder(numReports); %#ok + else + reportOutputFolder = parser.Results.ReportOutputFolder; + end + % Create test suite pvcell = struct2pvcell(parser.Unmatched); - suite = TestSuite.fromPackage('tests', 'IncludingSubpackages', true, pvcell{:}); + suite = TestSuite.fromPackage(parser.Results.Namespace, ... + 'IncludingSubpackages', true, pvcell{:}); if ~isempty(parser.Results.Selector) suite = suite.selectIf(parser.Results.Selector); end - + suite = suite.sortByFixtures(); + + % Configure test runner runner = TestRunner.withTextOutput('Verbosity', parser.Results.Verbosity); - resultsFile = fullfile(ws, 'testResults.xml'); + resultsFile = fullfile(reportOutputFolder, 'testResults.xml'); runner.addPlugin(XMLPlugin.producingJUnitFormat(resultsFile)); - - coverageFile = fullfile(ws, 'coverage.xml'); - [installDir, ~, ~] = fileparts(mfilename('fullpath')); - - ignoreFolders = {'tutorials', 'tools', '+contrib', '+util', 'external_packages', '+tests'}; - ignorePaths = {... - fullfile('+matnwb', '+extension', 'installAll.m'), ... - [mfilename '.m'], ... - 'nwbClearGenerated.m'}; - mfilePaths = getMfilePaths(installDir, ignoreFolders, ignorePaths); - if ~verLessThan('matlab', '9.3') && ~isempty(mfilePaths) - runner.addPlugin(CodeCoveragePlugin.forFile(mfilePaths,... - 'Producing', CoberturaFormat(coverageFile))); + + if parser.Results.ProduceCodeCoverage + filesForCoverage = getFilesForCoverage(); + if ~verLessThan('matlab', '9.3') && ~isempty(filesForCoverage) + coverageResultFile = fullfile(reportOutputFolder, 'coverage.xml'); + runner.addPlugin(CodeCoveragePlugin.forFile(filesForCoverage,... + 'Producing', CoberturaFormat(coverageResultFile))); + end end % add cobertura coverage - + + % Run tests results = runner.run(suite); - display(results); + if ~nargout + display(results) + end catch e disp(e.getReport('extended')); results = []; @@ -93,17 +102,35 @@ pv(2:2:n) = v; end -function paths = getMfilePaths(folder, excludeFolders, excludePaths) - mfiles = dir(fullfile(folder, '**', '*.m')); - excludeFolders = fullfile(folder, excludeFolders); - excludePaths = fullfile(folder, excludePaths); - paths = {}; - for i = 1:numel(mfiles) - file = mfiles(i); - filePath = fullfile(file.folder, file.name); - if any(startsWith(file.folder, excludeFolders)) || any(strcmp(filePath, excludePaths)) - continue; +function filePaths = getFilesForCoverage() + matnwbDir = misc.getMatnwbDir(); + + coverageIgnoreFile = fullfile(matnwbDir, '+tests', '.coverageignore'); + ignorePatterns = string(splitlines( fileread(coverageIgnoreFile) )); + ignorePatterns(ignorePatterns=="") = []; + + mFileListing = dir(fullfile(matnwbDir, '**', '*.m')); + absoluteFilePaths = fullfile({mFileListing.folder}, {mFileListing.name}); + relativePaths = replace(absoluteFilePaths, [matnwbDir filesep], ''); + + keep = ~startsWith(relativePaths, ignorePatterns); + filePaths = fullfile(matnwbDir, relativePaths(keep)); +end + +function [reportOutputFolder, folderCleanupObject] = createReportsFolder(numReports) + + reportRootFolder = fullfile(misc.getMatnwbDir, 'docs', 'reports'); + timestamp = string(datetime("now", 'Format', 'uuuu_MM_dd_HHmm')); + reportOutputFolder = fullfile(reportRootFolder, timestamp); + if ~isfolder(reportOutputFolder); mkdir(reportOutputFolder); end + + folderCleanupObject = onCleanup(... + @() deleteFolderIfCanceled(reportOutputFolder, numReports)); + + function deleteFolderIfCanceled(folderPath, numReports) + L = dir(fullfile(folderPath, '*.xml')); + if ~isequal(numel(L), numReports) + rmdir(folderPath, 's') end - paths{end+1} = filePath; %#ok end end