From 6b1dbc6672fb13d1dc2e0e30ae3e443aac1a8f77 Mon Sep 17 00:00:00 2001 From: Oliver Ruebel <oruebel@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:48:12 -0800 Subject: [PATCH 1/5] Add codecov badge to README.md (#135) * Add codecov badge to README.md * exclude tests from coverage trace * add outputs for debugging * update coverage workflow * debug coverage dir * debug coverage dir * modify exclusion criteria * revert coverage trace command --------- Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com> --- README.md | 3 ++- cmake/coverage.cmake | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 249d6d96..cd231d3f 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/codespell.yml) [](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/lint.yml) [](https://github.com/NeurodataWithoutBorders/aqnwb/actions/workflows/doxygen-gh-pages.yml) +[](https://app.codecov.io/github/NeurodataWithoutBorders/aqnwb?branch=main) [](https://neurodatawithoutborders.github.io/aqnwb/) [](https://nwb-overview.readthedocs.io/en/latest/nwb-project-analytics/docs/source/code_stat_pages/AqNWB_stats.html) @@ -30,4 +31,4 @@ See the [AqNWB Documentation](https://neurodatawithoutborders.github.io/aqnwb/) For more information about the license, contributing guidelines, code of conduct and other relevant documentation for developers please see the -[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html). \ No newline at end of file +[Developer Documentation](https://neurodatawithoutborders.github.io/aqnwb/devdocs.html). diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake index 6c4b03e2..868e5dc7 100644 --- a/cmake/coverage.cmake +++ b/cmake/coverage.cmake @@ -4,7 +4,7 @@ # customization issues set( COVERAGE_TRACE_COMMAND - lcov -c -q + lcov -c --verbose -o "${PROJECT_BINARY_DIR}/coverage.info" -d "${PROJECT_BINARY_DIR}" --include "${PROJECT_SOURCE_DIR}/src/*" From d76f8b7b384ee6dcd2fb775ac9c4d0bd709f0d4b Mon Sep 17 00:00:00 2001 From: Steph Prince <40640337+stephprince@users.noreply.github.com> Date: Tue, 21 Jan 2025 18:26:26 -0500 Subject: [PATCH 2/5] Add AnnotationSeries data type (#141) * add initial AnnotationSeries implementation * add convenience functions, remove dtype option * add draft of tests * update writeDataBlock error message * update AnnotationSeries methods for vlen strings * add tests * fix formatting * Update src/nwb/misc/AnnotationSeries.hpp Co-authored-by: Oliver Ruebel <oruebel@users.noreply.github.com> * update read example in tests --------- Co-authored-by: Oliver Ruebel <oruebel@users.noreply.github.com> --- CMakeLists.txt | 1 + src/io/hdf5/HDF5RecordingData.cpp | 3 +- src/nwb/NWBFile.cpp | 30 +++++++++++++ src/nwb/NWBFile.hpp | 14 ++++++ src/nwb/RecordingContainers.cpp | 17 +++++++ src/nwb/RecordingContainers.hpp | 16 +++++++ src/nwb/misc/AnnotationSeries.cpp | 71 ++++++++++++++++++++++++++++++ src/nwb/misc/AnnotationSeries.hpp | 71 ++++++++++++++++++++++++++++++ tests/CMakeLists.txt | 1 + tests/testMisc.cpp | 73 +++++++++++++++++++++++++++++++ tests/testNWBFile.cpp | 55 +++++++++++++++++++++++ 11 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 src/nwb/misc/AnnotationSeries.cpp create mode 100644 src/nwb/misc/AnnotationSeries.hpp create mode 100644 tests/testMisc.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 8fbe6787..f2287f36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ add_library( src/nwb/ecephys/SpikeEventSeries.cpp src/nwb/file/ElectrodeGroup.cpp src/nwb/file/ElectrodeTable.cpp + src/nwb/misc/AnnotationSeries.cpp src/nwb/hdmf/base/Container.cpp src/nwb/hdmf/base/Data.cpp src/nwb/hdmf/table/DynamicTable.cpp diff --git a/src/io/hdf5/HDF5RecordingData.cpp b/src/io/hdf5/HDF5RecordingData.cpp index c7f6d8b6..7fc80e90 100644 --- a/src/io/hdf5/HDF5RecordingData.cpp +++ b/src/io/hdf5/HDF5RecordingData.cpp @@ -53,7 +53,8 @@ Status HDF5RecordingData::writeDataBlock( || type.type == BaseDataType::Type::T_STR) { std::cerr << "HDF5RecordingData::writeDataBlock called for string data, " - "use HDF5RecordingData::writeStringDataBlock instead." + "use HDF5RecordingData::writeDataBlock with a string array " + "data input instead of void* data." << std::endl; return Status::Failure; } diff --git a/src/nwb/NWBFile.cpp b/src/nwb/NWBFile.cpp index 4d3b4743..dc23df75 100644 --- a/src/nwb/NWBFile.cpp +++ b/src/nwb/NWBFile.cpp @@ -16,6 +16,7 @@ #include "nwb/ecephys/ElectricalSeries.hpp" #include "nwb/ecephys/SpikeEventSeries.hpp" #include "nwb/file/ElectrodeGroup.hpp" +#include "nwb/misc/AnnotationSeries.hpp" #include "spec/core.hpp" #include "spec/hdmf_common.hpp" #include "spec/hdmf_experimental.hpp" @@ -298,6 +299,35 @@ Status NWBFile::createSpikeEventSeries( return Status::Success; } +Status NWBFile::createAnnotationSeries(std::vector<std::string> recordingNames, + RecordingContainers* recordingContainers, + std::vector<SizeType>& containerIndexes) +{ + if (!m_io->canModifyObjects()) { + return Status::Failure; + } + + for (size_t i = 0; i < recordingNames.size(); ++i) { + const std::string& recordingName = recordingNames[i]; + + std::string annotationSeriesPath = + AQNWB::mergePaths(acquisitionPath, recordingName); + + // Setup annotation series datasets + auto annotationSeries = + std::make_unique<AnnotationSeries>(annotationSeriesPath, m_io); + annotationSeries->initialize( + "Stores user annotations made during an experiment", + "no comments", + SizeArray {0}, + SizeArray {CHUNK_XSIZE}); + recordingContainers->addContainer(std::move(annotationSeries)); + containerIndexes.push_back(recordingContainers->size() - 1); + } + + return Status::Success; +} + template<SizeType N> void NWBFile::cacheSpecifications( const std::string& specPath, diff --git a/src/nwb/NWBFile.hpp b/src/nwb/NWBFile.hpp index a9e014ce..69ed79bf 100644 --- a/src/nwb/NWBFile.hpp +++ b/src/nwb/NWBFile.hpp @@ -131,6 +131,20 @@ class NWBFile : public Container RecordingContainers* recordingContainers = nullptr, std::vector<SizeType>& containerIndexes = emptyContainerIndexes); + /** @brief Create AnnotationSeries objects to record data into. + * Created objects are stored in recordingContainers. + * @param recordingNames vector indicating the names of the AnnotationSeries + * within the acquisition group + * @param recordingContainers The container to store the created TimeSeries. + * @param containerIndexes The indexes of the containers added to + * recordingContainers + * @return Status The status of the object creation operation. + */ + Status createAnnotationSeries( + std::vector<std::string> recordingNames, + RecordingContainers* recordingContainers = nullptr, + std::vector<SizeType>& containerIndexes = emptyContainerIndexes); + protected: /** * @brief Creates the default file structure. diff --git a/src/nwb/RecordingContainers.cpp b/src/nwb/RecordingContainers.cpp index dc21d29e..f65b785e 100644 --- a/src/nwb/RecordingContainers.cpp +++ b/src/nwb/RecordingContainers.cpp @@ -4,6 +4,7 @@ #include "nwb/ecephys/ElectricalSeries.hpp" #include "nwb/ecephys/SpikeEventSeries.hpp" #include "nwb/hdmf/base/Container.hpp" +#include "nwb/misc/AnnotationSeries.hpp" using namespace AQNWB::NWB; // Recording Container @@ -86,3 +87,19 @@ Status RecordingContainers::writeSpikeEventData(const SizeType& containerInd, return ses->writeSpike( numSamples, numChannels, data, timestamps, controlInput); } + +Status RecordingContainers::writeAnnotationSeriesData( + const SizeType& containerInd, + const SizeType& numSamples, + const std::vector<std::string> data, + const void* timestamps, + const void* controlInput) +{ + AnnotationSeries* as = + dynamic_cast<AnnotationSeries*>(getContainer(containerInd)); + + if (as == nullptr) + return Status::Failure; + + return as->writeAnnotation(numSamples, data, timestamps, controlInput); +} \ No newline at end of file diff --git a/src/nwb/RecordingContainers.hpp b/src/nwb/RecordingContainers.hpp index 33f2b1da..f57bcf0d 100644 --- a/src/nwb/RecordingContainers.hpp +++ b/src/nwb/RecordingContainers.hpp @@ -111,6 +111,22 @@ class RecordingContainers const void* timestamps, const void* controlInput = nullptr); + /** + * @brief Write AnnotationSeries data to a recordingContainer dataset. + * @param containerInd The index of the AnnotationSeries dataset within the + * AnnotationSeries containers. + * @param numSamples Number of samples in the time for the single event. + * @param data A vector of strings of data to write. + * @param timestamps A pointer to the timestamps block + * @param controlInput A pointer to the control block data (optional) + * @return The status of the write operation. + */ + Status writeAnnotationSeriesData(const SizeType& containerInd, + const SizeType& numSamples, + const std::vector<std::string> data, + const void* timestamps, + const void* controlInput = nullptr); + /** * @brief Get the number of recording containers */ diff --git a/src/nwb/misc/AnnotationSeries.cpp b/src/nwb/misc/AnnotationSeries.cpp new file mode 100644 index 00000000..5e5db340 --- /dev/null +++ b/src/nwb/misc/AnnotationSeries.cpp @@ -0,0 +1,71 @@ + +#include "nwb/misc/AnnotationSeries.hpp" + +#include "Utils.hpp" + +using namespace AQNWB::NWB; + +// AnnotationSeries +// Initialize the static registered_ member to trigger registration +REGISTER_SUBCLASS_IMPL(AnnotationSeries) + +/** Constructor */ +AnnotationSeries::AnnotationSeries(const std::string& path, + std::shared_ptr<IO::BaseIO> io) + : TimeSeries(path, io) +{ +} + +/** Destructor */ +AnnotationSeries::~AnnotationSeries() {} + +/** Initialization function*/ +void AnnotationSeries::initialize(const std::string& description, + const std::string& comments, + const SizeArray& dsetSize, + const SizeArray& chunkSize) +{ + TimeSeries::initialize( + IO::BaseDataType::V_STR, // fixed to string according to schema + "n/a", // unit fixed to "n/a" + description, + comments, + dsetSize, + chunkSize, + 1.0f, // conversion fixed to 1.0, since unit is n/a + -1.0f, // resolution fixed to -1.0 + 0.0f); // offset fixed to 0.0, since unit is n/a +} + +Status AnnotationSeries::writeAnnotation(const SizeType& numSamples, + std::vector<std::string> dataInput, + const void* timestampsInput, + const void* controlInput) +{ + std::vector<SizeType> dataShape = {numSamples}; + std::vector<SizeType> positionOffset = {this->m_samplesRecorded}; + + // Write timestamps + Status tsStatus = Status::Success; + tsStatus = this->timestamps->writeDataBlock( + dataShape, positionOffset, this->timestampsType, timestampsInput); + + // Write the data + Status dataStatus = this->data->writeDataBlock( + dataShape, positionOffset, this->m_dataType, dataInput); + + // Write the control data if it exists + if (controlInput != nullptr) { + tsStatus = this->control->writeDataBlock( + dataShape, positionOffset, this->controlType, controlInput); + } + + // track samples recorded + m_samplesRecorded += numSamples; + + if ((dataStatus != Status::Success) || (tsStatus != Status::Success)) { + return Status::Failure; + } else { + return Status::Success; + } +} \ No newline at end of file diff --git a/src/nwb/misc/AnnotationSeries.hpp b/src/nwb/misc/AnnotationSeries.hpp new file mode 100644 index 00000000..a9ac2ac1 --- /dev/null +++ b/src/nwb/misc/AnnotationSeries.hpp @@ -0,0 +1,71 @@ + +#pragma once + +#include <string> + +#include "Utils.hpp" +#include "io/BaseIO.hpp" +#include "io/ReadIO.hpp" +#include "nwb/base/TimeSeries.hpp" + +namespace AQNWB::NWB +{ +/** + * @brief TimeSeries storing text-based records about the experiment. + */ +class AnnotationSeries : public TimeSeries +{ +public: + // Register the AnnotationSeries + REGISTER_SUBCLASS(AnnotationSeries, "core") + + /** + * @brief Constructor. + * @param path The location of the AnnotationSeries in the file. + * @param io A shared pointer to the IO object. + */ + AnnotationSeries(const std::string& path, std::shared_ptr<IO::BaseIO> io); + + /** + * @brief Destructor + */ + ~AnnotationSeries(); + + /** + * @brief Initializes the AnnotationSeries + * @param description The description of the AnnotationSeries. + * @param dsetSize Initial size of the main dataset. This must be a vector + * with one element specifying the length in time. + * @param chunkSize Chunk size to use. + */ + void initialize(const std::string& description, + const std::string& comments, + const SizeArray& dsetSize, + const SizeArray& chunkSize); + + /** + * @brief Writes a channel to an AnnotationSeries dataset. + * @param numSamples The number of samples to write (length in time). + * @param dataInput A vector of strings. + * @param timestampsInput A pointer to the timestamps block. + * @param controlInput A pointer to the control block data (optional) + * @return The status of the write operation. + */ + Status writeAnnotation(const SizeType& numSamples, + const std::vector<std::string> dataInput, + const void* timestampsInput, + const void* controlInput = nullptr); + + DEFINE_FIELD(readData, + DatasetField, + std::string, + "data", + Annotations made during an experiment.) + +private: + /** + * @brief The number of samples already written per channel. + */ + SizeType m_samplesRecorded = 0; +}; +} // namespace AQNWB::NWB diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c5288219..8615085e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -18,6 +18,7 @@ add_executable(aqnwb_test testElementIdentifiers.cpp testFile.cpp testHDF5IO.cpp + testMisc.cpp testNWBFile.cpp testRecordingWorkflow.cpp testRegisteredType.cpp diff --git a/tests/testMisc.cpp b/tests/testMisc.cpp new file mode 100644 index 00000000..0770e1fc --- /dev/null +++ b/tests/testMisc.cpp @@ -0,0 +1,73 @@ +#include <H5Cpp.h> +#include <catch2/catch_test_macros.hpp> +#include <catch2/matchers/catch_matchers_all.hpp> + +#include "Types.hpp" +#include "Utils.hpp" +#include "io/BaseIO.hpp" +#include "nwb/misc/AnnotationSeries.hpp" +#include "testUtils.hpp" + +using namespace AQNWB; + +TEST_CASE("AnnotationSeries", "[misc]") +{ + // setup recording info + SizeType numSamples = 3; + std::string dataPath = "/annotations"; + std::vector<std::string> mockAnnotations = { + "Subject moved", + "Break started", + "Break ended", + }; + std::vector<double> mockTimestamps = getMockTimestamps(numSamples, 1); + std::vector<double> mockTimestamps2 = mockTimestamps; + for (double& value : mockTimestamps2) { + value += 5; + } + + SECTION("test writing annotations") + { + // setup io object + std::string path = getTestFilePath("AnnotationSeries.h5"); + std::shared_ptr<BaseIO> io = createIO("HDF5", path); + io->open(); + + // setup annotation series + NWB::AnnotationSeries as = NWB::AnnotationSeries(dataPath, io); + as.initialize( + "Test annotations", "Test comments", SizeArray {0}, SizeArray {1}); + + // write annotations multiple times to test adding to same dataset + Status writeStatus = + as.writeAnnotation(numSamples, mockAnnotations, mockTimestamps.data()); + REQUIRE(writeStatus == Status::Success); + Status writeStatus2 = + as.writeAnnotation(numSamples, mockAnnotations, mockTimestamps2.data()); + REQUIRE(writeStatus2 == Status::Success); + io->flush(); + + // Read annotations back from file + std::vector<std::string> expectedAnnotations = mockAnnotations; + expectedAnnotations.insert(expectedAnnotations.end(), + mockAnnotations.begin(), + mockAnnotations.end()); + std::vector<std::string> dataOut(expectedAnnotations.size()); + + auto readDataWrapper = as.readData(); + auto readAnnotationsDataTyped = readDataWrapper->values(); + REQUIRE(readAnnotationsDataTyped.data == expectedAnnotations); + + // Read timestamps + std::vector<double> expectedTimestamps = mockTimestamps; + expectedTimestamps.insert(expectedTimestamps.end(), + mockTimestamps2.begin(), + mockTimestamps2.end()); + std::vector<double> timestampsOut(expectedTimestamps.size()); + + auto readTimestampsWrapper = as.readTimestamps(); + auto readTimestampsDataTyped = readTimestampsWrapper->values(); + REQUIRE_THAT(readTimestampsDataTyped.data, + Catch::Matchers::Approx(expectedTimestamps)); + } +} diff --git a/tests/testNWBFile.cpp b/tests/testNWBFile.cpp index 35939c3f..9bbd8f29 100644 --- a/tests/testNWBFile.cpp +++ b/tests/testNWBFile.cpp @@ -10,6 +10,7 @@ #include "nwb/RecordingContainers.hpp" #include "nwb/base/TimeSeries.hpp" #include "nwb/ecephys/SpikeEventSeries.hpp" +#include "nwb/misc/AnnotationSeries.hpp" #include "testUtils.hpp" using namespace AQNWB; @@ -164,6 +165,60 @@ TEST_CASE("createMultipleEcephysDatasets", "[nwb]") nwbfile.finalize(); } +TEST_CASE("createAnnotationSeries", "[nwb]") +{ + std::string filename = getTestFilePath("createAnnotationSeries.nwb"); + + // initialize nwbfile object and create base structure + std::shared_ptr<IO::HDF5::HDF5IO> io = + std::make_shared<IO::HDF5::HDF5IO>(filename); + io->open(); + NWB::NWBFile nwbfile(io); + nwbfile.initialize(generateUuid()); + + // create Annotation Series + std::vector<std::string> mockAnnotationNames = {"annotations1", + "annotations2"}; + std::unique_ptr<NWB::RecordingContainers> recordingContainers = + std::make_unique<NWB::RecordingContainers>(); + Status resultCreate = nwbfile.createAnnotationSeries( + mockAnnotationNames, recordingContainers.get()); + REQUIRE(resultCreate == Status::Success); + + // start recording + Status resultStart = io->startRecording(); + REQUIRE(resultStart == Status::Success); + + // write annotation data + std::vector<std::string> mockAnnotations = { + "Start recording", "Subject moved", "End recording"}; + std::vector<double> mockTimestamps = {0.1, 0.5, 1.0}; + std::vector<SizeType> positionOffset = {0}; + SizeType dataShape = mockAnnotations.size(); + + // write to both annotation series + recordingContainers->writeAnnotationSeriesData( + 0, dataShape, mockAnnotations, mockTimestamps.data()); + recordingContainers->writeAnnotationSeriesData( + 1, dataShape, mockAnnotations, mockTimestamps.data()); + + // test searching for all AnnotationSeries objects + std::unordered_set<std::string> typesToSearch = {"core::AnnotationSeries"}; + std::unordered_map<std::string, std::string> found_types = + io->findTypes("/", typesToSearch, IO::SearchMode::CONTINUE_ON_TYPE); + REQUIRE(found_types.size() + == 2); // We should have annotations1 and annotations2 + for (const auto& pair : found_types) { + // only AnnotationSeries should be found + REQUIRE(pair.second == "core::AnnotationSeries"); + // only annotations1 or annotations2 should be in the list + REQUIRE(((pair.first == "/acquisition/annotations1") + || (pair.first == "/acquisition/annotations2"))); + } + + nwbfile.finalize(); +} + TEST_CASE("setCanModifyObjectsMode", "[nwb]") { std::string filename = getTestFilePath("testCanModifyObjectsMode.nwb"); From 5100ad21a1ef15cfe90b8d179c3ac2b450fe83c1 Mon Sep 17 00:00:00 2001 From: Oliver Ruebel <oruebel@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:43:09 -0800 Subject: [PATCH 3/5] Update docs/pages/userdocs/read.dox Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com> --- docs/pages/userdocs/read.dox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pages/userdocs/read.dox b/docs/pages/userdocs/read.dox index 38156850..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 associated with a storage objects (e.g., a Group) via \ref AQNWB::IO::BaseIO::getStorageObjects "getStoragebjects()" + * object associated with a storage object (e.g., a Group) via \ref AQNWB::IO::BaseIO::getStorageObjects "getStorageObjects()" * * \subsubsection read_design_wrapper_registeredType RegisteredType * From 67bc2cd483862f15ead95bbad59409c923cee891 Mon Sep 17 00:00:00 2001 From: Oliver Ruebel <oruebel@users.noreply.github.com> Date: Wed, 22 Jan 2025 16:43:25 -0800 Subject: [PATCH 4/5] Update src/Types.hpp Co-authored-by: Steph Prince <40640337+stephprince@users.noreply.github.com> --- src/Types.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Types.hpp b/src/Types.hpp index 17ebbdfc..6d45f767 100644 --- a/src/Types.hpp +++ b/src/Types.hpp @@ -39,7 +39,7 @@ class Types * \brief Helper struct to check if a value is a data field, i.e., * Dataset or Attribute * - * This function is used to enforce constrains on templated functions that + * This function is used to enforce constraints on templated functions that * should only be callable for valid StorageObjectType values */ template<StorageObjectType T> From 19fecb4225c58b70e8448124878a537fd8b72d92 Mon Sep 17 00:00:00 2001 From: Oliver Ruebel <oruebel@lbl.gov> Date: Wed, 22 Jan 2025 23:30:16 -0800 Subject: [PATCH 5/5] Add additional test case for HDF5IO::getStorageObjects --- tests/testHDF5IO.cpp | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/testHDF5IO.cpp b/tests/testHDF5IO.cpp index 72aacf11..f50831f9 100644 --- a/tests/testHDF5IO.cpp +++ b/tests/testHDF5IO.cpp @@ -142,6 +142,38 @@ TEST_CASE("getStorageObjects", "[hdf5io]") REQUIRE(groupContent.size() == 0); } + 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");