Skip to content

Commit 529f330

Browse files
authored
Merge branch 'main' into wip-support-schema-2.10.0
2 parents 3fe6482 + 270e871 commit 529f330

13 files changed

Lines changed: 303 additions & 174 deletions

File tree

+io/createParsedType.m

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,35 +19,56 @@
1919
% Outputs:
2020
% typeInstance - The generated neurodata type instance.
2121

22-
2322
warnState = warning('off', 'NWB:CheckUnset:InvalidProperties');
2423
cleanupObj = onCleanup(@(s) warning(warnState)); % Make sure warning state is reset later
2524

2625
[lastWarningMessage, lastWarningID] = lastwarn('', ''); % Clear last warning
2726

28-
typeInstance = feval(typeName, varargin{:}); % Create the type.
27+
try
28+
typeInstance = feval(typeName, varargin{:}); % Create the type.
29+
catch exception
30+
lastwarn(lastWarningMessage, lastWarningID); % Reset last warning
31+
32+
% Add information about which data type failed, and where in the
33+
% file it is located.
34+
newException = MException('NWB:createParsedType:TypeCreationFailed', ...
35+
'Failed to create object of type "%s" in file location "%s".', ...
36+
typeName, typePath);
37+
38+
% Add full error stack to the exception's cause for easier
39+
% debugging
40+
extendedCause = MException(exception.identifier, ...
41+
getReport(exception, "extended"));
42+
newException = newException.addCause(extendedCause);
43+
if ~isempty(exception.cause)
44+
for i = 1:numel(exception.cause)
45+
newException = newException.addCause(exception.cause{i});
46+
end
47+
end
48+
throw(newException)
49+
end
2950

3051
[warningMessage, warningID] = lastwarn();
3152

3253
% Handle any warnings if they occurred.
3354
if ~isempty(warningMessage)
3455
if strcmp( warningID, 'NWB:CheckUnset:InvalidProperties' )
35-
56+
3657
clear cleanupObj % Reset last warning state
3758

3859
if endsWith(warningMessage, '.')
3960
warningMessage = warningMessage(1:end-1);
4061
end
4162

4263
updatedMessage = sprintf('%s at file location "%s"\n', warningMessage, typePath);
43-
64+
4465
disclaimer = 'NB: The properties in question were dropped while reading the file.';
4566

4667
suggestion = [...
4768
'Consider checking the schema version of the file with '...
4869
'`util.getSchemaVersion(filename)` and comparing with the ' ...
4970
'YAML namespace version present in nwb-schema/core/nwb.namespace.yaml' ];
50-
71+
5172
warning(warningID, '%s\n%s\n\n%s', updatedMessage, disclaimer, suggestion)
5273
else
5374
% Pass, warning has already been displayed

+tests/+system/DynamicTableTest.m

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -148,20 +148,6 @@ function appendRaggedContainer(~, file)
148148
end
149149

150150
methods (Test)
151-
function getNegativeIdTest(testCase)
152-
vector = types.hdmf_common.VectorData('description', 'data column', 'data', (1:10) .');
153-
ids = types.hdmf_common.ElementIdentifiers('data', ((-9):0) .');
154-
testtable = types.hdmf_common.DynamicTable( ...
155-
'description', 'test table with DynamicTableRegion', ...
156-
'colnames', {'vec'}, ...
157-
'vec', vector, ...
158-
'id', ids ...
159-
);
160-
for i = 1:length(vector.data)
161-
testCase.verifyEqual(testtable.getRow(i), testtable.getRow(ids.data(i), 'useId', true));
162-
end
163-
end
164-
165151
function getRowTest(testCase)
166152
Table = testCase.file.intervals_trials;
167153

@@ -282,34 +268,6 @@ function toTableTest(testCase)
282268
);
283269
end
284270
end
285-
286-
function toTableNdArrayTest(testCase)
287-
% toTableNdArrayTest Test to table for a plane segmentation table
288-
289-
% Generate fake image_mask data
290-
imaging_shape = [100, 100];
291-
x = imaging_shape(1);
292-
y = imaging_shape(2);
293-
294-
n_rois = 20;
295-
image_mask = zeros(y, x, n_rois);
296-
for i = 1:n_rois
297-
start = randi(90,2,1);
298-
image_mask(start(1):start(1)+10, start(2):start(2)+10, 1) = 1;
299-
end
300-
301-
% add data to NWB structures
302-
plane_segmentation = types.core.PlaneSegmentation( ...
303-
'colnames', {'image_mask'}, ...
304-
'description', 'output from segmenting my favorite imaging plane', ...
305-
'id', types.hdmf_common.ElementIdentifiers('data', int64(0:19)'), ...
306-
'image_mask', types.hdmf_common.VectorData('data', image_mask, 'description', 'image masks') ...
307-
);
308-
309-
T = plane_segmentation.toTable();
310-
testCase.verifyClass(T, 'table');
311-
testCase.verifySize(T, [n_rois, 2]); % 2 columns, id and image_mask
312-
end
313271

314272
function DynamicTableCheckTest(testCase)
315273
% Verify that the checkConfig utility function
@@ -334,33 +292,5 @@ function DynamicTableCheckTest(testCase)
334292
actualLength = length(Table.id.data);
335293
testCase.verifyEqual(expectedLength, actualLength)
336294
end
337-
338-
function testEmptyTable(testCase)
339-
% validate that empty colnames should work if the number of
340-
% columns agree. (Yes, id is not considered a column so the
341-
% height of the dynamic table is still 3 but with zero
342-
% columns).
343-
types.core.TimeIntervals(...
344-
'description', 'test dynamic table column',...
345-
'id', types.hdmf_common.ElementIdentifiers('data', (0:2)'));
346-
347-
% validate that properties checking works (start_time is a
348-
% property of TimeIntervals)
349-
testCase.verifyError(@()types.core.TimeIntervals(...
350-
'description', 'test error', ...
351-
'start_time', types.hdmf_common.VectorData( ...
352-
'description', 'start time column', ...
353-
'data', (1:5)' ...
354-
)), ...
355-
'NWB:DynamicTable:CheckConfig:ColumnNamesMismatch');
356-
357-
% validate that "vectordata" set checking works.
358-
testCase.verifyError(@()types.core.TimeIntervals(...
359-
'description', 'test error', ...
360-
'randomvalues', types.hdmf_common.VectorData( ...
361-
'description', 'random values', ...
362-
'data', mat2cell(rand(25,2), repmat(5, 5, 1)))), ...
363-
'NWB:DynamicTable:CheckConfig:ColumnNamesMismatch');
364-
end
365295
end
366296
end

+tests/+unit/+io/testCreateParsedType.m

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,22 @@ function testCreateTypeWithInvalidInputs(testCase)
3232

3333
testCase.verifyClass(type, testType)
3434
end
35+
36+
function testCreateTypeWithInvalidPropertyValue(testCase)
37+
testPath = 'some/dataset/path';
38+
testType = 'types.hdmf_common.VectorIndex';
39+
kwargs = {'data', 'text is not numeric indices'};
40+
41+
try
42+
io.createParsedType(testPath, testType, kwargs{:});
43+
testCase.verifyFail('Expected io.createParsedType to throw an error.');
44+
catch exception
45+
testCase.verifyEqual(...
46+
exception.identifier, ...
47+
'NWB:createParsedType:TypeCreationFailed');
48+
testCase.verifyNotEmpty(strfind(exception.message, testType));
49+
testCase.verifyNotEmpty(strfind(exception.message, testPath));
50+
end
51+
end
3552
end
3653
end

+tests/+unit/dynamicTableTest.m

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ function setupClass(testCase)
77
end
88

99
methods (Test)
10+
function testEmptyTable(testCase)
11+
% validate that empty colnames should work if the number of
12+
% columns agree. (Yes, id is not considered a column so the
13+
% height of the dynamic table is still 3 but with zero
14+
% columns).
15+
types.core.TimeIntervals(...
16+
'description', 'test dynamic table column',...
17+
'id', types.hdmf_common.ElementIdentifiers('data', (0:2)'));
18+
19+
% validate that properties checking works (start_time is a
20+
% property of TimeIntervals)
21+
testCase.verifyError(@()types.core.TimeIntervals(...
22+
'description', 'test error', ...
23+
'start_time', types.hdmf_common.VectorData( ...
24+
'description', 'start time column', ...
25+
'data', (1:5)' ...
26+
)), ...
27+
'NWB:DynamicTable:CheckConfig:ColumnNamesMismatch');
28+
29+
% validate that "vectordata" set checking works.
30+
testCase.verifyError(@()types.core.TimeIntervals(...
31+
'description', 'test error', ...
32+
'randomvalues', types.hdmf_common.VectorData( ...
33+
'description', 'random values', ...
34+
'data', mat2cell(rand(25,2), repmat(5, 5, 1)))), ...
35+
'NWB:DynamicTable:CheckConfig:ColumnNamesMismatch');
36+
end
37+
38+
function testGetNegativeId(testCase)
39+
vector = types.hdmf_common.VectorData('description', 'data column', 'data', (1:10) .');
40+
ids = types.hdmf_common.ElementIdentifiers('data', ((-9):0) .');
41+
testtable = types.hdmf_common.DynamicTable( ...
42+
'description', 'test table with DynamicTableRegion', ...
43+
'colnames', {'vec'}, ...
44+
'vec', vector, ...
45+
'id', ids ...
46+
);
47+
for i = 1:length(vector.data)
48+
testCase.verifyEqual(testtable.getRow(i), testtable.getRow(ids.data(i), 'useId', true));
49+
end
50+
end
51+
1052
function testNwbToTableWithReferencedTablesAsRowIndices(testCase)
1153
% The default mode for the toTable() method is to return the row indices
1254
% for dynamic table regions. This test verifies that the data type of
@@ -33,7 +75,35 @@ function testNwbToTableWithReferencedTablesAsTableRows(testCase)
3375
testCase.verifyClass(row1colA, 'table')
3476
testCase.verifyClass(row1colB, 'table')
3577
end
36-
78+
79+
function toTableNdArrayTest(testCase)
80+
% toTableNdArrayTest Test to table for a plane segmentation table
81+
82+
% Generate fake image_mask data
83+
imaging_shape = [100, 100];
84+
x = imaging_shape(1);
85+
y = imaging_shape(2);
86+
87+
n_rois = 20;
88+
image_mask = zeros(y, x, n_rois);
89+
for i = 1:n_rois
90+
start = randi(90,2,1);
91+
image_mask(start(1):start(1)+10, start(2):start(2)+10, 1) = 1;
92+
end
93+
94+
% add data to NWB structures
95+
plane_segmentation = types.core.PlaneSegmentation( ...
96+
'colnames', {'image_mask'}, ...
97+
'description', 'output from segmenting my favorite imaging plane', ...
98+
'id', types.hdmf_common.ElementIdentifiers('data', int64(0:19)'), ...
99+
'image_mask', types.hdmf_common.VectorData('data', image_mask, 'description', 'image masks') ...
100+
);
101+
102+
T = plane_segmentation.toTable();
103+
testCase.verifyClass(T, 'table');
104+
testCase.verifySize(T, [n_rois, 2]); % 2 columns, id and image_mask
105+
end
106+
37107
function testClearDynamicTable(testCase)
38108
dtr_table = testCase.createDynamicTableWithTableRegionReferences();
39109
types.util.dynamictable.clear(dtr_table)
@@ -186,6 +256,35 @@ function testAddMultipleColumnsThrowsErrorIfAnyExists(testCase)
186256
@() dynamicTable.addColumn('newColumn', newColumn, 'columnA', duplicateColumn), ...
187257
'NWB:DynamicTable:AddColumn:ColumnExists');
188258
end
259+
260+
function testAddDataPipeWithWrongHeightToDynamicTable(testCase)
261+
262+
% Create an expandable table where height equals 2.
263+
dynamicTable = types.hdmf_common.DynamicTable( ...
264+
'description', 'test expandable dynamic table', ...
265+
'colnames', {'columnA'}, ...
266+
'columnA', types.hdmf_common.VectorData( ...
267+
'description', '1d expandable column', ...
268+
'data', types.untyped.DataPipe( ...
269+
'data', [1, 2], ...
270+
'maxSize', Inf)), ...
271+
'id', types.hdmf_common.ElementIdentifiers( ...
272+
'data', types.untyped.DataPipe( ...
273+
'data', int64([0, 1]), ...
274+
'maxSize', Inf)));
275+
276+
% Create an expandable column where height equals 3
277+
columnB = types.hdmf_common.VectorData( ...
278+
'description', '2d expandable column vector', ...
279+
'data', types.untyped.DataPipe( ...
280+
'data', 1:3, ...
281+
'maxSize', Inf, ...
282+
'axis', 1));
283+
284+
testCase.verifyError( ...
285+
@() dynamicTable.addColumn('columnB', columnB), ...
286+
'NWB:DynamicTable:AddColumn:MissingRows');
287+
end
189288
end
190289

191290
methods (Static, Access=private)

+tests/+unit/nwbReadTest.m

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,26 @@ function testLoadAll(testCase)
116116
% Todo: Acquisition (But, loadAll is currently not recursive.)
117117
end
118118

119+
function readFileWithInvalidTimeSeriesTimestampsShape(testCase)
120+
fileName = 'invalid_timeseries_timestamps_shape.nwb';
121+
createInvalidTimeSeriesFile(fileName)
122+
123+
try
124+
nwbRead(fileName, "ignorecache");
125+
testCase.assertFail('Expected nwbRead to fail when timestamps are 2-D.')
126+
catch exception
127+
testCase.verifyEqual(exception.identifier, ...
128+
'NWB:createParsedType:TypeCreationFailed')
129+
testCase.verifySubstring(exception.message, ...
130+
'Failed to create object of type "types.core.TimeSeries"')
131+
testCase.verifySubstring(exception.message, ...
132+
'file location "/acquisition/bad_ts"')
133+
testCase.verifyNotEmpty(exception.cause)
134+
testCase.verifySubstring(exception.cause{1}.message, ...
135+
'Invalid shape for property "timestamps".')
136+
end
137+
end
138+
119139
function readWithStringFilenameArg(testCase)
120140
fileName = "testReadWithStringArg.nwb";
121141
nwbExport(tests.factory.NWBFile(), fileName)
@@ -198,3 +218,26 @@ function changeVersionNumberInFile(fileName, newVersionNumber)
198218
end
199219
end
200220
end
221+
222+
function createInvalidTimeSeriesFile(fileName)
223+
224+
nwbExport(tests.factory.NWBFile(), fileName)
225+
226+
fileId = H5F.open(fileName, 'H5F_ACC_RDWR', 'H5P_DEFAULT');
227+
fileCleanup = onCleanup(@() H5F.close(fileId));
228+
229+
% Create an invalid time series in file
230+
groupId = H5G.create(fileId, '/acquisition/bad_ts', 'H5P_DEFAULT', 'H5P_DEFAULT', 'H5P_DEFAULT');
231+
groupCleanup = onCleanup(@() H5G.close(groupId)); %#ok<NASGU>
232+
233+
h5writeatt(fileName, '/acquisition/bad_ts', 'neurodata_type', 'TimeSeries');
234+
h5writeatt(fileName, '/acquisition/bad_ts', 'namespace', 'core');
235+
h5writeatt(fileName, '/acquisition/bad_ts', 'description', 'invalid timestamps shape');
236+
237+
h5create(fileName, '/acquisition/bad_ts/data', [4 1], 'Datatype', 'double');
238+
h5write(fileName, '/acquisition/bad_ts/data', (1:4)');
239+
h5writeatt(fileName, '/acquisition/bad_ts/data', 'unit', 'a.u.');
240+
241+
h5create(fileName, '/acquisition/bad_ts/timestamps', [2 2], 'Datatype', 'double');
242+
h5write(fileName, '/acquisition/bad_ts/timestamps', reshape(1:4, 2, 2));
243+
end

0 commit comments

Comments
 (0)