Files
clawhdf5/crates/clawhdf5-format/tests/reference_tests.rs
Omar Sobh 55959b4920
CI / test (push) Failing after 15s
ci: wire up CI, fix no_std build, fix stale package names in scripts
- Add .gitea/workflows/ci.yml running scripts/ci-test.sh (fmt, clippy,
  test, no_std check) on push/PR to main.
- Fix stale rustyhdf5-py/rustyhdf5-format package names in
  ci-test.sh/check-nostd.sh, which had been silently no-op'ing those
  checks (cargo warns but doesn't fail on an unknown --exclude/-p
  target).
- With those checks actually running, fix the real issues they surface:
  - clippy: useless_conversion in chunked_write.rs, byte_char_slices in
    global_heap.rs/object_header.rs.
  - cargo fmt: apply formatting across the workspace (whitespace only).
  - no_std (thumbv7em-none-eabihf) build errors in clawhdf5-format:
    core::sync::atomic::AtomicU64 doesn't exist on that target (no
    native 64-bit atomics) — switch profiling.rs's counters to
    portable-atomic, which falls back to a CAS-based emulation there
    and is a no-op wrapper elsewhere. Add missing alloc imports for
    Box (filters.rs), Vec (filters_szip.rs), and format! (dict_encoding.rs)
    on no_std paths. Replace f64::powi (std/libm-only) with a small
    local exponentiation-by-squaring helper in the scale-offset filter.
2026-08-05 10:50:13 -07:00

314 lines
9.6 KiB
Rust

//! Integration tests for HDF5 reference type reading.
use clawhdf5_format::data_read::{read_object_references, read_region_references};
use clawhdf5_format::datatype::{Datatype, ReferenceType};
#[test]
fn object_ref_single_valid() {
let dt = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let raw = 4096u64.to_le_bytes().to_vec();
let refs = read_object_references(&raw, &dt, 8).unwrap();
assert_eq!(refs.len(), 1);
assert_eq!(refs[0].address, 4096);
assert!(!refs[0].is_null());
}
#[test]
fn object_ref_null_detection() {
let dt = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let raw = u64::MAX.to_le_bytes().to_vec();
let refs = read_object_references(&raw, &dt, 8).unwrap();
assert_eq!(refs.len(), 1);
assert!(refs[0].is_null());
}
#[test]
fn object_ref_multiple() {
let dt = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let mut raw = Vec::new();
raw.extend_from_slice(&100u64.to_le_bytes());
raw.extend_from_slice(&u64::MAX.to_le_bytes());
raw.extend_from_slice(&200u64.to_le_bytes());
raw.extend_from_slice(&300u64.to_le_bytes());
raw.extend_from_slice(&u64::MAX.to_le_bytes());
let refs = read_object_references(&raw, &dt, 8).unwrap();
assert_eq!(refs.len(), 5);
assert_eq!(refs[0].address, 100);
assert!(refs[1].is_null());
assert_eq!(refs[2].address, 200);
assert_eq!(refs[3].address, 300);
assert!(refs[4].is_null());
}
#[test]
fn object_ref_4byte_offset_size() {
let dt = Datatype::Reference {
size: 4,
ref_type: ReferenceType::Object,
};
let mut raw = Vec::new();
raw.extend_from_slice(&256u32.to_le_bytes());
raw.extend_from_slice(&u32::MAX.to_le_bytes());
let refs = read_object_references(&raw, &dt, 4).unwrap();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].address, 256);
assert!(refs[1].is_null());
}
#[test]
fn object_ref_wrong_type_errors() {
let dt = Datatype::Reference {
size: 12,
ref_type: ReferenceType::DatasetRegion,
};
let raw = vec![0u8; 12];
let err = read_object_references(&raw, &dt, 8).unwrap_err();
assert!(matches!(
err,
clawhdf5_format::error::FormatError::TypeMismatch { .. }
));
}
#[test]
fn object_ref_non_reference_type_errors() {
let dt = Datatype::FixedPoint {
size: 8,
byte_order: clawhdf5_format::datatype::DatatypeByteOrder::LittleEndian,
signed: false,
bit_offset: 0,
bit_precision: 64,
};
let raw = vec![0u8; 8];
let err = read_object_references(&raw, &dt, 8).unwrap_err();
assert!(matches!(
err,
clawhdf5_format::error::FormatError::TypeMismatch { .. }
));
}
#[test]
fn object_ref_size_mismatch() {
let dt = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let raw = vec![0u8; 7]; // not a multiple of 8
let err = read_object_references(&raw, &dt, 8).unwrap_err();
assert!(matches!(
err,
clawhdf5_format::error::FormatError::DataSizeMismatch { .. }
));
}
#[test]
fn region_ref_basic() {
let dt = Datatype::Reference {
size: 12,
ref_type: ReferenceType::DatasetRegion,
};
let raw: Vec<u8> = (0..24).collect();
let refs = read_region_references(&raw, &dt).unwrap();
assert_eq!(refs.len(), 2);
assert_eq!(refs[0].raw, (0u8..12).collect::<Vec<u8>>());
assert_eq!(refs[1].raw, (12u8..24).collect::<Vec<u8>>());
}
#[test]
fn region_ref_wrong_type_errors() {
let dt = Datatype::Reference {
size: 8,
ref_type: ReferenceType::Object,
};
let raw = vec![0u8; 8];
let err = read_region_references(&raw, &dt).unwrap_err();
assert!(matches!(
err,
clawhdf5_format::error::FormatError::TypeMismatch { .. }
));
}
#[test]
fn region_ref_size_mismatch() {
let dt = Datatype::Reference {
size: 12,
ref_type: ReferenceType::DatasetRegion,
};
let raw = vec![0u8; 11]; // not a multiple of 12
let err = read_region_references(&raw, &dt).unwrap_err();
assert!(matches!(
err,
clawhdf5_format::error::FormatError::DataSizeMismatch { .. }
));
}
// ---- h5py integration: read object references from an h5py-created file ----
#[test]
fn h5py_object_reference_roundtrip() {
// Generate an HDF5 file with h5py containing object references
let path = std::env::temp_dir().join("clawhdf5_test_objrefs.h5");
let script = format!(
r#"
import h5py
import numpy as np
f = h5py.File('{}', 'w')
f.create_dataset('target_a', data=[1.0, 2.0, 3.0])
f.create_dataset('target_b', data=[10, 20, 30])
refs = [f['target_a'].ref, f['target_b'].ref]
f.create_dataset('refs', data=refs)
f.close()
print('ok')
"#,
path.display()
);
let output = std::process::Command::new("python3")
.args(["-c", &script])
.output();
let output = match output {
Ok(o) if o.status.success() => o,
_ => {
eprintln!("skipping h5py_object_reference_roundtrip: python3+h5py not available");
return;
}
};
let stdout = String::from_utf8(output.stdout).unwrap();
if !stdout.trim().contains("ok") {
eprintln!("skipping h5py_object_reference_roundtrip: h5py script failed");
return;
}
// Read the file and parse object references
let file_data = std::fs::read(&path).unwrap();
let sig_offset = clawhdf5_format::signature::find_signature(&file_data).unwrap();
let sb = clawhdf5_format::superblock::Superblock::parse(&file_data, sig_offset).unwrap();
let root_oh = clawhdf5_format::object_header::ObjectHeader::parse(
&file_data,
sb.root_group_address as usize,
sb.offset_size,
sb.length_size,
)
.unwrap();
// Find the 'refs' dataset -- handle both v1 (SymbolTable/B-tree) and v2 (Link) groups
let mut refs_addr = None;
// Try v2 Link messages first
for msg in &root_oh.messages {
if msg.msg_type == clawhdf5_format::message_type::MessageType::Link {
let link = clawhdf5_format::link_message::LinkMessage::parse(&msg.data, sb.offset_size)
.unwrap();
if link.name == "refs"
&& let clawhdf5_format::link_message::LinkTarget::Hard {
object_header_address,
} = link.link_target
{
refs_addr = Some(object_header_address);
}
}
}
// Fall back to v1 SymbolTable (B-tree + local heap) used by default h5py files
if refs_addr.is_none() {
for msg in &root_oh.messages {
if msg.msg_type == clawhdf5_format::message_type::MessageType::SymbolTable {
let sym = clawhdf5_format::symbol_table::SymbolTableMessage::parse(
&msg.data,
sb.offset_size,
)
.unwrap();
let entries = clawhdf5_format::group_v1::resolve_v1_group_entries(
&file_data,
&sym,
sb.offset_size,
sb.length_size,
)
.unwrap();
for entry in entries {
if entry.name == "refs" {
refs_addr = Some(entry.object_header_address);
break;
}
}
}
}
}
let refs_addr = refs_addr.expect("'refs' dataset link not found");
let ds_oh = clawhdf5_format::object_header::ObjectHeader::parse(
&file_data,
refs_addr as usize,
sb.offset_size,
sb.length_size,
)
.unwrap();
let mut found_dt = None;
let mut found_ds = None;
let mut found_layout = None;
for msg in &ds_oh.messages {
match msg.msg_type {
clawhdf5_format::message_type::MessageType::Datatype => {
let (dt, _) = clawhdf5_format::datatype::Datatype::parse(&msg.data).unwrap();
found_dt = Some(dt);
}
clawhdf5_format::message_type::MessageType::Dataspace => {
found_ds = Some(
clawhdf5_format::dataspace::Dataspace::parse(&msg.data, sb.length_size)
.unwrap(),
);
}
clawhdf5_format::message_type::MessageType::DataLayout => {
found_layout = Some(
clawhdf5_format::data_layout::DataLayout::parse(
&msg.data,
sb.offset_size,
sb.length_size,
)
.unwrap(),
);
}
_ => {}
}
}
let ref_dt = found_dt.expect("no datatype in refs dataset");
let ref_ds = found_ds.expect("no dataspace in refs dataset");
let ref_layout = found_layout.expect("no layout in refs dataset");
// Verify the datatype is an object reference
match &ref_dt {
Datatype::Reference { ref_type, .. } => {
assert_eq!(*ref_type, ReferenceType::Object);
}
_ => panic!("expected Reference datatype, got {:?}", ref_dt),
}
let raw = clawhdf5_format::data_read::read_raw_data(&file_data, &ref_layout, &ref_ds, &ref_dt)
.unwrap();
let obj_refs = read_object_references(&raw, &ref_dt, sb.offset_size).unwrap();
assert_eq!(obj_refs.len(), 2);
// Both references should be non-null and point to valid addresses
assert!(!obj_refs[0].is_null());
assert!(!obj_refs[1].is_null());
assert_ne!(obj_refs[0].address, obj_refs[1].address);
// Clean up
let _ = std::fs::remove_file(&path);
}