diff --git a/docs/pages/userdocs/read.dox b/docs/pages/userdocs/read.dox index 363ac471..10f27958 100644 --- a/docs/pages/userdocs/read.dox +++ b/docs/pages/userdocs/read.dox @@ -308,7 +308,7 @@ * - \ref AQNWB::IO::BaseIO "BaseIO", \ref AQNWB::IO::HDF5::HDF5IO "HDF5IO" are responsible for * i) reading type attribute and group information, ii) searching the file for typed objects via * \ref AQNWB::IO::BaseIO::findTypes "findTypes()" methods, and iii) retrieving the paths of all - * object within a group via \ref AQNWB::IO::BaseIO::getGroupObjects "getGroupObjects()" + * object associated with a storage object (e.g., a Group) via \ref AQNWB::IO::BaseIO::getStorageObjects "getStorageObjects()" * * \subsubsection read_design_wrapper_registeredType RegisteredType * @@ -320,7 +320,9 @@ * methods that we can use to instantiate any registered subclass just using the ``io`` object * and ``path`` for the object in the file. \ref AQNWB::NWB::RegisteredType "RegisteredType" can read * the type information from the corresponding `namespace` and `neurodata_type` attributes to - * determine the full type and in run look up the corresponding class in its registry and create the type. + * determine the full type, then look up the corresponding class in its registry, and then create the type. + * Using \ref AQNWB::NWB::RegisteredType::readField "RegisteredType::readField" also provides a + * general mechanism for reading arbitrary fields. * * \subsubsection read_design_wrapper_subtypes Child classes of RegisteredType (e.g., Container) * @@ -540,5 +542,26 @@ * * \snippet tests/examples/test_ecephys_data_read.cpp example_read_only_stringattr_snippet * + * + * \subsubsection read_design_example_read_arbitrary_field Reading arbitrary fields + * + * Even if there is no dedicated `DEFINE_FIELD` definition available, we can still read + * any arbitrary sub-field associated with a particular \ref AQNWB::NWB::RegisteredType "RegisteredType" + * via the generic \ref AQNWB::NWB::RegisteredType::readField "RegisteredType::readField" method. The main + * difference is that for datasets and attributes we need to specify all the additional information + * (e.g., the relative path, object type, and data type) ourselves, whereas using `DEFINE_FIELD` + * this information has already been specified for us. For example, to read the data from + * the \ref AQNWB::NWB::ElectricalSeries "ElectricalSeries" we can call: + * + * \snippet tests/examples/test_ecephys_data_read.cpp example_read_generic_dataset_field_snippet + * + * Similarly, we can also read any sub-fields that are itself \ref AQNWB::NWB::RegisteredType "RegisteredType" + * objects via \ref AQNWB::NWB::RegisteredType::readField "RegisteredType::readField" (e.g., to read custom + * \ref AQNWB::NWB::VectorData "VectorData" columns of a \ref AQNWB::NWB::DynamicTable "DynamicTable"). In + * contrast to dataset and attribute fields, we here only need to specify the relative path of the field. + * \ref AQNWB::NWB::RegisteredType "RegisteredType" in turn can read the type information from the + * `neurodata_type` and `namespace` attributes in the file directly. + * + * \snippet tests/examples/test_ecephys_data_read.cpp example_read_generic_registeredtype_field_snippet */ diff --git a/src/Types.hpp b/src/Types.hpp index 03e7789f..6d45f767 100644 --- a/src/Types.hpp +++ b/src/Types.hpp @@ -35,6 +35,19 @@ class Types Undefined = -1 }; + /** + * \brief Helper struct to check if a value is a data field, i.e., + * Dataset or Attribute + * + * This function is used to enforce constraints on templated functions that + * should only be callable for valid StorageObjectType values + */ + template + struct IsDataStorageObjectType + : std::integral_constant + { + }; + /** * @brief Alias for the size type used in the project. */ diff --git a/src/io/BaseIO.cpp b/src/io/BaseIO.cpp index d2d7f05f..18cbe8f7 100644 --- a/src/io/BaseIO.cpp +++ b/src/io/BaseIO.cpp @@ -77,7 +77,6 @@ std::unordered_map BaseIO::findTypes( { // Check if the current object exists as a dataset or group if (objectExists(current_path)) { - std::cout << "Current Path: " << current_path << std::endl; // Check if we have a typed object if (attributeExists(current_path + "/neurodata_type") && attributeExists(current_path + "/namespace")) @@ -92,8 +91,6 @@ std::unordered_map BaseIO::findTypes( std::string full_type = namespace_attr.data[0] + "::" + neurodata_type_attr.data[0]; - std::cout << "Full name: " << full_type << std::endl; - // Check if the full type matches any of the given types if (types.find(full_type) != types.end()) { found_types[current_path] = full_type; @@ -103,9 +100,14 @@ std::unordered_map BaseIO::findTypes( // object if (search_mode == SearchMode::CONTINUE_ON_TYPE) { // Get the list of objects inside the current group - std::vector objects = getGroupObjects(current_path); + std::vector> objects = + getStorageObjects(current_path, StorageObjectType::Undefined); for (const auto& obj : objects) { - searchTypes(AQNWB::mergePaths(current_path, obj)); + if (obj.second == StorageObjectType::Group + || obj.second == StorageObjectType::Dataset) + { + searchTypes(AQNWB::mergePaths(current_path, obj.first)); + } } } } catch (...) { @@ -117,9 +119,14 @@ std::unordered_map BaseIO::findTypes( else { // Get the list of objects inside the current group - std::vector objects = getGroupObjects(current_path); + std::vector> objects = + getStorageObjects(current_path, StorageObjectType::Undefined); for (const auto& obj : objects) { - searchTypes(AQNWB::mergePaths(current_path, obj)); + if (obj.second == StorageObjectType::Group + || obj.second == StorageObjectType::Dataset) + { + searchTypes(AQNWB::mergePaths(current_path, obj.first)); + } } } } diff --git a/src/io/BaseIO.hpp b/src/io/BaseIO.hpp index c6df0a89..e44d780c 100644 --- a/src/io/BaseIO.hpp +++ b/src/io/BaseIO.hpp @@ -96,12 +96,12 @@ enum class SearchMode /** * @brief Stop searching inside an object once a matching type is found. */ - STOP_ON_TYPE, + STOP_ON_TYPE = 1, /** * @brief Continue searching inside an object even after a matching type is * found. */ - CONTINUE_ON_TYPE + CONTINUE_ON_TYPE = 2, }; /** @@ -223,19 +223,26 @@ class BaseIO virtual bool attributeExists(const std::string& path) const = 0; /** - * @brief Gets the list of objects inside a group. + * @brief Gets the list of storage objects (groups, datasets, attributes) + * inside a group. * - * This function returns a vector of relative paths of all objects inside - * the specified group. If the input path is not a group (e.g., as dataset - * or attribute or invalid object), then an empty list should be - * returned. + * This function returns the relative paths and storage type of all objects + * inside the specified group. If the input path is an attribute then an empty + * list should be returned. If the input path is a dataset, then only the + * attributes will be returned. Note, this function is not recursive, i.e., + * it only looks for storage objects associated directly with the given path. * * @param path The path to the group. + * @param objectType Define which types of storage object to look for, i.e., + * only objects of this specified type will be returned. * - * @return A vector of relative paths of all objects inside the group. + * @return A vector of pairs of relative paths and their corresponding storage + * object types. */ - virtual std::vector getGroupObjects( - const std::string& path) const = 0; + virtual std::vector> + getStorageObjects(const std::string& path, + const StorageObjectType& objectType = + StorageObjectType::Undefined) const = 0; /** * @brief Finds all datasets and groups of the given types in the HDF5 file. diff --git a/src/io/hdf5/HDF5IO.cpp b/src/io/hdf5/HDF5IO.cpp index 2bbd9482..2313b05f 100644 --- a/src/io/hdf5/HDF5IO.cpp +++ b/src/io/hdf5/HDF5IO.cpp @@ -989,16 +989,61 @@ bool HDF5IO::attributeExists(const std::string& path) const return (attributePtr != nullptr); } -std::vector HDF5IO::getGroupObjects(const std::string& path) const +std::vector> +HDF5IO::getStorageObjects(const std::string& path, + const StorageObjectType& objectType) const + { - std::vector objects; - if (getH5ObjectType(path) == H5O_TYPE_GROUP) { + std::vector> objects; + + H5O_type_t h5Type = getH5ObjectType(path); + if (h5Type == H5O_TYPE_GROUP) { H5::Group group = m_file->openGroup(path); hsize_t num_objs = group.getNumObjs(); for (hsize_t i = 0; i < num_objs; ++i) { - objects.push_back(group.getObjnameByIdx(i)); + std::string objName = group.getObjnameByIdx(i); + H5G_obj_t objType = group.getObjTypeByIdx(i); + StorageObjectType storageObjectType; + switch (objType) { + case H5G_GROUP: + storageObjectType = StorageObjectType::Group; + break; + case H5G_DATASET: + storageObjectType = StorageObjectType::Dataset; + break; + default: + storageObjectType = StorageObjectType::Undefined; + } + if (storageObjectType == objectType + || objectType == StorageObjectType::Undefined) + { + objects.emplace_back(objName, storageObjectType); + } + } + + // Include attributes for groups + if (objectType == StorageObjectType::Attribute + || objectType == StorageObjectType::Undefined) + { + SizeType numAttrs = group.getNumAttrs(); + for (SizeType i = 0; i < numAttrs; ++i) { + H5::Attribute attr = group.openAttribute(i); + objects.emplace_back(attr.getName(), StorageObjectType::Attribute); + } + } + } else if (h5Type == H5O_TYPE_DATASET) { + if (objectType == StorageObjectType::Attribute + || objectType == StorageObjectType::Undefined) + { + H5::DataSet dataset = m_file->openDataSet(path); + SizeType numAttrs = dataset.getNumAttrs(); + for (SizeType i = 0; i < numAttrs; ++i) { + H5::Attribute attr = dataset.openAttribute(i); + objects.emplace_back(attr.getName(), StorageObjectType::Attribute); + } } } + return objects; } diff --git a/src/io/hdf5/HDF5IO.hpp b/src/io/hdf5/HDF5IO.hpp index 45c7663a..f0f00cfd 100644 --- a/src/io/hdf5/HDF5IO.hpp +++ b/src/io/hdf5/HDF5IO.hpp @@ -294,19 +294,26 @@ class HDF5IO : public BaseIO bool attributeExists(const std::string& path) const override; /** - * @brief Gets the list of objects inside a group. + * @brief Gets the list of storage objects (groups, datasets, attributes) + * inside a group. * - * This function returns a vector of relative paths of all objects inside - * the specified group. If the input path is not a group (e.g., as dataset - * or attribute or invalid object), then an empty list should be - * returned. + * This function returns the relative paths and storage type of all objects + * inside the specified group. If the input path is an attribute then an empty + * list should be returned. If the input path is a dataset, then only the + * attributes will be returned. Note, this function is not recursive, i.e., + * it only looks for storage objects associated directly with the given path. * * @param path The path to the group. + * @param objectType Define which types of storage object to look for, i.e., + * only objects of this specified type will be returned. * - * @return A vector of relative paths of all objects inside the group. + * @return A vector of pairs of relative paths and their corresponding storage + * object types. */ - std::vector getGroupObjects( - const std::string& path) const override; + virtual std::vector> + getStorageObjects(const std::string& path, + const StorageObjectType& objectType = + StorageObjectType::Undefined) const override; /** * @brief Returns the HDF5 type of object at a given path. diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index dc23df75..87d3ade4 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -66,7 +66,8 @@ Status NWBFile::initialize(const std::string& identifierText, bool NWBFile::isInitialized() const { - auto existingGroupObjects = m_io->getGroupObjects("/"); + std::vector> existingGroupObjects = + m_io->getStorageObjects("/", StorageObjectType::Group); if (existingGroupObjects.size() == 0) { return false; } @@ -85,8 +86,8 @@ bool NWBFile::isInitialized() const // Iterate over the existing objects and add to foundObjects if it's a // required object for (const auto& obj : existingGroupObjects) { - if (requiredObjects.find(obj) != requiredObjects.end()) { - foundObjects.insert(obj); + if (requiredObjects.find(obj.first) != requiredObjects.end()) { + foundObjects.insert(obj.first); } } diff --git a/src/nwb/RegisteredType.hpp b/src/nwb/RegisteredType.hpp index 4f71b61c..2d7b1fd0 100644 --- a/src/nwb/RegisteredType.hpp +++ b/src/nwb/RegisteredType.hpp @@ -198,6 +198,51 @@ class RegisteredType return (getNamespace() + "::" + getTypeName()); } + /** + * @brief Support reading of arbitrary fields by their relative path + * + * This function provided as a general "backup" to support reading of + * arbitrary fields even if the sub-class may not have an explicit + * DEFINE_FIELD specified for the field. If a DEFINE_FIELD exists then + * the corresponding read method should be used as it avoids the need + * for specifying most (if not all) of the function an template + * parameters needed by this function. + * + * @param fieldPath The relative path of the field within the current type, + * i.e., relative to `m_path` + * @tparam SOT The storage object type. This must be a either + * StorageObjectType::Dataset or StorageObjectType::Attribute + * @tparam VTYPE The value type of the field to be read. + * @tparam Enable SFINAE (Substitution Failure Is Not An Error) mechanism + * to enable this function only if SOT is a Dataset or Attribute. + * + * @return ReadDataWrapper object for lazy reading of the field + */ + template::value, + int>::type = 0> + inline std::unique_ptr> readField( + const std::string& fieldPath) const + { + return std::make_unique>( + m_io, AQNWB::mergePaths(m_path, fieldPath)); + } + + /** + * @brief Read a field that is itself a RegisteredType + * + * @param fieldPath The relative path of the field within the current type, + * i.e., relative to `m_path. The field must itself be RegisteredType + * + * @return A unique_ptr to the created instance of the subclass. + */ + inline std::shared_ptr readField( + const std::string& fieldPath) + { + return this->create(AQNWB::mergePaths(m_path, fieldPath), m_io); + } + protected: /** * @brief Register a subclass name and its factory function in the registry. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8615085e..77b8b9ce 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,6 +12,7 @@ include(Catch) # ---- Tests ---- add_executable(aqnwb_test + testBaseIO.cpp testData.cpp testDevice.cpp testEcephys.cpp diff --git a/tests/examples/test_ecephys_data_read.cpp b/tests/examples/test_ecephys_data_read.cpp index 916cc17c..ab791973 100644 --- a/tests/examples/test_ecephys_data_read.cpp +++ b/tests/examples/test_ecephys_data_read.cpp @@ -268,5 +268,29 @@ TEST_CASE("ElectricalSeriesReadExample", "[ecephys]") readElectricalSeries->readDataUnit()->values().data[0]; REQUIRE(esUnitValue == std::string("volts")); // [example_read_only_stringattr_snippet] + + // [example_read_generic_dataset_field_snippet] + // Read the data field via the generic readField method + auto readElectricalSeriesData3 = + readElectricalSeries->readField( + std::string("data")); + // Read the data values as usual + DataBlock readDataValues3 = readElectricalSeriesData3->values(); + REQUIRE(readDataValues3.data.size() == (numSamples * numChannels)); + // [example_read_generic_dataset_field_snippet] + + // [example_read_generic_registeredtype_field_snippet] + // read the NWBFile + auto readNWBFile = + NWB::RegisteredType::create("/", readio); + // read the ElectricalSeries from the NWBFile object via the readField + // method returning a generic std::shared_ptr + auto readRegisteredType = readNWBFile->readField(esdata_path); + // cast the generic pointer to the more specific ElectricalSeries + std::shared_ptr readElectricalSeries2 = + std::dynamic_pointer_cast( + readRegisteredType); + REQUIRE(readElectricalSeries2 != nullptr); + // [example_read_generic_registeredtype_field_snippet] } } diff --git a/tests/testBaseIO.cpp b/tests/testBaseIO.cpp new file mode 100644 index 00000000..01f16b67 --- /dev/null +++ b/tests/testBaseIO.cpp @@ -0,0 +1,122 @@ +#include + +#include "io/hdf5/HDF5IO.hpp" +#include "testUtils.hpp" + +using namespace AQNWB::IO; + +TEST_CASE("Test findTypes functionality", "[BaseIO]") +{ + std::string filename = getTestFilePath("test_findTypes.h5"); + HDF5::HDF5IO io(filename); + io.open(FileMode::Overwrite); + + SECTION("Empty file returns empty result") + { + auto result = + io.findTypes("/", {"core::NWBFile"}, SearchMode::STOP_ON_TYPE); + REQUIRE(result.empty()); + } + + SECTION("Single type at root") + { + // Create root group with type attributes + io.createGroup("/"); + io.createAttribute("core", "/", "namespace"); + io.createAttribute("NWBFile", "/", "neurodata_type"); + + auto result = + io.findTypes("/", {"core::NWBFile"}, SearchMode::STOP_ON_TYPE); + REQUIRE(result.size() == 1); + REQUIRE(result["/"] == "core::NWBFile"); + } + + SECTION("Search for dataset type") + { + // Create root group with type attributes + io.createGroup("/"); + io.createArrayDataSet( + BaseDataType::I32, SizeArray {0}, SizeArray {1}, "/dataset1"); + io.createAttribute("hdmf-common", "/dataset1", "namespace"); + io.createAttribute("VectorData", "/dataset1", "neurodata_type"); + + auto result = io.findTypes( + "/", {"hdmf-common::VectorData"}, SearchMode::STOP_ON_TYPE); + REQUIRE(result.size() == 1); + REQUIRE(result["/dataset1"] == "hdmf-common::VectorData"); + } + + SECTION("Multiple nested types with STOP_ON_TYPE") + { + // Setup hierarchy + io.createGroup("/"); + io.createAttribute("core", "/", "namespace"); + io.createAttribute("NWBFile", "/", "neurodata_type"); + + io.createGroup("/testProcessingModule"); + io.createAttribute("core", "/testProcessingModule", "namespace"); + io.createAttribute( + "ProcessingModule", "/testProcessingModule", "neurodata_type"); + + auto result = io.findTypes("/", + {"core::NWBFile", "core::ProcessingModule"}, + SearchMode::STOP_ON_TYPE); + REQUIRE(result.size() == 1); + REQUIRE(result["/"] == "core::NWBFile"); + } + + SECTION("Multiple nested types with CONTINUE_ON_TYPE") + { + // Setup hierarchy + io.createGroup("/"); + io.createAttribute("core", "/", "namespace"); + io.createAttribute("NWBFile", "/", "neurodata_type"); + + io.createGroup("/testProcessingModule"); + io.createAttribute("core", "/testProcessingModule", "namespace"); + io.createAttribute( + "ProcessingModule", "/testProcessingModule", "neurodata_type"); + + auto result = io.findTypes("/", + {"core::NWBFile", "core::ProcessingModule"}, + SearchMode::CONTINUE_ON_TYPE); + REQUIRE(result.size() == 2); + REQUIRE(result["/"] == "core::NWBFile"); + REQUIRE(result["/testProcessingModule"] == "core::ProcessingModule"); + } + + SECTION("Non-matching types are not included") + { + // Setup hierarchy + io.createGroup("/"); + io.createAttribute("core", "/", "namespace"); + io.createAttribute("NWBFile", "/", "neurodata_type"); + + io.createGroup("/testProcessingModule"); + io.createAttribute("core", "/testProcessingModule", "namespace"); + io.createAttribute( + "ProcessingModule", "/testProcessingModule", "neurodata_type"); + + auto result = + io.findTypes("/", {"core::Device"}, SearchMode::CONTINUE_ON_TYPE); + REQUIRE(result.empty()); + } + + SECTION("Missing attributes are handled gracefully") + { + // Create group with missing neurodata_type attribute + io.createGroup("/"); + io.createAttribute("core", "/", "namespace"); + + io.createGroup("/testProcessingModule"); + io.createAttribute("core", "/testProcessingModule", "namespace"); + io.createAttribute( + "ProcessingModule", "/testProcessingModule", "neurodata_type"); + + auto result = io.findTypes( + "/", {"core::ProcessingModule"}, SearchMode::CONTINUE_ON_TYPE); + REQUIRE(result.size() == 1); + REQUIRE(result["/testProcessingModule"] == "core::ProcessingModule"); + } + io.close(); +} \ No newline at end of file diff --git a/tests/testHDF5IO.cpp b/tests/testHDF5IO.cpp index f01479e3..f50831f9 100644 --- a/tests/testHDF5IO.cpp +++ b/tests/testHDF5IO.cpp @@ -128,21 +128,53 @@ TEST_CASE("createGroup", "[hdf5io]") } } -TEST_CASE("getGroupObjects", "[hdf5io]") +TEST_CASE("getStorageObjects", "[hdf5io]") { // create and open file - std::string filename = getTestFilePath("test_getGroupObjects.h5"); - IO::HDF5::HDF5IO hdf5io(filename); + std::string filename = getTestFilePath("test_getStorageObjects.h5"); + AQNWB::IO::HDF5::HDF5IO hdf5io(filename); hdf5io.open(); SECTION("empty group") { hdf5io.createGroup("/emptyGroup"); - auto groupContent = hdf5io.getGroupObjects("/emptyGroup"); + auto groupContent = hdf5io.getStorageObjects("/emptyGroup"); REQUIRE(groupContent.size() == 0); } - SECTION("group with datasets and subgroups") + SECTION("attribute") + { + int attrData1 = 42; + hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/", "attr1"); + auto attributeContent = hdf5io.getStorageObjects("/attr1"); + REQUIRE(attributeContent.size() == 0); + } + + SECTION("dataset w/o attribute") + { + // Dataset without attributes + hdf5io.createArrayDataSet( + BaseDataType::I32, SizeArray {0}, SizeArray {1}, "/data"); + auto datasetContent = hdf5io.getStorageObjects("/data"); + REQUIRE(datasetContent.size() == 0); + + // Dataset with attribute + int attrData1 = 42; + hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/data", "attr1"); + auto dataContent2 = hdf5io.getStorageObjects("/data"); + REQUIRE(dataContent2.size() == 1); + REQUIRE( + dataContent2[0] + == std::make_pair(std::string("attr1"), StorageObjectType::Attribute)); + } + + SECTION("invalid path") + { + auto invalidPathContent = hdf5io.getStorageObjects("/invalid/path"); + REQUIRE(invalidPathContent.size() == 0); + } + + SECTION("group with datasets, subgroups, and attributes") { hdf5io.createGroup("/data"); hdf5io.createGroup("/data/subgroup1"); @@ -152,15 +184,43 @@ TEST_CASE("getGroupObjects", "[hdf5io]") hdf5io.createArrayDataSet( BaseDataType::I32, SizeArray {0}, SizeArray {1}, "/data/dataset2"); - auto groupContent = hdf5io.getGroupObjects("/data"); - REQUIRE(groupContent.size() == 4); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "subgroup1") + // Add attributes to the group + int attrData1 = 42; + hdf5io.createAttribute(BaseDataType::I32, &attrData1, "/data", "attr1"); + int attrData2 = 43; + hdf5io.createAttribute(BaseDataType::I32, &attrData2, "/data", "attr2"); + + auto groupContent = hdf5io.getStorageObjects("/data"); + REQUIRE(groupContent.size() == 6); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("subgroup1"), + StorageObjectType::Group)) != groupContent.end()); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "subgroup2") + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("subgroup2"), + StorageObjectType::Group)) != groupContent.end()); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "dataset1") + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("dataset1"), + StorageObjectType::Dataset)) != groupContent.end()); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "dataset2") + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("dataset2"), + StorageObjectType::Dataset)) + != groupContent.end()); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("attr1"), + StorageObjectType::Attribute)) + != groupContent.end()); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("attr2"), + StorageObjectType::Attribute)) != groupContent.end()); } @@ -169,17 +229,66 @@ TEST_CASE("getGroupObjects", "[hdf5io]") hdf5io.createGroup("/rootGroup1"); hdf5io.createGroup("/rootGroup2"); - auto groupContent = hdf5io.getGroupObjects("/"); + auto groupContent = hdf5io.getStorageObjects("/"); REQUIRE(groupContent.size() == 2); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "rootGroup1") + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("rootGroup1"), + StorageObjectType::Group)) + != groupContent.end()); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("rootGroup2"), + StorageObjectType::Group)) + != groupContent.end()); + } + + SECTION("filter by object type") + { + hdf5io.createGroup("/filterGroup"); + hdf5io.createGroup("/filterGroup/subgroup1"); + hdf5io.createArrayDataSet(BaseDataType::I32, + SizeArray {0}, + SizeArray {1}, + "/filterGroup/dataset1"); + + // Add attributes to the group + int attrData = 44; + hdf5io.createAttribute( + BaseDataType::I32, &attrData, "/filterGroup", "attr1"); + + auto groupContent = + hdf5io.getStorageObjects("/filterGroup", StorageObjectType::Group); + REQUIRE(groupContent.size() == 1); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("subgroup1"), + StorageObjectType::Group)) != groupContent.end()); - REQUIRE(std::find(groupContent.begin(), groupContent.end(), "rootGroup2") + + groupContent = + hdf5io.getStorageObjects("/filterGroup", StorageObjectType::Dataset); + REQUIRE(groupContent.size() == 1); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("dataset1"), + StorageObjectType::Dataset)) + != groupContent.end()); + + groupContent = + hdf5io.getStorageObjects("/filterGroup", StorageObjectType::Attribute); + REQUIRE(groupContent.size() == 1); + REQUIRE(std::find(groupContent.begin(), + groupContent.end(), + std::make_pair(std::string("attr1"), + StorageObjectType::Attribute)) != groupContent.end()); } // close file hdf5io.close(); } +// END TEST_CASE("getStorageObjects", "[hdf5io]") TEST_CASE("HDF5IO; write datasets", "[hdf5io]") { @@ -1437,8 +1546,29 @@ TEST_CASE("HDF5IO; read dataset", "[hdf5io]") SECTION("read unsupported data type") { - // TODO Add a test that tries to read some unsupported data type - // such as a strange compound data type + // open file + std::string path = getTestFilePath("test_ReadUnsupportedDataType.h5"); + std::shared_ptr hdf5io = + std::make_shared(path); + hdf5io->open(); + + // Create a compound datatype + H5::CompType compoundType(sizeof(double) * 2); + compoundType.insertMember("real", 0, H5::PredType::NATIVE_DOUBLE); + compoundType.insertMember( + "imag", sizeof(double), H5::PredType::NATIVE_DOUBLE); + + // Create dataset with compound type directly using HDF5 C++ API + H5::H5File file(path, H5F_ACC_RDWR); + hsize_t dims[1] = {5}; + H5::DataSpace dataspace(1, dims); + H5::DataSet dataset = + file.createDataSet("ComplexData", compoundType, dataspace); + + // Attempt to read the dataset - should throw an exception + REQUIRE_THROWS_AS(hdf5io->readDataset("/ComplexData"), std::runtime_error); + + hdf5io->close(); } } diff --git a/tests/testRegisteredType.cpp b/tests/testRegisteredType.cpp index 5ece9a29..a5f9efcd 100644 --- a/tests/testRegisteredType.cpp +++ b/tests/testRegisteredType.cpp @@ -10,6 +10,40 @@ using namespace AQNWB::NWB; +// Test class with custom type name +class CustomNameType : public RegisteredType +{ +public: + CustomNameType(const std::string& path, std::shared_ptr io) + : RegisteredType(path, io) + { + } + + virtual std::string getTypeName() const override { return "CustomType"; } + virtual std::string getNamespace() const override { return "test"; } +}; + +// Test class with field definitions +class TestFieldType : public RegisteredType +{ +public: + TestFieldType(const std::string& path, std::shared_ptr io) + : RegisteredType(path, io) + { + } + + virtual std::string getTypeName() const override { return "TestFieldType"; } + virtual std::string getNamespace() const override { return "test"; } + + DEFINE_FIELD(testAttribute, + AttributeField, + int32_t, + "test_attr", + "Test attribute field") + DEFINE_FIELD( + testDataset, DatasetField, float, "test_dataset", "Test dataset field") +}; + TEST_CASE("RegisterType", "[base]") { SECTION("test that the registry is working") @@ -27,23 +61,14 @@ TEST_CASE("RegisterType", "[base]") auto registry = RegisteredType::getRegistry(); auto factoryMap = RegisteredType::getFactoryMap(); // TODO we are checking for at least 10 registered types because that is how - // many - // were defined at the time of implementation of this test. We know we - // will add more, but we would like to avoid having to update this test - // every time, so we are only checking for at least 10 + // many were defined at the time of implementation of this test. We know we + // will add more, but we would like to avoid having to update this test + // every time, so we are only checking for at least 10 REQUIRE(registry.size() >= 10); REQUIRE(factoryMap.size() >= 10); REQUIRE(registry.size() == factoryMap.size()); // Test that we can indeed instantiate all registered types - // This also ensures that each factory function works correctly, - // and hence, that all subtypes implement the expected constructor - // for the RegisteredType::create method. This is similar to - // checking: - // for (const auto& pair : factoryMap) { - // auto instance = pair.second(examplePath, io); - // REQUIRE(instance != nullptr); - // } std::cout << "Registered Types:" << std::endl; for (const auto& entry : factoryMap) { const std::string& subclassFullName = entry.first; @@ -73,6 +98,9 @@ TEST_CASE("RegisterType", "[base]") // Check that the examplePath is set as expected REQUIRE(instance->getPath() == examplePath); + + // Test getFullName + REQUIRE(instance->getFullName() == (typeNamespace + "::" + typeName)); } } @@ -135,5 +163,108 @@ TEST_CASE("RegisterType", "[base]") auto readTS = AQNWB::NWB::RegisteredType::create(examplePath, io); std::string readTSType = readContainer->getTypeName(); REQUIRE(readTSType == "TimeSeries"); + + // Attempt to read the TimeSeries using the generic readField method + // By providing an empty path we tell readField to read itself + std::shared_ptr readRegisteredType = + readContainer->readField(std::string("")); + REQUIRE(readRegisteredType != nullptr); + std::shared_ptr readRegisteredTypeAsTimeSeries = + std::dynamic_pointer_cast(readRegisteredType); + REQUIRE(readRegisteredTypeAsTimeSeries != nullptr); + } + + SECTION("test error handling for invalid type creation") + { + std::string filename = getTestFilePath("testInvalidType.h5"); + std::shared_ptr io = std::make_unique(filename); + std::string examplePath("/example/path"); + + // Test creating with non-existent type name + auto invalidInstance = + RegisteredType::create("invalid::Type", examplePath, io); + REQUIRE(invalidInstance == nullptr); + + // Test creating with empty type name + auto emptyInstance = RegisteredType::create("", examplePath, io); + REQUIRE(emptyInstance == nullptr); + + // Test creating with malformed type name (missing namespace) + auto malformedInstance = + RegisteredType::create("NoNamespace", examplePath, io); + REQUIRE(malformedInstance == nullptr); + } + + SECTION("test custom type name") + { + std::string filename = getTestFilePath("testCustomType.h5"); + std::shared_ptr io = std::make_unique(filename); + std::string examplePath("/example/path"); + + // Create instance of custom named type + auto customInstance = std::make_shared(examplePath, io); + REQUIRE(customInstance != nullptr); + REQUIRE(customInstance->getTypeName() == "CustomType"); + REQUIRE(customInstance->getNamespace() == "test"); + REQUIRE(customInstance->getFullName() == "test::CustomType"); + } + + SECTION("test field definitions") + { + std::string filename = getTestFilePath("testFields.h5"); + std::shared_ptr io = std::make_unique(filename); + io->open(); + std::string examplePath("/test_fields"); + + // Create test instance + auto testInstance = std::make_shared(examplePath, io); + REQUIRE(testInstance != nullptr); + + // Create parent group + io->createGroup(examplePath); + + // Create test data + const int32_t attrValue = 42; + const std::vector datasetValues = {1.0f, 2.0f, 3.0f}; + + // Write test data + io->createAttribute( + BaseDataType::I32, &attrValue, examplePath, "test_attr"); + auto datasetRecordingData = + io->createArrayDataSet(BaseDataType::F32, + SizeArray {3}, + SizeArray {3}, + examplePath + "/test_dataset"); + datasetRecordingData->writeDataBlock( + SizeArray {3}, SizeArray {0}, BaseDataType::F32, datasetValues.data()); + + // Test attribute field + auto attrWrapper = testInstance->testAttribute(); + REQUIRE(attrWrapper != nullptr); + auto attrData = attrWrapper->values(); + REQUIRE(attrData.data[0] == attrValue); + + // Test dataset field + auto datasetWrapper = testInstance->testDataset(); + REQUIRE(datasetWrapper != nullptr); + auto datasetData = datasetWrapper->values(); + REQUIRE(datasetData.data == datasetValues); + + // Test reading using the general readField method + // Read test_attr via readField + auto attrWrapper2 = testInstance->readField( + std::string("test_attr")); + REQUIRE(attrWrapper2 != nullptr); + auto attrData2 = attrWrapper2->values(); + REQUIRE(attrData2.data[0] == attrValue); + + // Read test_dataset via readField + auto datasetWrapper2 = testInstance->readField( + std::string("test_dataset")); + REQUIRE(datasetWrapper2 != nullptr); + auto datasetData2 = datasetWrapper2->values(); + REQUIRE(datasetData2.data == datasetValues); + + io->close(); } }