Skip to content

Commit 7d4e43e

Browse files
authored
MATLAB version matrix (#685)
1 parent f89f9ec commit 7d4e43e

36 files changed

+425
-136
lines changed

+io/+internal/+h5/mustBeH5File.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
function mustBeH5File(value)
22
arguments
3-
value {mustBeFile}
3+
value {matnwb.common.compatibility.mustBeFile}
44
end
55

66
VALID_FILE_ENDING = ["h5", "nwb"];

+io/+internal/+h5/mustBeH5FileReference.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
function mustBeH5FileReference(value)
22
arguments
3-
value {mustBeA(value, ["char", "string", "H5ML.id"])}
3+
value {matnwb.common.compatibility.mustBeA(value, ["char", "string", "H5ML.id"])}
44
end
55

66
if isa(value, "char") || isa(value, "string")

+io/+internal/+h5/openGroup.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
% openGroup Opens an HDF5 group at given location and ensures cleanup.
33

44
arguments
5-
fileId {mustBeA(fileId, "H5ML.id")}
5+
fileId {matnwb.common.compatibility.mustBeA(fileId, "H5ML.id")}
66
h5Location (1,1) string
77
end
88

+io/+internal/+h5/openObject.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
% openObject Opens an HDF5 object at given location and ensures cleanup.
33

44
arguments
5-
fileId {mustBeA(fileId, "H5ML.id")}
5+
fileId {matnwb.common.compatibility.mustBeA(fileId, "H5ML.id")}
66
objectLocation (1,1) string
77
end
88

+io/parseAttributes.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,24 @@
3939
attributes(deleteMask) = [];
4040
for i=1:length(attributes)
4141
attr = attributes(i);
42+
4243
switch attr.Datatype.Class
44+
case 'H5T_STRING'
45+
% H5 String type attributes are loaded differently in releases
46+
% prior to MATLAB R2020a. For details, see:
47+
% https://se.mathworks.com/help/matlab/ref/h5readatt.html
48+
attributeValue = attr.Value;
49+
if verLessThan('matlab', '9.8') % MATLAB < R2020a
50+
if iscell(attr.Value)
51+
if isempty(attr.Value)
52+
attributeValue = '';
53+
elseif isscalar(attr.Value)
54+
attributeValue = attr.Value{1};
55+
else
56+
attributeValue = attr.Value;
57+
end
58+
end
59+
end
4360
case 'H5T_REFERENCE'
4461
fid = H5F.open(filename, 'H5F_ACC_RDONLY', 'H5P_DEFAULT');
4562
aid = H5A.open_by_name(fid, context, attr.Name);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
function result = height(value)
2+
% height - Number of rows in an array.
3+
%
4+
% From R2020b: height returns the number of rows of an array
5+
6+
if verLessThan('matlab', '9.9') && (isnumeric(value) || iscell(value)) %#ok<VERLESSMATLAB> - MATLAB < R2020b
7+
valueSize = size(value);
8+
result = valueSize(1);
9+
else
10+
result = height(value);
11+
end
12+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function mustBeA(value, classNames)
2+
% mustBeA - Check if value is one of specified classes
3+
%
4+
% mustBeA was introduced in R2020b.
5+
6+
arguments
7+
value
8+
classNames (1,:) string
9+
end
10+
11+
if verLessThan('matlab', '9.9') %#ok<VERLESSMATLAB>
12+
% Custom implementation (MATLAB < R2020b)
13+
try
14+
mustBeMember(class(value), classNames)
15+
catch ME
16+
ME = MException(...
17+
'MATLAB:validators:mustBeA', ...
18+
'Value must be one of the following types: %s.', strjoin(classNames, ', '));
19+
throwAsCaller(ME)
20+
end
21+
else % Use available builtin
22+
try
23+
mustBeA(value, classNames)
24+
catch ME
25+
throwAsCaller(ME)
26+
end
27+
end
28+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
function mustBeFile(filePath)
2+
% mustBeFile - Check if value is path name of existing file.
3+
%
4+
% mustBeFile was introduced in R2020b. In order to support older releases
5+
% of MATLAB, this function implements mustBeFile also for older releases.
6+
%
7+
% Note: Currently only works for scalar strings
8+
9+
arguments
10+
filePath (1,1) string
11+
end
12+
13+
if verLessThan('matlab', '9.9') %#ok<VERLESSMATLAB>
14+
% Custom implementation (MATLAB < R2020b)
15+
try
16+
matnwb.common.compatibility.mustBeNonzeroLengthText(filePath)
17+
catch ME
18+
throwAsCaller(ME)
19+
end
20+
isValid = isfile(filePath);
21+
22+
if ~isValid
23+
ME = MException(...
24+
'MATLAB:validators:mustBeFile', ...
25+
'The following file does not exist: ''%s''.', filePath);
26+
throwAsCaller(ME)
27+
end
28+
else % Use available builtin
29+
try
30+
mustBeFile(filePath)
31+
catch ME
32+
throwAsCaller(ME)
33+
end
34+
end
35+
end
36+
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function mustBeFolder(folderPath)
2+
% mustBeFolder - Check if value is path name of existing folder.
3+
%
4+
% mustBeFolder was introduced in R2020b. In order to support older releases
5+
% of MATLAB, this function implements mustBeFolder also for older releases.
6+
%
7+
% Note: Currently only works for scalar strings
8+
9+
arguments
10+
folderPath (1,1) string
11+
end
12+
13+
if verLessThan('matlab', '9.9') %#ok<VERLESSMATLAB>
14+
% Custom implementation (MATLAB < R2020b)
15+
isValid = isfolder(folderPath);
16+
17+
if ~isValid
18+
ME = MException(...
19+
'MATLAB:validators:mustBeFolder', ...
20+
'The following folder does not exist: ''%s''.', folderPath);
21+
throwAsCaller(ME)
22+
end
23+
else % Use available builtin
24+
try
25+
mustBeFolder(folderPath)
26+
catch ME
27+
throwAsCaller(ME)
28+
end
29+
end
30+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
function mustBeNonzeroLengthText(text)
2+
% mustBeNonzeroLengthText - Check that text has 1 or more characters.
3+
%
4+
% mustBeNonzeroLengthText was introduced in R2020b. In order to support
5+
% older releases of MATLAB, this function implements
6+
% mustBeNonzeroLengthText also for older releases.
7+
8+
if verLessThan('matlab', '9.9') %#ok<VERLESSMATLAB>
9+
% Custom implementation (MATLAB < R2020b)
10+
isValid = ischar(text) || isstring(text) || ...
11+
(iscell(text) && all(cellfun(@(c) isa(c, 'char') || isa(c, 'string'), text)));
12+
if isValid
13+
if ischar(text)
14+
isValid = ~isempty(text);
15+
elseif isstring(text)
16+
isValid = ~isempty(char(text));
17+
elseif iscell(text)
18+
isValid = ~isempty(text) && ~all( cellfun(@(c) isempty(c), text) );
19+
end
20+
end
21+
if ~isValid
22+
ME = MException(...
23+
'MATLAB:validators:mustBeNonzeroLengthText', ...
24+
'Value must be text with one or more characters.');
25+
throwAsCaller(ME)
26+
end
27+
else % Use available builtin
28+
try
29+
mustBeNonzeroLengthText(text)
30+
catch ME
31+
throwAsCaller(ME)
32+
end
33+
end
34+
end
35+
36+

+matnwb/+common/mustBeNwbFile.m

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
function mustBeNwbFile(filePath)
22
% mustBeNwbFile - Check that file path points to existing file with .nwb extension
33
arguments
4-
filePath (1,1) string {mustBeFile}
4+
filePath (1,1) string {matnwb.common.compatibility.mustBeFile}
55
end
66
assert(endsWith(filePath, ".nwb", "IgnoreCase", true))
7-
end
7+
end

+schemes/loadNamespace.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
arguments
1313
namespaceName (1,1) string
14-
generatedTypesDirectory (1,1) string {mustBeFolder} = ...
14+
generatedTypesDirectory (1,1) string {matnwb.common.compatibility.mustBeFolder} = ...
1515
schemes.utility.findRootDirectoryForGeneratedTypes()
1616
end
1717

+tests/+abstract/NwbTestCase.m

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,7 @@ function clearExtension(testCase, extensionName)
3333

3434
methods (Static, Access = protected)
3535
function [nwbFile, nwbFileCleanup] = readNwbFileWithPynwb(nwbFilename)
36-
try
37-
io = py.pynwb.NWBHDF5IO(nwbFilename);
38-
nwbFile = io.read();
39-
nwbFileCleanup = onCleanup(@(x) closePyNwbObject(io));
40-
catch ME
41-
error(ME.message)
42-
end
43-
44-
function closePyNwbObject(io)
45-
io.close()
46-
end
36+
[nwbFile, nwbFileCleanup] = tests.util.readWithPynwb(nwbFilename);
4737
end
4838

4939
function nwbFilename = getRandomFilename()

+tests/+fixtures/NwbClearGeneratedFixture.m

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@
1010
% See also matlab.unittest.fixtures.Fixture generateCore nwbClearGenerated
1111

1212
properties
13-
TypesOutputFolder (1,1) string {mustBeFolder} = misc.getMatnwbDir
13+
TypesOutputFolder (1,1) string ...
14+
{matnwb.common.compatibility.mustBeFolder} = misc.getMatnwbDir()
1415
end
1516

1617
methods
1718
function fixture = NwbClearGeneratedFixture(outputFolder)
1819
arguments
19-
outputFolder (1,1) string {mustBeFolder} = misc.getMatnwbDir
20+
outputFolder (1,1) string ...
21+
{matnwb.common.compatibility.mustBeFolder} = misc.getMatnwbDir()
2022
end
2123
fixture.TypesOutputFolder = outputFolder;
2224
end

+tests/+fixtures/SetEnvironmentVariableFixture.m

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ function setup(fixture) %#ok<MANU>
8181
envMap(key) = value;
8282
end
8383
end
84+
85+
function tf = isenv(varName)
86+
tf = ~isempty( getenv(varName) );
87+
end

+tests/+system/+tutorial/PynwbTutorialTest.m

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ function setupClass(testCase)
7171

7272
% Add site-packages to python path
7373
testCase.PythonEnvironment = getenv('PYTHONPATH');
74-
L = dir('temp_venv/lib/python*/site-*'); % Find the site-packages folder
74+
if isunix
75+
L = dir('temp_venv/lib/python*/site-*'); % Find the site-packages folder
76+
elseif ispc
77+
L = dir('temp_venv/Lib/site-*'); % Find the site-packages folder
78+
end
7579
pythonPath = fullfile(L.folder, L.name);
7680
setenv('PYTHONPATH', pythonPath)
7781

@@ -195,17 +199,20 @@ function installPythonDependencies(testCase)
195199

196200
% Note: Without a token, github api requests are limited to 60 per
197201
% hour. The listFilesInRepo will make 4 requests per call
198-
if isenv('GITHUB_TOKEN')
199-
token = getenv('GITHUB_TOKEN');
200-
else
201-
token = '';
202-
end
202+
token = getenv('GITHUB_TOKEN'); % If not present, defaults to empty char
203203

204204
allFilePaths = listFilesInRepo(...
205205
'NeurodataWithoutBorders', 'pynwb', 'docs/gallery/', token);
206-
206+
207207
% Exclude files that are not .py files.
208-
[~, fileNames, fileExt] = fileparts(allFilePaths);
208+
try
209+
[~, fileNames, fileExt] = fileparts(allFilePaths);
210+
catch % Support MATLAB R2019b:
211+
[fileNames, fileExt] = deal(cell(size(allFilePaths)));
212+
for i = 1:numel(allFilePaths)
213+
[~, fileNames{i}, fileExt{i}] = fileparts(allFilePaths{i});
214+
end
215+
end
209216
keep = strcmp(fileExt, '.py');
210217
allFilePaths = allFilePaths(keep);
211218

+tests/+system/+tutorial/TutorialTest.m

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,14 @@ function testTutorial(testCase, tutorialFile) %#ok<INUSD>
7171
% code which overloads display methods for nwb types/objects.
7272
C = evalc( 'run(tutorialFile)' ); %#ok<NASGU>
7373

74-
testCase.readTutorialNwbFileWithPynwb()
75-
testCase.inspectTutorialFileWithNwbInspector()
74+
skipChecks = getenv("SKIP_PYNWB_COMPATIBILITY_TEST_FOR_TUTORIALS");
75+
skipChecks = ~isempty(skipChecks) && logical(str2double(skipChecks));
76+
if skipChecks
77+
% pass
78+
else
79+
testCase.readTutorialNwbFileWithPynwb()
80+
testCase.inspectTutorialFileWithNwbInspector()
81+
end
7682
end
7783
end
7884

@@ -82,10 +88,8 @@ function readTutorialNwbFileWithPynwb(testCase)
8288
nwbFileNameList = testCase.listNwbFiles();
8389
for nwbFilename = nwbFileNameList
8490
try
85-
io = py.pynwb.NWBHDF5IO(nwbFilename);
86-
nwbObject = io.read();
91+
[nwbObject, nwbFileCleanup] = tests.util.readWithPynwb(nwbFilename); %#ok<ASGLU>
8792
testCase.verifyNotEmpty(nwbObject, 'The NWB file should not be empty.');
88-
io.close()
8993
catch ME
9094
error(ME.message)
9195
end
@@ -146,7 +150,7 @@ function inspectTutorialFileWithNwbInspector(testCase)
146150
"check_image_series_external_file_valid", ...
147151
"check_regular_timestamps"
148152
];
149-
153+
[resultsIn(:).ignore] = deal(false);
150154
for i = 1:numel(resultsIn)
151155
resultsIn(i).ignore = any(strcmp(CHECK_IGNORE, resultsIn(i).check_function_name));
152156

+tests/+unit/+io/+internal/+h5/MustBeH5FileTest.m

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ function createTestFiles(testCase)
2323
testCase.createH5File(h5FileName)
2424
testCase.createNwbFile(nwbFileName)
2525

26-
% Create file which is not h5
27-
s = system( sprintf("touch %s", testCase.InvalidFileName{1}) );
28-
assert(s==0)
26+
% Create file which is not h5
27+
fid = fopen(testCase.InvalidFileName{1}, 'w');
28+
fwrite(fid, 0);
29+
fclose(fid);
2930
end
3031
end
3132

+tests/+unit/+io/WriteTest.m

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,17 @@ function testWriteBooleanAttribute(testCase)
2020

2121
% Define target dataset path and create it in the HDF5 file
2222
io.writeAttribute(fid, '/test', true); % First write to create the dataset
23-
23+
2424
% Read using h5readatt and confirm value
2525
value = h5readatt(filename, '/', 'test');
26-
testCase.verifyTrue( strcmp(value, 'TRUE'))
26+
if ~isempty(value)
27+
testCase.verifyTrue( strcmp(value, 'TRUE'))
28+
else
29+
% Pass this test. h5readatt does not properly read enum
30+
% values in Releases <= R2022a. Also, in MatNWB attributes
31+
% are parsed using io.parseAttributes, so this verification is
32+
% not critical.
33+
end
2734

2835
% Read using io.parseAttributes and confirm value
2936
blackList = struct(...

+tests/+unit/+schema/RequiredPropsTest.m

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ function setupNwbFile(testCase)
2323
testCase.nwbFileObj.acquisition.set('Test', testCase.testGroup);
2424

2525
% Create a filename for each test
26-
testCase.nwbFileName = matlab.lang.internal.uuid() + ".nwb";
26+
try
27+
testCase.nwbFileName = matlab.lang.internal.uuid() + ".nwb";
28+
catch
29+
randUuid = string(java.util.UUID.randomUUID().toString());
30+
testCase.nwbFileName = randUuid + ".nwb";
31+
end
2732
end
2833
end
2934

0 commit comments

Comments
 (0)