use std::collections::HashMap;
use std::fmt;
use crate::address::BaseAddress;
use crate::datatype::Datatype;
use crate::display::{DISPLAY_MAX_MEMBERS, Dims, EscapedName, write_elided};
pub use crate::file_writer::AttrValue;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum DType {
F32,
F64,
I8,
I16,
I32,
I64,
U8,
U16,
U32,
U64,
String,
Compound(Vec<(std::string::String, DType)>),
Enum(Vec<std::string::String>),
Array(Box<DType>, Vec<u32>),
VariableLengthString,
ObjectReference,
Other(Box<Datatype>),
}
impl fmt::Display for DType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DType::F32 => write!(f, "f32"),
DType::F64 => write!(f, "f64"),
DType::I8 => write!(f, "i8"),
DType::I16 => write!(f, "i16"),
DType::I32 => write!(f, "i32"),
DType::I64 => write!(f, "i64"),
DType::U8 => write!(f, "u8"),
DType::U16 => write!(f, "u16"),
DType::U32 => write!(f, "u32"),
DType::U64 => write!(f, "u64"),
DType::String => write!(f, "string"),
DType::VariableLengthString => write!(f, "vlen_string"),
DType::ObjectReference => write!(f, "object_ref"),
DType::Compound(fields) => {
write!(f, "compound{{")?;
for (i, (name, dt)) in fields.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}: {dt}", EscapedName(name))?;
}
write_elided(f, fields.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
write!(f, "}}")
}
DType::Enum(names) => {
write!(f, "enum[")?;
for (i, name) in names.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", EscapedName(name))?;
}
write_elided(f, names.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
write!(f, "]")
}
DType::Array(base, dims) => write!(f, "array<{base}, {}>", Dims(dims)),
DType::Other(dt) => write!(f, "other({dt})"),
}
}
}
pub(crate) fn classify_datatype(dt: &Datatype) -> DType {
match dt {
Datatype::FloatingPoint { size: 4, .. } => DType::F32,
Datatype::FloatingPoint { size: 8, .. } => DType::F64,
Datatype::FixedPoint {
size: 1,
signed: true,
..
} => DType::I8,
Datatype::FixedPoint {
size: 2,
signed: true,
..
} => DType::I16,
Datatype::FixedPoint {
size: 4,
signed: true,
..
} => DType::I32,
Datatype::FixedPoint {
size: 8,
signed: true,
..
} => DType::I64,
Datatype::FixedPoint {
size: 1,
signed: false,
..
} => DType::U8,
Datatype::FixedPoint {
size: 2,
signed: false,
..
} => DType::U16,
Datatype::FixedPoint {
size: 4,
signed: false,
..
} => DType::U32,
Datatype::FixedPoint {
size: 8,
signed: false,
..
} => DType::U64,
Datatype::String { .. } => DType::String,
Datatype::VariableLength {
is_string: true, ..
} => DType::VariableLengthString,
Datatype::Compound { members, .. } => {
let fields = members
.iter()
.map(|m| (m.name.clone(), classify_datatype(&m.datatype)))
.collect();
DType::Compound(fields)
}
Datatype::Enumeration { members, .. } => {
let names = members.iter().map(|m| m.name.clone()).collect();
DType::Enum(names)
}
Datatype::Array {
base_type,
dimensions,
} => DType::Array(Box::new(classify_datatype(base_type)), dimensions.clone()),
Datatype::Reference {
ref_type: crate::datatype::ReferenceType::Object,
..
} => DType::ObjectReference,
_ => DType::Other(Box::new(dt.clone())),
}
}
pub(crate) fn attrs_to_map<S: crate::source::Source + ?Sized>(
attrs: &[crate::attribute::AttributeMessage],
source: &S,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> HashMap<std::string::String, AttrValue> {
let mut map = HashMap::new();
for attr in attrs {
if let Some(val) = decode_attr_value(attr, source, offset_size, length_size, base_address) {
map.insert(attr.name.clone(), val);
}
}
map
}
fn decode_attr_value<S: crate::source::Source + ?Sized>(
attr: &crate::attribute::AttributeMessage,
source: &S,
offset_size: u8,
length_size: u8,
base_address: BaseAddress,
) -> Option<AttrValue> {
use crate::dataspace::DataspaceType;
use crate::datatype::Datatype;
let scalar = attr.dataspace.space_type == DataspaceType::Scalar;
match crate::data_read::effective_numeric(&attr.datatype) {
Datatype::FloatingPoint { size: 4, .. } => {
let vals = attr.read_as_f64().ok()?;
if scalar {
Some(AttrValue::F32(narrow_f32(*vals.first()?)))
} else {
Some(AttrValue::F32Array(
vals.into_iter().map(narrow_f32).collect(),
))
}
}
Datatype::FloatingPoint { .. } => {
let vals = attr.read_as_f64().ok()?;
if scalar {
Some(AttrValue::F64(*vals.first()?))
} else {
Some(AttrValue::F64Array(vals))
}
}
Datatype::FixedPoint {
signed: true, size, ..
} => signed_attr_value(attr.read_as_i64().ok()?, scalar, *size),
Datatype::FixedPoint {
signed: false,
size,
..
} => unsigned_attr_value(attr.read_as_u64().ok()?, scalar, *size),
Datatype::String { charset, size, .. } => {
let strings = attr.read_as_strings().ok()?;
Some(if scalar {
crate::type_builders::decoded_fixed_string(one_or_empty(strings), *size, charset)
} else {
crate::type_builders::decoded_fixed_string_array(strings, *size, charset)
})
}
Datatype::VariableLength {
is_string,
base_type,
charset,
..
} if *is_string || is_ascii_char_vlen_base(base_type) => {
let strings = crate::vl_data::read_vl_strings_from_source(
source,
&attr.raw_data,
attr.dataspace.num_elements(),
offset_size,
length_size,
base_address,
crate::vl_data::VlenStringReadOptions::default(),
)
.ok()?;
match (
vlen_string_shape(*is_string, base_type, charset.as_ref()),
scalar,
) {
(VlenStringShape::AsciiCharSequence, true) => {
Some(AttrValue::AsciiString(one_or_empty(strings)))
}
(VlenStringShape::AsciiCharSequence, false) => {
Some(AttrValue::VarLenAsciiCharArray(strings))
}
(VlenStringShape::Ascii, true) => {
Some(AttrValue::VarLenAsciiString(one_or_empty(strings)))
}
(VlenStringShape::Ascii, false) => Some(AttrValue::VarLenAsciiStringArray(strings)),
(VlenStringShape::Utf8, true) => {
Some(AttrValue::VarLenString(one_or_empty(strings)))
}
(VlenStringShape::Utf8, false) => Some(AttrValue::VarLenStringArray(strings)),
}
}
_ => None,
}
}
#[expect(
clippy::cast_possible_truncation,
reason = "narrows a value the reader widened from the 4 bytes the file holds, which is exact for the IEEE layout"
)]
fn narrow_f32(value: f64) -> f32 {
value as f32
}
fn signed_attr_value(values: Vec<i64>, scalar: bool, width: u32) -> Option<AttrValue> {
let narrowed = match width {
1 => narrow_elements(&values, scalar, AttrValue::I8, AttrValue::I8Array),
2 => narrow_elements(&values, scalar, AttrValue::I16, AttrValue::I16Array),
4 => narrow_elements(&values, scalar, AttrValue::I32, AttrValue::I32Array),
_ => None,
};
match narrowed {
Some(value) => Some(value),
None if scalar => Some(AttrValue::I64(*values.first()?)),
None => Some(AttrValue::I64Array(values)),
}
}
fn unsigned_attr_value(values: Vec<u64>, scalar: bool, width: u32) -> Option<AttrValue> {
let narrowed = match width {
1 => narrow_elements(&values, scalar, AttrValue::U8, AttrValue::U8Array),
2 => narrow_elements(&values, scalar, AttrValue::U16, AttrValue::U16Array),
4 => narrow_elements(&values, scalar, AttrValue::U32, AttrValue::U32Array),
_ => None,
};
match narrowed {
Some(value) => Some(value),
None if scalar => Some(AttrValue::U64(*values.first()?)),
None => Some(AttrValue::U64Array(values)),
}
}
fn narrow_elements<S: Copy, T: TryFrom<S>>(
values: &[S],
scalar: bool,
one: fn(T) -> AttrValue,
many: fn(Vec<T>) -> AttrValue,
) -> Option<AttrValue> {
if scalar {
return Some(one(T::try_from(*values.first()?).ok()?));
}
let narrowed: Vec<T> = values
.iter()
.map(|&v| T::try_from(v).ok())
.collect::<Option<Vec<T>>>()?;
Some(many(narrowed))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum VlenStringShape {
AsciiCharSequence,
Ascii,
Utf8,
}
fn vlen_string_shape(
is_string: bool,
base_type: &crate::datatype::Datatype,
charset: Option<&crate::datatype::CharacterSet>,
) -> VlenStringShape {
use crate::datatype::CharacterSet;
if !is_string && is_ascii_char_vlen_base(base_type) {
return VlenStringShape::AsciiCharSequence;
}
if charset == Some(&CharacterSet::Ascii) {
VlenStringShape::Ascii
} else {
VlenStringShape::Utf8
}
}
fn one_or_empty(strings: Vec<std::string::String>) -> std::string::String {
strings.into_iter().next().unwrap_or_default()
}
fn is_ascii_char_vlen_base(base: &crate::datatype::Datatype) -> bool {
use crate::datatype::{CharacterSet, Datatype};
matches!(
base,
Datatype::String {
size: 1,
charset: CharacterSet::Ascii,
..
}
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datatype::{Datatype, DatatypeByteOrder};
#[test]
fn an_unusual_size_arrives_whole_and_writes_its_width() {
let int = Datatype::FixedPoint {
size: u32::MAX,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 0,
};
let float = Datatype::FloatingPoint {
size: u32::MAX,
byte_order: DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 0,
exponent_location: 0,
exponent_size: 0,
mantissa_location: 0,
mantissa_size: 0,
exponent_bias: 0,
};
let bits = u64::from(u32::MAX) * 8;
for (dt, prefix) in [(&int, 'i'), (&float, 'f')] {
let classified = classify_datatype(dt);
assert_eq!(classified, DType::Other(Box::new(dt.clone())));
assert_eq!(
classified.to_string(),
format!("other({prefix}{bits}(bits 0..0))")
);
}
}
#[test]
fn an_unclassified_type_reaches_the_caller_whole_through_either_recursion() {
let opaque = Datatype::Opaque {
size: 3,
tag: b"rgb".to_vec(),
};
let carried = DType::Other(Box::new(opaque.clone()));
let compound = Datatype::Compound {
size: 3,
members: vec![crate::datatype::CompoundMember {
name: "pixel".into(),
byte_offset: 0,
datatype: opaque.clone(),
}],
};
let DType::Compound(fields) = classify_datatype(&compound) else {
panic!("a compound classifies as one");
};
assert_eq!(fields, vec![("pixel".to_string(), carried.clone())]);
let array = Datatype::Array {
base_type: Box::new(opaque),
dimensions: vec![2, 3],
};
assert_eq!(
classify_datatype(&array),
DType::Array(Box::new(carried), vec![2, 3])
);
}
fn round_trip(cases: &[(&str, AttrValue)]) -> HashMap<std::string::String, AttrValue> {
let mut builder = crate::writer::FileBuilder::new();
for (name, value) in cases {
builder.set_attr(name, value.clone());
}
builder.create_dataset("x").with_f64_data(&[1.0]);
let bytes = builder.finish().unwrap();
crate::File::from_bytes(bytes)
.unwrap()
.root()
.attrs()
.unwrap()
}
#[test]
fn every_string_variant_round_trips_to_itself() {
let cases = vec![
("utf8_scalar", AttrValue::String("m/s".into())),
("utf8_one", AttrValue::StringArray(vec!["m/s".into()])),
(
"utf8_two",
AttrValue::StringArray(vec!["m/s".into(), "kg".into()]),
),
("ascii_scalar", AttrValue::AsciiString("double".into())),
(
"ascii_one",
AttrValue::AsciiStringArray(vec!["double".into()]),
),
(
"ascii_two",
AttrValue::AsciiStringArray(vec!["double".into(), "int16".into()]),
),
(
"vlen_one",
AttrValue::VarLenAsciiCharArray(vec!["x".into()]),
),
(
"vlen_three",
AttrValue::VarLenAsciiCharArray(vec!["x".into(), "y".into(), "velocity".into()]),
),
("vlen_utf8_scalar", AttrValue::VarLenString("mètre".into())),
(
"vlen_utf8_one",
AttrValue::VarLenStringArray(vec!["mètre".into()]),
),
(
"vlen_ascii_scalar",
AttrValue::VarLenAsciiString("double".into()),
),
(
"vlen_ascii_two",
AttrValue::VarLenAsciiStringArray(vec!["x".into(), "yy".into()]),
),
(
"ascii_sized",
AttrValue::ascii_string_sized("ok", 64).unwrap(),
),
(
"ascii_array_sized",
AttrValue::ascii_string_array_sized(vec!["north".into(), "s".into()], 16).unwrap(),
),
("utf8_sized", AttrValue::string_sized("mètre", 32).unwrap()),
(
"utf8_array_sized",
AttrValue::string_array_sized(vec!["m/s".into()], 12).unwrap(),
),
];
let read = round_trip(&cases);
for (name, written) in &cases {
assert_eq!(read.get(*name), Some(written), "attribute {name}");
}
}
#[test]
fn a_width_the_content_implies_reads_back_as_the_plain_variant() {
let cases = vec![
("exact", AttrValue::ascii_string_sized("double", 6).unwrap()),
("empty", AttrValue::ascii_string_sized("", 1).unwrap()),
(
"longest",
AttrValue::string_array_sized(vec!["m/s".into(), "kg".into()], 3).unwrap(),
),
];
let read = round_trip(&cases);
assert_eq!(
read.get("exact"),
Some(&AttrValue::AsciiString("double".into()))
);
assert_eq!(
read.get("empty"),
Some(&AttrValue::AsciiString(String::new()))
);
assert_eq!(
read.get("longest"),
Some(&AttrValue::StringArray(vec!["m/s".into(), "kg".into()]))
);
}
fn decode_raw(
datatype: Datatype,
raw_data: Vec<u8>,
dimensions: Vec<u64>,
) -> Option<AttrValue> {
use crate::dataspace::{Dataspace, DataspaceType};
let scalar = dimensions.is_empty();
let attr = crate::attribute::AttributeMessage {
name: "a".into(),
datatype,
dataspace: Dataspace {
space_type: if scalar {
DataspaceType::Scalar
} else {
DataspaceType::Simple
},
#[expect(clippy::cast_possible_truncation)]
rank: dimensions.len() as u8,
dimensions,
max_dimensions: None,
},
raw_data,
datatype_location: crate::shared_message::DatatypeLocation::Inline,
};
decode_attr_value(
&attr,
&crate::source::BytesSource::new(Vec::new()),
8,
8,
BaseAddress::ZERO,
)
}
#[test]
fn a_slot_narrower_than_its_decoded_text_is_not_reported_as_padded() {
let value = decode_raw(
Datatype::String {
size: 2,
padding: crate::datatype::StringPadding::NullPad,
charset: crate::datatype::CharacterSet::Ascii,
},
vec![0xB0, b'C'],
vec![],
)
.expect("a fixed ASCII attribute decodes");
assert_eq!(
value,
AttrValue::AsciiString("\u{FFFD}C".into()),
"a slot its own text does not fit must not claim a width"
);
assert!(
value.as_str().is_some_and(|s| s.len() > 2),
"the case only bites because the decoded text outgrew the slot"
);
}
#[test]
fn an_enum_attribute_decodes_as_its_base_type() {
let h5py_bool =
crate::type_builders::EnumTypeBuilder::with_base(crate::type_builders::make_i8_type())
.value("FALSE", 0)
.value("TRUE", 1)
.build()
.unwrap();
assert_eq!(
decode_raw(h5py_bool.clone(), vec![1], vec![]),
Some(AttrValue::I8(1))
);
assert_eq!(
decode_raw(h5py_bool, vec![1, 0, 1], vec![3]),
Some(AttrValue::I8Array(vec![1, 0, 1]))
);
}
#[test]
fn an_unsigned_enum_attribute_keeps_its_base_signedness_and_width() {
let mode =
crate::type_builders::EnumTypeBuilder::with_base(crate::type_builders::make_u16_type())
.value("low", 1)
.value("high", 40_000)
.build()
.unwrap();
assert_eq!(
decode_raw(mode, 40_000_u16.to_le_bytes().to_vec(), vec![]),
Some(AttrValue::U16(40_000))
);
}
#[test]
fn every_float_variant_round_trips_to_itself() {
let cases = vec![
("f64_scalar", AttrValue::F64(1.5)),
("f64_one", AttrValue::F64Array(vec![1.5])),
("f64_two", AttrValue::F64Array(vec![1.5, 2.5])),
("f32_scalar", AttrValue::F32(1.5)),
("f32_one", AttrValue::F32Array(vec![1.5])),
("f32_two", AttrValue::F32Array(vec![f32::MIN, f32::MAX])),
(
"f32_edges",
AttrValue::F32Array(vec![
f32::MIN_POSITIVE,
f32::from_bits(1),
f32::EPSILON,
-0.0,
f32::INFINITY,
]),
),
];
let read = round_trip(&cases);
for (name, written) in &cases {
assert_eq!(read.get(*name), Some(written), "attribute {name}");
}
}
#[test]
fn a_full_range_unsigned_value_survives_at_every_length() {
let read = round_trip(&[
("scalar", AttrValue::U64(u64::MAX)),
("one", AttrValue::U64Array(vec![u64::MAX])),
("two", AttrValue::U64Array(vec![u64::MAX, 1])),
]);
assert_eq!(read.get("scalar"), Some(&AttrValue::U64(u64::MAX)));
assert_eq!(read.get("one"), Some(&AttrValue::U64Array(vec![u64::MAX])));
assert_eq!(
read.get("two"),
Some(&AttrValue::U64Array(vec![u64::MAX, 1]))
);
for (name, expected) in [
("scalar", vec![u64::MAX]),
("one", vec![u64::MAX]),
("two", vec![u64::MAX, 1]),
] {
let value = read.get(name).expect("present");
assert_eq!(value.to_u64s(), Some(expected), "{name} must read unsigned");
assert_eq!(
value.to_i64s(),
None,
"{name} does not fit an i64 and must not wrap"
);
}
assert_eq!(read["scalar"].as_u64(), Some(u64::MAX));
assert_eq!(read["one"].as_u64(), Some(u64::MAX));
assert_eq!(
read["two"].as_u64(),
None,
"two elements are not a single value"
);
}
#[test]
fn only_a_sequence_of_ascii_chars_claims_the_matlab_shape() {
use crate::datatype::{CharacterSet, Datatype, DatatypeByteOrder, StringPadding};
let char_base = Datatype::String {
size: 1,
padding: StringPadding::NullTerminate,
charset: CharacterSet::Ascii,
};
let int_base = Datatype::FixedPoint {
size: 1,
byte_order: DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 8,
};
assert_eq!(
vlen_string_shape(false, &char_base, None),
VlenStringShape::AsciiCharSequence
);
assert_eq!(
vlen_string_shape(true, &char_base, Some(&CharacterSet::Ascii)),
VlenStringShape::Ascii
);
assert_eq!(
vlen_string_shape(true, &char_base, Some(&CharacterSet::Utf8)),
VlenStringShape::Utf8
);
assert_eq!(
vlen_string_shape(true, &int_base, Some(&CharacterSet::Ascii)),
VlenStringShape::Ascii
);
assert_eq!(
vlen_string_shape(true, &int_base, Some(&CharacterSet::Utf8)),
VlenStringShape::Utf8
);
assert_eq!(
vlen_string_shape(true, &int_base, None),
VlenStringShape::Utf8
);
}
#[test]
fn an_empty_string_attribute_is_not_dropped() {
let cases = vec![
("utf8", AttrValue::String(std::string::String::new())),
("ascii", AttrValue::AsciiString(std::string::String::new())),
];
let read = round_trip(&cases);
for (name, written) in &cases {
assert_eq!(
read.get(*name),
Some(written),
"attribute {name} must survive with its empty value"
);
}
}
#[test]
fn every_integer_width_round_trips_to_itself() {
let cases = vec![
("i8", AttrValue::I8(-7)),
("i8_one", AttrValue::I8Array(vec![-7])),
("i8_two", AttrValue::I8Array(vec![i8::MIN, i8::MAX])),
("i16", AttrValue::I16(-7)),
("i16_one", AttrValue::I16Array(vec![-7])),
("i16_two", AttrValue::I16Array(vec![i16::MIN, i16::MAX])),
("i32", AttrValue::I32(-7)),
("i32_one", AttrValue::I32Array(vec![-7])),
("i32_two", AttrValue::I32Array(vec![i32::MIN, i32::MAX])),
("u8", AttrValue::U8(7)),
("u8_one", AttrValue::U8Array(vec![7])),
("u8_two", AttrValue::U8Array(vec![0, u8::MAX])),
("u16", AttrValue::U16(7)),
("u16_one", AttrValue::U16Array(vec![7])),
("u16_two", AttrValue::U16Array(vec![0, u16::MAX])),
("u32", AttrValue::U32(7)),
("u32_one", AttrValue::U32Array(vec![7])),
("u32_two", AttrValue::U32Array(vec![0, u32::MAX])),
("i64", AttrValue::I64(-7)),
("i64_one", AttrValue::I64Array(vec![-7])),
("i64_two", AttrValue::I64Array(vec![i64::MIN, i64::MAX])),
("u64", AttrValue::U64(7)),
("u64_one", AttrValue::U64Array(vec![7])),
("u64_two", AttrValue::U64Array(vec![0, u64::MAX])),
];
let read = round_trip(&cases);
for (name, written) in &cases {
assert_eq!(read.get(*name), Some(written), "attribute {name}");
}
}
#[test]
fn a_width_with_no_rust_integer_widens() {
let three_byte = Datatype::FixedPoint {
size: 3,
byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 24,
};
assert_eq!(
decode_raw(three_byte.clone(), vec![0xFF, 0xFF, 0xFF], vec![]),
Some(AttrValue::U64(0x00FF_FFFF))
);
assert_eq!(
decode_raw(three_byte, vec![1, 0, 0, 2, 0, 0], vec![2]),
Some(AttrValue::U64Array(vec![1, 2]))
);
}
#[test]
fn a_value_outside_its_declared_width_is_not_wrapped() {
let overwide = Datatype::FixedPoint {
size: 1,
byte_order: crate::datatype::DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 16,
};
assert_eq!(
decode_raw(overwide.clone(), vec![0xFF], vec![]),
Some(AttrValue::I64(255)),
"a scalar past `i8` must widen, not wrap"
);
assert_eq!(
decode_raw(overwide, vec![0x01, 0xFF], vec![2]),
Some(AttrValue::I64Array(vec![1, 255])),
"one element past `i8` widens the whole array, since a mixed answer \
would carry two different meanings for one attribute"
);
}
#[test]
fn accessors_span_the_shapes_the_reader_now_distinguishes() {
let read = round_trip(&[
("scalar", AttrValue::AsciiString("double".into())),
("one", AttrValue::StringArray(vec!["double".into()])),
(
"vlen",
AttrValue::VarLenAsciiCharArray(vec!["double".into()]),
),
("vlen_std", AttrValue::VarLenAsciiString("double".into())),
(
"vlen_std_one",
AttrValue::VarLenStringArray(vec!["double".into()]),
),
]);
for name in ["scalar", "one", "vlen", "vlen_std", "vlen_std_one"] {
assert_eq!(
read.get(name).and_then(AttrValue::as_str),
Some("double"),
"attribute {name}"
);
}
}
}
#[cfg(all(test, feature = "std"))]
mod display_tests {
use super::*;
use crate::datatype::{CharacterSet, Datatype, DatatypeByteOrder, StringPadding};
#[test]
fn an_array_shape_is_not_a_debug_slice() {
let dtype = DType::Array(Box::new(DType::F32), vec![2, 3]);
assert_eq!(dtype.to_string(), "array<f32, 2x3>");
assert_eq!(
DType::Array(Box::new(DType::U8), vec![4]).to_string(),
"array<u8, 4>"
);
}
#[test]
fn an_unclassified_type_carries_the_type_and_writes_a_summary() {
let vax = Datatype::FloatingPoint {
size: 4,
byte_order: DatatypeByteOrder::Vax,
bit_offset: 0,
bit_precision: 32,
exponent_location: 23,
exponent_size: 8,
mantissa_location: 0,
mantissa_size: 23,
exponent_bias: 127,
};
assert_eq!(classify_datatype(&vax), DType::F32);
let time = Datatype::Time {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
bit_precision: 32,
};
let classified = classify_datatype(&time);
assert_eq!(classified, DType::Other(Box::new(time.clone())));
assert_eq!(classified.to_string(), "other(time32)");
let opaque = Datatype::Opaque {
size: 3,
tag: b"rgb".to_vec(),
};
assert_eq!(
classify_datatype(&opaque).to_string(),
"other(opaque[3] \"rgb\")",
"not `other(Opaque {{ size: 3, tag: [114, 103, 98] }})`"
);
}
#[test]
fn a_curated_member_name_is_escaped_in_either_variant() {
let compound = DType::Compound(vec![("a\nb".into(), DType::I32)]).to_string();
assert!(!compound.chars().any(char::is_control), "{compound}");
assert_eq!(compound, "compound{a\\nb: i32}");
let enumeration = DType::Enum(vec!["a\u{1b}[31mb".into()]).to_string();
assert!(!enumeration.chars().any(char::is_control), "{enumeration}");
assert_eq!(enumeration, "enum[a\\u{1b}[31mb]");
}
#[test]
fn a_curated_member_list_is_elided_in_either_variant() {
let over_cap = DISPLAY_MAX_MEMBERS + 2;
let names: Vec<String> = (0..over_cap).map(|i| format!("m{i}")).collect();
let compound = DType::Compound(
names
.iter()
.map(|name| (name.clone(), DType::I32))
.collect(),
);
let enumeration = DType::Enum(names);
for (dtype, close) in [(compound, "}"), (enumeration, "]")] {
let shown = dtype.to_string();
assert!(shown.ends_with(&format!(", … 2 more{close}")), "{shown}");
assert!(
!shown.contains(&format!("m{DISPLAY_MAX_MEMBERS}")),
"{shown}"
);
}
}
#[test]
fn dtype_and_datatype_agree_on_the_names_they_share() {
let identical = [
Datatype::FixedPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
signed: true,
bit_offset: 0,
bit_precision: 32,
},
Datatype::FloatingPoint {
size: 4,
byte_order: DatatypeByteOrder::LittleEndian,
bit_offset: 0,
bit_precision: 32,
exponent_location: 23,
exponent_size: 8,
mantissa_location: 0,
mantissa_size: 23,
exponent_bias: 127,
},
];
for datatype in identical {
assert_eq!(
classify_datatype(&datatype).to_string(),
datatype.to_string()
);
}
let string = Datatype::String {
size: 8,
padding: StringPadding::NullPad,
charset: CharacterSet::Ascii,
};
assert_eq!(classify_datatype(&string).to_string(), "string");
assert_eq!(string.to_string(), "string[8] ascii null-pad");
assert!(
string
.to_string()
.starts_with(&classify_datatype(&string).to_string()),
"the longer spelling still opens with the shorter one"
);
}
}