Skip to content
This repository was archived by the owner on Feb 26, 2025. It is now read-only.

Commit 7638232

Browse files
authored
Improve testing infrastructure. (#871)
Adds/reimplements abstractions for the following: * Create multi-dimensional array filled with suitable values. * Traits for accessing values. * Traits for hiding the difference of DataSet and Attribute. * Useful utilities such as `ravel`, `unravel` and `flat_size`.
1 parent 065234d commit 7638232

File tree

6 files changed

+1019
-1
lines changed

6 files changed

+1019
-1
lines changed

doc/developer_guide.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,134 @@ release. Once this is done perform a final round of updates:
9191
* Update BlueBrain Spack recipe to use the archive and not the Git commit.
9292
* Update the upstream Spack recipe.
9393

94+
## Writing Tests
95+
### Generate Multi-Dimensional Test Data
96+
Input array of any dimension and type can be generated using the template class
97+
`DataGenerator`. For example:
98+
```
99+
auto dims = std::vector<size_t>{4, 2};
100+
auto values = testing::DataGenerator<std::vector<std::array<double, 2>>::create(dims);
101+
```
102+
Generates an `std::vector<std::array<double, 2>>` initialized with suitable
103+
values.
104+
105+
If "suitable" isn't specific enough, one can specify a callback:
106+
```
107+
auto callback = [](const std::vector<size_t>& indices) {
108+
return 42.0;
109+
}
110+
111+
auto values = testing::DataGenerator<std::vector<double>>::create(dims, callback);
112+
```
113+
114+
The `dims` can be generated via `testing::DataGenerator::default_dims` or by
115+
using `testing::DataGenerator::sanitize_dims`. Remember, that certain
116+
containers are fixed size and that we often compute the number of elements by
117+
multiplying the dims.
118+
119+
### Generate Scalar Test Data
120+
To generate a single "suitable" element use template class `DefaultValues`, e.g.
121+
```
122+
auto default_values = testing::DefaultValues<double>();
123+
auto x = testing::DefaultValues<double>(indices);
124+
```
125+
126+
### Accessing Elements
127+
To access a particular element from an unknown container use the following trait:
128+
```
129+
using trait = testing::ContainerTraits<std::vector<std::array<int, 2>>;
130+
// auto x = values[1][0];
131+
auto x = trait::get(values, {1, 0});
132+
133+
// values[1][0] = 42.0;
134+
trait::set(values, {1, 0}, 42.0);
135+
```
136+
137+
### Utilities For Multi-Dimensional Arrays
138+
Use `testing::DataGenerator::allocate` to allocate an array (without filling
139+
it) and `testing::copy` to copy an array from one type to another. There's
140+
`testing::ravel`, `testing::unravel` and `testing::flat_size` to compute the
141+
position in a flat array from a multi-dimensional index, the reverse and the
142+
number of element in the multi-dimensional array.
143+
144+
### Deduplicating DataSet and Attribute
145+
Due to how HighFive is written testing `DataSet` and `Attribute` often requires
146+
duplicating the entire test code because somewhere a `createDataSet` must be
147+
replaced with `createAttribute`. Use `testing::AttributeCreateTraits` and
148+
`testing::DataSetCreateTraits`. For example,
149+
```
150+
template<class CreateTraits>
151+
void check_write(...) {
152+
// Same as one of:
153+
// file.createDataSet(name, values);
154+
// file.createAttribute(name, values);
155+
CreateTraits::create(file, name, values);
156+
}
157+
```
158+
159+
### Test Organization
160+
#### Multi-Dimensional Arrays
161+
All tests for reading/writing whole multi-dimensional arrays to datasets or
162+
attributes belong in `tests/unit/tests_high_five_multi_dimensional.cpp`. This
163+
includes write/read cycles; checking all the generic edges cases, e.g. empty
164+
arrays and mismatching sizes; and checking non-reallocation.
165+
166+
Read/Write cycles are implemented in two distinct checks. One for writing and
167+
another for reading. When checking writing we read with a "trusted"
168+
multi-dimensional array (a nested `std::vector`), and vice-versa when checking
169+
reading. This matters because certain bugs, like writing a column major array
170+
as if it were row-major can't be caught if one reads it back into a
171+
column-major array.
172+
173+
Remember, `std::vector<bool>` is very different from all other `std::vector`s.
174+
175+
Every container `template<class T> C;` should at least be checked with all of
176+
the following `T`s that are supported by the container: `bool`, `double`,
177+
`std::string`, `std::vector`, `std::array`. The reason is `bool` and
178+
`std::string` are special, `double` is just a POD, `std::vector` requires
179+
dynamic memory allocation and `std::array` is statically allocated.
180+
181+
Similarly, each container should be put inside an `std::vector` and an
182+
`std::array`.
183+
184+
#### Scalar Data Set
185+
Write-read cycles for scalar values should be implemented in
186+
`tests/unit/tests_high_five_scalar.cpp`.
187+
188+
#### Data Types
189+
Unit-tests related to checking that `DataType` API, go in
190+
`tests/unit/tests_high_data_type.cpp`.
191+
192+
#### Selections
193+
Anything selection related goes in `tests/unit/test_high_five_selection.cpp`.
194+
This includes things like `ElementSet` and `HyperSlab`.
195+
196+
#### Strings
197+
Regular write-read cycles for strings are performed along with the other types,
198+
see above. This should cover compatibility of `std::string` with all
199+
containers. However, additional testing is required, e.g. character set,
200+
padding, fixed vs. variable length. These all go in
201+
`tests/unit/test_high_five_string.cpp`.
202+
203+
#### Specific Tests For Optional Containers
204+
If containers, e.g. `Eigen::Matrix` require special checks those go in files
205+
called `tests/unit/test_high_five_*.cpp` where `*` is `eigen` for Eigen.
206+
207+
#### Memory Layout Assumptions
208+
In HighFive we make assumptions about the memory layout of certain types. For
209+
example, we assume that
210+
```
211+
auto array = std::vector<std::array<double, 2>>(n);
212+
doube * ptr = (double*) array.data();
213+
```
214+
is a sensible thing to do. We assume similar about `bool` and
215+
`details::Boolean`. These types of tests go into
216+
`tests/unit/tests_high_five_memory_layout.cpp`.
217+
218+
#### H5Easy
219+
Anything `H5Easy` related goes in files with the appropriate name.
220+
221+
#### Everything Else
222+
What's left goes in `tests/unit/test_high_five_base.cpp`. This covers opening
223+
files, groups, dataset or attributes; checking certain pathological edge cases;
224+
etc.

tests/unit/create_traits.hpp

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#pragma once
2+
3+
namespace HighFive {
4+
namespace testing {
5+
6+
/// \brief Trait for `createAttribute`.
7+
///
8+
/// The point of these is to simplify testing. The typical issue is that we
9+
/// need to write the tests twice, one with `createDataSet` and then again with
10+
/// `createAttribute`. This trait allows us to inject this difference.
11+
struct AttributeCreateTraits {
12+
using type = Attribute;
13+
14+
template <class Hi5>
15+
static Attribute get(Hi5& hi5, const std::string& name) {
16+
return hi5.getAttribute(name);
17+
}
18+
19+
20+
template <class Hi5, class Container>
21+
static Attribute create(Hi5& hi5, const std::string& name, const Container& container) {
22+
return hi5.createAttribute(name, container);
23+
}
24+
25+
template <class Hi5>
26+
static Attribute create(Hi5& hi5,
27+
const std::string& name,
28+
const DataSpace& dataspace,
29+
const DataType& datatype) {
30+
return hi5.createAttribute(name, dataspace, datatype);
31+
}
32+
33+
template <class T, class Hi5>
34+
static Attribute create(Hi5& hi5, const std::string& name, const DataSpace& dataspace) {
35+
auto datatype = create_datatype<T>();
36+
return hi5.template createAttribute<T>(name, dataspace);
37+
}
38+
};
39+
40+
/// \brief Trait for `createDataSet`.
41+
struct DataSetCreateTraits {
42+
using type = DataSet;
43+
44+
template <class Hi5>
45+
static DataSet get(Hi5& hi5, const std::string& name) {
46+
return hi5.getDataSet(name);
47+
}
48+
49+
template <class Hi5, class Container>
50+
static DataSet create(Hi5& hi5, const std::string& name, const Container& container) {
51+
return hi5.createDataSet(name, container);
52+
}
53+
54+
template <class Hi5>
55+
static DataSet create(Hi5& hi5,
56+
const std::string& name,
57+
const DataSpace& dataspace,
58+
const DataType& datatype) {
59+
return hi5.createDataSet(name, dataspace, datatype);
60+
}
61+
62+
template <class T, class Hi5>
63+
static DataSet create(Hi5& hi5, const std::string& name, const DataSpace& dataspace) {
64+
auto datatype = create_datatype<T>();
65+
return hi5.template createDataSet<T>(name, dataspace);
66+
}
67+
};
68+
69+
} // namespace testing
70+
} // namespace HighFive

0 commit comments

Comments
 (0)