use hdf5_pure::{
AttrValue, CharacterSet, Datatype, Error, File, FileBuilder, FormatError, StringPadding,
VlenStringReadOptions,
};
use tempfile::tempdir;
#[test]
fn fixed_width_strings_round_trip_through_a_file() {
let values = ["north", "s", "", "east"];
let mut b = FileBuilder::new();
b.create_dataset("derived_ascii")
.with_ascii_strings(&values)
.unwrap();
b.create_dataset("sized_ascii")
.with_ascii_strings_sized(&values, 16)
.unwrap();
b.create_dataset("derived_utf8")
.with_strings(&values)
.unwrap();
b.create_dataset("sized_utf8")
.with_strings_sized(&values, 16)
.unwrap();
let file = File::from_bytes(b.finish().unwrap()).unwrap();
for (name, width, charset) in [
("derived_ascii", 5, CharacterSet::Ascii),
("sized_ascii", 16, CharacterSet::Ascii),
("derived_utf8", 5, CharacterSet::Utf8),
("sized_utf8", 16, CharacterSet::Utf8),
] {
let ds = file.dataset(name).unwrap();
assert_eq!(
ds.datatype().unwrap(),
Datatype::String {
size: width,
padding: StringPadding::NullPad,
charset,
},
"{name}"
);
assert_eq!(ds.shape().unwrap(), vec![4], "{name}");
assert_eq!(ds.read_string().unwrap(), values, "{name}");
assert_eq!(ds.read_string_rows(1, 2).unwrap(), ["s", ""], "{name}");
}
}
#[test]
fn a_utf8_value_is_measured_in_bytes_and_read_back_whole() {
let values = ["mètre", "K", "°C"];
let mut b = FileBuilder::new();
b.create_dataset("units").with_strings(&values).unwrap();
let file = File::from_bytes(b.finish().unwrap()).unwrap();
let ds = file.dataset("units").unwrap();
assert_eq!(
ds.datatype().unwrap(),
Datatype::String {
size: 6,
padding: StringPadding::NullPad,
charset: CharacterSet::Utf8,
}
);
assert_eq!(ds.read_string().unwrap(), values);
}
#[test]
fn a_declared_width_leaves_room_for_a_later_longer_value() {
let dir = tempdir().unwrap();
let path = dir.path().join("stations.h5");
let mut b = FileBuilder::new();
b.create_dataset("station")
.with_ascii_strings_sized(&["north", "s"], 16)
.unwrap()
.with_maxshape(&[u64::MAX])
.with_chunks(&[4]);
b.write(&path).unwrap();
let appended = ["north-northeast", "e"];
{
let session = File::open_rw(&path).unwrap();
let mut raw = Vec::new();
for value in appended {
let mut element = value.as_bytes().to_vec();
element.resize(16, 0);
raw.extend_from_slice(&element);
}
session
.dataset("station")
.unwrap()
.append_staged(|a| {
a.append_raw(&raw);
})
.unwrap();
session.commit().unwrap();
}
let file = File::open(&path).unwrap();
let ds = file.dataset("station").unwrap();
assert_eq!(ds.shape().unwrap(), vec![4]);
assert_eq!(
ds.read_string().unwrap(),
["north", "s", "north-northeast", "e"]
);
}
#[test]
fn a_refused_width_leaves_the_builder_usable() {
let mut b = FileBuilder::new();
let ds = b.create_dataset("station");
assert!(matches!(
ds.with_ascii_strings_sized(&["north"], 2),
Err(FormatError::FixedStringTooLong { index: 0, .. })
));
assert!(matches!(
ds.with_ascii_strings_sized(&["north"], 0),
Err(FormatError::ZeroFixedStringWidth)
));
ds.with_ascii_strings_sized(&["north"], 8).unwrap();
let file = File::from_bytes(b.finish().unwrap()).unwrap();
assert_eq!(
file.dataset("station").unwrap().read_string().unwrap(),
["north"]
);
}
#[test]
fn a_fixed_width_dataset_is_refused_by_the_vlen_reader() {
let mut b = FileBuilder::new();
b.create_dataset("station")
.with_ascii_strings(&["north"])
.unwrap();
let file = File::from_bytes(b.finish().unwrap()).unwrap();
let ds = file.dataset("station").unwrap();
assert!(
matches!(
ds.read_vlen_strings(VlenStringReadOptions::default()),
Err(Error::Format(FormatError::TypeMismatch {
expected: "VariableLength string",
..
}))
),
"expected a type mismatch naming the type it wanted"
);
assert_eq!(ds.read_string().unwrap(), ["north"]);
}
#[test]
fn all_empty_values_take_a_one_byte_datatype() {
let mut b = FileBuilder::new();
b.create_dataset("blank")
.with_ascii_strings(&["", "", ""])
.unwrap();
let file = File::from_bytes(b.finish().unwrap()).unwrap();
let ds = file.dataset("blank").unwrap();
assert_eq!(
ds.datatype().unwrap(),
Datatype::String {
size: 1,
padding: StringPadding::NullPad,
charset: CharacterSet::Ascii,
}
);
assert_eq!(ds.read_string().unwrap(), ["", "", ""]);
}
fn attr_width(file: &File, dataset: &str, attr: &str) -> u32 {
let types = file.dataset(dataset).unwrap().attr_datatypes().unwrap();
match types.get(attr) {
Some(Datatype::String { size, .. }) => *size,
other => panic!("expected a string attribute datatype, got {other:?}"),
}
}
#[test]
fn a_declared_attribute_width_round_trips_through_a_file() {
let mut b = FileBuilder::new();
b.create_dataset("reading")
.with_i32_data(&[1])
.set_attr("units", AttrValue::ascii_string_sized("ok", 64).unwrap())
.set_attr(
"labels",
AttrValue::string_array_sized(vec!["north".into(), "s".into()], 16).unwrap(),
);
let file = File::from_bytes(b.finish().unwrap()).unwrap();
assert_eq!(attr_width(&file, "reading", "units"), 64);
assert_eq!(attr_width(&file, "reading", "labels"), 16);
let attrs = file.dataset("reading").unwrap().attrs().unwrap();
assert_eq!(attrs["units"].as_str(), Some("ok"));
assert_eq!(attrs["labels"].as_strings().unwrap(), ["north", "s"]);
}
#[test]
fn a_declared_slot_keeps_its_width_when_it_is_rewritten() {
let dir = tempdir().unwrap();
let path = dir.path().join("slots.h5");
let mut b = FileBuilder::new();
b.create_dataset("d")
.with_i32_data(&[1])
.set_attr("sized", AttrValue::ascii_string_sized("hello", 64).unwrap())
.set_attr("plain", AttrValue::AsciiString("hello".into()));
b.write(&path).unwrap();
{
let file = File::open(&path).unwrap();
assert_eq!(attr_width(&file, "d", "sized"), 64);
assert_eq!(attr_width(&file, "d", "plain"), 5);
}
{
let session = File::open_rw(&path).unwrap();
let mut dataset = session.dataset("d").unwrap();
dataset
.set_attr("sized", AttrValue::ascii_string_sized("x", 64).unwrap())
.unwrap();
dataset
.set_attr("plain", AttrValue::AsciiString("x".into()))
.unwrap();
session.commit().unwrap();
}
let file = File::open(&path).unwrap();
assert_eq!(
attr_width(&file, "d", "sized"),
64,
"a declared width must survive being rewritten from a shorter value"
);
assert_eq!(
attr_width(&file, "d", "plain"),
1,
"a derived width follows the content, which is what a declared one is for"
);
let attrs = file.dataset("d").unwrap().attrs().unwrap();
assert_eq!(attrs["sized"].as_str(), Some("x"));
assert_eq!(attrs["plain"].as_str(), Some("x"));
}
#[test]
fn a_slot_read_back_can_be_rewritten_without_measuring_it() {
let dir = tempdir().unwrap();
let path = dir.path().join("reuse.h5");
let mut b = FileBuilder::new();
b.create_dataset("d").with_i32_data(&[1]).set_attr(
"units",
AttrValue::ascii_string_sized("metres", 32).unwrap(),
);
b.write(&path).unwrap();
let rewritten = {
let file = File::open(&path).unwrap();
let attrs = file.dataset("d").unwrap().attrs().unwrap();
match &attrs["units"] {
AttrValue::AsciiStringSized { width, .. } => {
AttrValue::ascii_string_sized("feet", width.get()).unwrap()
}
other => panic!("expected a sized ASCII string, got {other:?}"),
}
};
{
let session = File::open_rw(&path).unwrap();
session
.dataset("d")
.unwrap()
.set_attr("units", rewritten)
.unwrap();
session.commit().unwrap();
}
let file = File::open(&path).unwrap();
assert_eq!(attr_width(&file, "d", "units"), 32);
let attrs = file.dataset("d").unwrap().attrs().unwrap();
assert_eq!(attrs["units"].as_str(), Some("feet"));
}
#[test]
fn a_group_attribute_keeps_a_declared_width_through_an_edit() {
let dir = tempdir().unwrap();
let path = dir.path().join("group_slot.h5");
let mut b = FileBuilder::new();
let mut g = b.create_group("run");
g.create_dataset("d").with_i32_data(&[1]);
b.add_group(g.finish());
b.write(&path).unwrap();
{
let session = File::open_rw(&path).unwrap();
session
.group("run")
.unwrap()
.set_attr("units", AttrValue::ascii_string_sized("ok", 48).unwrap())
.unwrap();
session.commit().unwrap();
}
let file = File::open(&path).unwrap();
let group = file.group("run").unwrap();
assert!(
matches!(
group.attr_datatypes().unwrap().get("units"),
Some(Datatype::String { size: 48, .. })
),
"a group attribute must keep its declared width, got {:?}",
group.attr_datatypes().unwrap().get("units")
);
assert_eq!(group.attrs().unwrap()["units"].as_str(), Some("ok"));
}