feat: add Virtual Dataset (VDS) write support and round-trip tests
Add `virtual_sources: Option<Vec<VdsMapping>>` field and `with_virtual_sources()` method to `DatasetBuilder`. In `FileWriter::finish()`, VDS datasets skip raw-data storage and instead serialize their source mappings into a global heap collection (version-1 same-file encoding) referenced by an HDF5 v4 layout-class-3 message. The two-pass address-computation loop handles VDS in both passes: pass 1 computes the fixed-size OH and pre-builds the heap blob; pass 2 places the blob at the correct file offset and rebuilds the OH with the real global heap address. Three new tests verify: (a) same-file two-source round-trip with mapping verification, (b) external-file source encoding, and (c) empty mapping list. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bae80d030b
commit
28a0dc3384
@@ -8,6 +8,7 @@ use alloc::{string::String, string::ToString, vec, vec::Vec};
|
||||
|
||||
use crate::attribute::AttributeMessage;
|
||||
use crate::chunked_write::{ChunkOptions, build_chunked_data_at_ext};
|
||||
use crate::data_layout::VdsMapping;
|
||||
use crate::dataspace::{Dataspace, DataspaceType};
|
||||
use crate::error::FormatError;
|
||||
use crate::link_message::{LinkMessage, LinkTarget};
|
||||
@@ -790,6 +791,118 @@ fn serialize_attribute_info(fh_addr: u64, btree_name_addr: u64) -> Vec<u8> {
|
||||
data
|
||||
}
|
||||
|
||||
// ---- VDS helpers ----
|
||||
|
||||
/// Serialize VDS mappings into the on-disk format stored in the global heap.
|
||||
///
|
||||
/// Layout (version 1, same-file references):
|
||||
/// ```text
|
||||
/// version(1) · nused(8, LE) · entry[nused]
|
||||
/// ```
|
||||
/// Each entry: same-file marker(0x04) · source_dataset\0 · source_sel · virtual_sel
|
||||
pub(crate) fn serialize_vds_mappings(mappings: &[VdsMapping]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.push(1u8); // block version 1 (same-file marker for "." source_file)
|
||||
buf.extend_from_slice(&(mappings.len() as u64).to_le_bytes()); // nused
|
||||
for m in mappings {
|
||||
if m.source_file == "." {
|
||||
buf.push(0x04); // same-file marker
|
||||
} else {
|
||||
buf.extend_from_slice(m.source_file.as_bytes());
|
||||
buf.push(0); // null terminator
|
||||
}
|
||||
buf.extend_from_slice(m.source_dataset.as_bytes());
|
||||
buf.push(0); // null terminator
|
||||
buf.extend_from_slice(&m.source_selection);
|
||||
buf.extend_from_slice(&m.virtual_selection);
|
||||
}
|
||||
// Checksum (4 bytes at end; parsers skip it after reaching nused entries)
|
||||
let cksum = crate::checksum::jenkins_lookup3(&buf);
|
||||
buf.extend_from_slice(&cksum.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
/// Build a minimal global heap collection containing a single object.
|
||||
///
|
||||
/// Returns the serialized collection bytes. The object index is always 1.
|
||||
///
|
||||
/// Global heap collection layout:
|
||||
/// ```text
|
||||
/// "GCOL"(4) · version(1) · reserved(3) · collection_size(8)
|
||||
/// · [index(2) · ref_count(2) · reserved(4) · object_size(8) · data · padding]
|
||||
/// · free-space-marker(2)
|
||||
/// ```
|
||||
pub(crate) fn build_global_heap_collection(object_data: &[u8]) -> Vec<u8> {
|
||||
let ls = LENGTH_SIZE as usize;
|
||||
let header_size = 8 + ls; // sig(4)+ver(1)+rsv(3)+coll_size(ls)
|
||||
let obj_header_size = 8 + ls; // idx(2)+rc(2)+rsv(4)+obj_size(ls)
|
||||
let padded_data_len = pad8(object_data.len());
|
||||
let free_marker_size = 2;
|
||||
let collection_size = header_size + obj_header_size + padded_data_len + free_marker_size;
|
||||
|
||||
let mut buf = Vec::with_capacity(collection_size);
|
||||
buf.extend_from_slice(b"GCOL");
|
||||
buf.push(1); // version
|
||||
buf.extend_from_slice(&[0u8; 3]); // reserved
|
||||
buf.extend_from_slice(&(collection_size as u64).to_le_bytes()); // collection_size
|
||||
|
||||
// Object 1
|
||||
buf.extend_from_slice(&1u16.to_le_bytes()); // index
|
||||
buf.extend_from_slice(&1u16.to_le_bytes()); // reference count
|
||||
buf.extend_from_slice(&[0u8; 4]); // reserved
|
||||
buf.extend_from_slice(&(object_data.len() as u64).to_le_bytes()); // object size
|
||||
buf.extend_from_slice(object_data);
|
||||
// Pad object data to 8-byte boundary
|
||||
let pad = padded_data_len - object_data.len();
|
||||
buf.extend_from_slice(&vec![0u8; pad]);
|
||||
|
||||
// Free space marker
|
||||
buf.extend_from_slice(&0u16.to_le_bytes());
|
||||
|
||||
debug_assert_eq!(buf.len(), collection_size);
|
||||
buf
|
||||
}
|
||||
|
||||
/// Round up to the next multiple of 8.
|
||||
fn pad8(x: usize) -> usize {
|
||||
(x + 7) & !7
|
||||
}
|
||||
|
||||
/// Build a Virtual Dataset object header.
|
||||
///
|
||||
/// The layout message for a VDS dataset is:
|
||||
/// ```text
|
||||
/// version(1=4) · class(1=3) · global_heap_address(8) · global_heap_index(4)
|
||||
/// ```
|
||||
pub(crate) fn build_vds_dataset_oh(
|
||||
dt: &Datatype,
|
||||
ds: &Dataspace,
|
||||
global_heap_addr: u64,
|
||||
attrs: &[AttributeMessage],
|
||||
dense_blob: Option<&DenseAttrBlob>,
|
||||
fill_time: FillTime,
|
||||
) -> Vec<u8> {
|
||||
let mut w = ObjectHeaderWriter::new();
|
||||
w.add_message_with_flags(MessageType::Datatype, dt.serialize(), 0x01);
|
||||
w.add_message(MessageType::Dataspace, ds.serialize(LENGTH_SIZE));
|
||||
w.add_message_with_flags(MessageType::FillValue, vec![3, fill_time.to_byte()], 0x01);
|
||||
// VDS layout message: version=4, class=3, global_heap_address(8), global_heap_index=1(4)
|
||||
let mut dl = Vec::new();
|
||||
dl.push(4u8); // version
|
||||
dl.push(3u8); // class = virtual
|
||||
dl.extend_from_slice(&global_heap_addr.to_le_bytes());
|
||||
dl.extend_from_slice(&1u32.to_le_bytes()); // object index 1 in the collection
|
||||
w.add_message(MessageType::DataLayout, dl);
|
||||
if let Some(blob) = dense_blob {
|
||||
w.add_message(MessageType::AttributeInfo, blob.attr_info_message.clone());
|
||||
} else {
|
||||
for attr in attrs {
|
||||
w.add_message(MessageType::Attribute, attr.serialize(LENGTH_SIZE));
|
||||
}
|
||||
}
|
||||
w.serialize()
|
||||
}
|
||||
|
||||
fn write_offset(buf: &mut Vec<u8>, val: u64, offset_size: u8) {
|
||||
match offset_size {
|
||||
2 => buf.extend_from_slice(&(val as u16).to_le_bytes()),
|
||||
@@ -879,6 +992,8 @@ impl FileWriter {
|
||||
fill_time: FillTime,
|
||||
compact: bool,
|
||||
alignment: usize,
|
||||
/// VDS source mappings (set for Virtual datasets).
|
||||
virtual_sources: Option<Vec<VdsMapping>>,
|
||||
}
|
||||
struct GrpFlat {
|
||||
name: String,
|
||||
@@ -886,14 +1001,18 @@ impl FileWriter {
|
||||
ds_indices: Vec<usize>,
|
||||
}
|
||||
|
||||
let mut all_ds: Vec<DsFlat> = Vec::new();
|
||||
let mut groups: Vec<GrpFlat> = Vec::new();
|
||||
let mut root_ds_indices: Vec<usize> = Vec::new();
|
||||
|
||||
for db in self.root_datasets {
|
||||
// Helper: convert a DatasetBuilder into DsFlat, handling VDS (which
|
||||
// does not require a `data` field).
|
||||
let flatten_ds = |db: DatasetBuilder| -> Result<DsFlat, FormatError> {
|
||||
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
||||
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
||||
let raw = db.data.ok_or(FormatError::DatasetMissingData)?;
|
||||
let is_vds = db.virtual_sources.is_some();
|
||||
let raw = if is_vds {
|
||||
// VDS datasets have no raw data stored in this file.
|
||||
db.data.unwrap_or_default()
|
||||
} else {
|
||||
db.data.ok_or(FormatError::DatasetMissingData)?
|
||||
};
|
||||
let max_dimensions = db.maxshape.clone();
|
||||
let dspace = Dataspace {
|
||||
space_type: if shape.is_empty() {
|
||||
@@ -918,8 +1037,7 @@ impl FileWriter {
|
||||
};
|
||||
attrs.extend(p.build_attrs(&raw));
|
||||
}
|
||||
root_ds_indices.push(all_ds.len());
|
||||
all_ds.push(DsFlat {
|
||||
Ok(DsFlat {
|
||||
name: db.name,
|
||||
dt,
|
||||
ds: dspace,
|
||||
@@ -930,7 +1048,17 @@ impl FileWriter {
|
||||
fill_time: db.fill_time,
|
||||
compact: db.compact,
|
||||
alignment: db.alignment,
|
||||
});
|
||||
virtual_sources: db.virtual_sources,
|
||||
})
|
||||
};
|
||||
|
||||
let mut all_ds: Vec<DsFlat> = Vec::new();
|
||||
let mut groups: Vec<GrpFlat> = Vec::new();
|
||||
let mut root_ds_indices: Vec<usize> = Vec::new();
|
||||
|
||||
for db in self.root_datasets {
|
||||
root_ds_indices.push(all_ds.len());
|
||||
all_ds.push(flatten_ds(db)?);
|
||||
}
|
||||
|
||||
for g in self.groups.into_iter() {
|
||||
@@ -940,46 +1068,8 @@ impl FileWriter {
|
||||
}
|
||||
let mut ds_idx = Vec::new();
|
||||
for db in g.datasets {
|
||||
let dt = db.datatype.ok_or(FormatError::DatasetMissingData)?;
|
||||
let shape = db.shape.ok_or(FormatError::DatasetMissingShape)?;
|
||||
let raw = db.data.ok_or(FormatError::DatasetMissingData)?;
|
||||
let max_dimensions = db.maxshape.clone();
|
||||
let dspace = Dataspace {
|
||||
space_type: if shape.is_empty() {
|
||||
DataspaceType::Scalar
|
||||
} else {
|
||||
DataspaceType::Simple
|
||||
},
|
||||
rank: shape.len() as u8,
|
||||
dimensions: shape,
|
||||
max_dimensions,
|
||||
};
|
||||
let mut attrs = Vec::new();
|
||||
for (n, v) in &db.attrs {
|
||||
attrs.push(build_attr_message(n, v));
|
||||
}
|
||||
#[cfg(feature = "provenance")]
|
||||
if let Some(ref prov) = db.provenance {
|
||||
let p = crate::provenance::Provenance {
|
||||
creator: prov.creator.clone(),
|
||||
timestamp: prov.timestamp.clone(),
|
||||
source: prov.source.clone(),
|
||||
};
|
||||
attrs.extend(p.build_attrs(&raw));
|
||||
}
|
||||
ds_idx.push(all_ds.len());
|
||||
all_ds.push(DsFlat {
|
||||
name: db.name,
|
||||
dt,
|
||||
ds: dspace,
|
||||
raw,
|
||||
attrs,
|
||||
chunk_options: db.chunk_options,
|
||||
maxshape: db.maxshape,
|
||||
fill_time: db.fill_time,
|
||||
compact: db.compact,
|
||||
alignment: db.alignment,
|
||||
});
|
||||
all_ds.push(flatten_ds(db)?);
|
||||
}
|
||||
groups.push(GrpFlat {
|
||||
name: g.name,
|
||||
@@ -993,15 +1083,20 @@ impl FileWriter {
|
||||
root_attrs.push(build_attr_message(n, v));
|
||||
}
|
||||
|
||||
let is_vds: Vec<bool> = all_ds
|
||||
.iter()
|
||||
.map(|d| d.virtual_sources.is_some())
|
||||
.collect();
|
||||
let is_chunked: Vec<bool> = all_ds
|
||||
.iter()
|
||||
.map(|d| d.chunk_options.is_chunked() || d.maxshape.is_some())
|
||||
.enumerate()
|
||||
.map(|(i, d)| !is_vds[i] && (d.chunk_options.is_chunked() || d.maxshape.is_some()))
|
||||
.collect();
|
||||
// Determine which datasets use compact storage
|
||||
let is_compact: Vec<bool> = all_ds
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, d)| !is_chunked[i] && d.compact && d.raw.len() <= 65535)
|
||||
.map(|(i, d)| !is_vds[i] && !is_chunked[i] && d.compact && d.raw.len() <= 65535)
|
||||
.collect();
|
||||
let root_dense = root_attrs.len() > DENSE_ATTR_THRESHOLD;
|
||||
let group_dense: Vec<bool> = groups
|
||||
@@ -1065,7 +1160,31 @@ impl FileWriter {
|
||||
let mut dummy_blobs: Vec<DataBlob> = Vec::new();
|
||||
let mut dummy_cursor = 0u64;
|
||||
for (i, d) in all_ds.iter().enumerate() {
|
||||
if is_chunked[i] {
|
||||
if is_vds[i] {
|
||||
// VDS: dummy OH with address 0 to get the OH size. The global
|
||||
// heap blob will be placed after the OHs in pass 2.
|
||||
let dense_blob = if ds_dense[i] {
|
||||
Some(build_dense_attrs(&d.attrs, 0))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let oh = build_vds_dataset_oh(
|
||||
&d.dt,
|
||||
&d.ds,
|
||||
0, // dummy address
|
||||
&d.attrs,
|
||||
dense_blob.as_ref(),
|
||||
d.fill_time,
|
||||
);
|
||||
// Global heap blob size is address-independent; compute it now
|
||||
// so pass 2 can place it correctly.
|
||||
let vds_mappings = d.virtual_sources.as_deref().unwrap_or(&[]);
|
||||
let gcol_bytes = build_global_heap_collection(&serialize_vds_mappings(vds_mappings));
|
||||
dummy_blobs.push(DataBlob {
|
||||
data: gcol_bytes, // store heap blob here temporarily
|
||||
oh_bytes: oh,
|
||||
});
|
||||
} else if is_chunked[i] {
|
||||
let chunk_dims = d.chunk_options.resolve_chunk_dims(&d.ds.dimensions);
|
||||
let elem_size = d.dt.type_size() as usize;
|
||||
let result = build_chunked_data_at_ext(
|
||||
@@ -1214,7 +1333,25 @@ impl FileWriter {
|
||||
let global_align_threshold = self.alignment_threshold;
|
||||
let global_align_bytes = self.alignment_bytes;
|
||||
for (i, d) in all_ds.iter().enumerate() {
|
||||
if is_chunked[i] {
|
||||
if is_vds[i] {
|
||||
// VDS: place the global heap collection right after the OHs,
|
||||
// then rebuild the OH with the real heap address.
|
||||
let gcol_bytes = &dummy_blobs[i].data; // pre-computed in pass 1
|
||||
let heap_addr = cursor2 as u64;
|
||||
cursor2 += gcol_bytes.len();
|
||||
let oh = build_vds_dataset_oh(
|
||||
&d.dt,
|
||||
&d.ds,
|
||||
heap_addr,
|
||||
&d.attrs,
|
||||
ds_dense_blobs[i].as_ref(),
|
||||
d.fill_time,
|
||||
);
|
||||
ds_blobs2.push(DataBlob {
|
||||
data: gcol_bytes.clone(),
|
||||
oh_bytes: oh,
|
||||
});
|
||||
} else if is_chunked[i] {
|
||||
let chunk_dims = d.chunk_options.resolve_chunk_dims(&d.ds.dimensions);
|
||||
let elem_size = d.dt.type_size() as usize;
|
||||
let base_address = cursor2 as u64;
|
||||
@@ -1707,4 +1844,200 @@ mod tests {
|
||||
let err = finalize_parallel(vec![b0, b1]).unwrap_err();
|
||||
assert!(matches!(err, FormatError::DuplicateDatasetName(_)));
|
||||
}
|
||||
|
||||
// ---- Virtual Dataset (VDS) round-trip tests ----
|
||||
|
||||
/// Serialize an H5S ALL selection (type=3, version=1, 16 bytes).
|
||||
fn sel_all() -> Vec<u8> {
|
||||
vec![
|
||||
3, 0, 0, 0, // type = ALL
|
||||
1, 0, 0, 0, // version
|
||||
0, 0, 0, 0, // reserved
|
||||
0, 0, 0, 0, // length (unused for ALL)
|
||||
]
|
||||
}
|
||||
|
||||
/// Serialize an H5S HYPER selection (version 3, rank 1, enc_size 2).
|
||||
/// Encodes start=`start`, stride=1, count=1, block=`block`.
|
||||
fn sel_hyper_1d(start: u16, block: u16) -> Vec<u8> {
|
||||
let mut v = vec![
|
||||
2, 0, 0, 0, // type = HYPER
|
||||
3, 0, 0, 0, // version 3
|
||||
0x01, // flags = regular
|
||||
0x02, // enc_size = 2 (u16 per coordinate)
|
||||
1, 0, 0, 0, // rank = 1
|
||||
];
|
||||
v.extend_from_slice(&start.to_le_bytes()); // start
|
||||
v.extend_from_slice(&1u16.to_le_bytes()); // stride
|
||||
v.extend_from_slice(&1u16.to_le_bytes()); // count
|
||||
v.extend_from_slice(&block.to_le_bytes()); // block
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vds_write_read_virtual_layout() {
|
||||
use crate::data_layout::DataLayout;
|
||||
|
||||
// A virtual dataset /vds of shape [8] backed by two same-file sources:
|
||||
// /src_a maps to virtual[0:4] and /src_b maps to virtual[4:8].
|
||||
let mapping_a = VdsMapping {
|
||||
source_file: ".".into(),
|
||||
source_dataset: "src_a".into(),
|
||||
source_selection: sel_all(),
|
||||
virtual_selection: sel_hyper_1d(0, 4),
|
||||
};
|
||||
let mapping_b = VdsMapping {
|
||||
source_file: ".".into(),
|
||||
source_dataset: "src_b".into(),
|
||||
source_selection: sel_all(),
|
||||
virtual_selection: sel_hyper_1d(4, 4),
|
||||
};
|
||||
|
||||
let mut fw = FileWriter::new();
|
||||
// Source datasets (real data in this file)
|
||||
fw.create_dataset("src_a").with_f64_data(&[1.0, 2.0, 3.0, 4.0]);
|
||||
fw.create_dataset("src_b").with_f64_data(&[5.0, 6.0, 7.0, 8.0]);
|
||||
// Virtual dataset
|
||||
fw.create_dataset("vds")
|
||||
.with_shape(&[8])
|
||||
.with_f64_data(&[]) // shape hint; raw data is ignored for VDS
|
||||
.with_virtual_sources(vec![mapping_a, mapping_b]);
|
||||
|
||||
let bytes = fw.finish().unwrap();
|
||||
|
||||
// Verify the virtual dataset resolves to DataLayout::Virtual
|
||||
let sig = signature::find_signature(&bytes).unwrap();
|
||||
let sb = Superblock::parse(&bytes, sig).unwrap();
|
||||
let vds_addr = resolve_path_any(&bytes, &sb, "vds").unwrap();
|
||||
let hdr = ObjectHeader::parse(
|
||||
&bytes,
|
||||
vds_addr as usize,
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let dl_data = &hdr
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| m.msg_type == MessageType::DataLayout)
|
||||
.unwrap()
|
||||
.data;
|
||||
|
||||
let mut layout =
|
||||
DataLayout::parse(dl_data, sb.offset_size, sb.length_size).unwrap();
|
||||
|
||||
// Before resolution, mappings field is empty.
|
||||
assert!(
|
||||
matches!(layout, DataLayout::Virtual { .. }),
|
||||
"expected Virtual layout, got {layout:?}"
|
||||
);
|
||||
|
||||
// Resolve VDS mappings from the global heap.
|
||||
layout.resolve_vds_mappings(&bytes, sb.length_size).unwrap();
|
||||
|
||||
match &layout {
|
||||
DataLayout::Virtual { mappings, .. } => {
|
||||
assert_eq!(mappings.len(), 2, "expected 2 VDS mappings");
|
||||
assert_eq!(mappings[0].source_file, ".");
|
||||
assert_eq!(mappings[0].source_dataset, "src_a");
|
||||
assert_eq!(mappings[1].source_file, ".");
|
||||
assert_eq!(mappings[1].source_dataset, "src_b");
|
||||
|
||||
// Verify the virtual selections cover [0:4] and [4:8].
|
||||
use crate::selection::Selection;
|
||||
let (vsel_a, _) =
|
||||
Selection::decode_serialized(&mappings[0].virtual_selection).unwrap();
|
||||
let (vsel_b, _) =
|
||||
Selection::decode_serialized(&mappings[1].virtual_selection).unwrap();
|
||||
assert_eq!(vsel_a.iter_linear_1d(8).unwrap(), vec![0, 1, 2, 3]);
|
||||
assert_eq!(vsel_b.iter_linear_1d(8).unwrap(), vec![4, 5, 6, 7]);
|
||||
}
|
||||
other => panic!("expected Virtual layout after resolution, got {other:?}"),
|
||||
}
|
||||
|
||||
// Source datasets still readable normally.
|
||||
assert_eq!(read_dataset_f64(&bytes, "src_a"), vec![1.0, 2.0, 3.0, 4.0]);
|
||||
assert_eq!(read_dataset_f64(&bytes, "src_b"), vec![5.0, 6.0, 7.0, 8.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vds_external_source_file() {
|
||||
use crate::data_layout::DataLayout;
|
||||
|
||||
// A VDS mapping referencing an external file ("other.h5").
|
||||
let mapping_ext = VdsMapping {
|
||||
source_file: "other.h5".into(),
|
||||
source_dataset: "data".into(),
|
||||
source_selection: sel_all(),
|
||||
virtual_selection: sel_all(),
|
||||
};
|
||||
|
||||
let mut fw = FileWriter::new();
|
||||
fw.create_dataset("ext_vds")
|
||||
.with_shape(&[10])
|
||||
.with_f64_data(&[]) // shape hint only
|
||||
.with_virtual_sources(vec![mapping_ext]);
|
||||
|
||||
let bytes = fw.finish().unwrap();
|
||||
|
||||
let sig = signature::find_signature(&bytes).unwrap();
|
||||
let sb = Superblock::parse(&bytes, sig).unwrap();
|
||||
let addr = resolve_path_any(&bytes, &sb, "ext_vds").unwrap();
|
||||
let hdr =
|
||||
ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||
let dl_data = &hdr
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| m.msg_type == MessageType::DataLayout)
|
||||
.unwrap()
|
||||
.data;
|
||||
let mut layout =
|
||||
DataLayout::parse(dl_data, sb.offset_size, sb.length_size).unwrap();
|
||||
layout.resolve_vds_mappings(&bytes, sb.length_size).unwrap();
|
||||
|
||||
match &layout {
|
||||
DataLayout::Virtual { mappings, .. } => {
|
||||
assert_eq!(mappings.len(), 1);
|
||||
assert_eq!(mappings[0].source_file, "other.h5");
|
||||
assert_eq!(mappings[0].source_dataset, "data");
|
||||
}
|
||||
other => panic!("expected Virtual, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vds_empty_mapping_list() {
|
||||
use crate::data_layout::DataLayout;
|
||||
|
||||
let mut fw = FileWriter::new();
|
||||
fw.create_dataset("empty_vds")
|
||||
.with_shape(&[0])
|
||||
.with_f64_data(&[])
|
||||
.with_virtual_sources(vec![]);
|
||||
|
||||
let bytes = fw.finish().unwrap();
|
||||
|
||||
let sig = signature::find_signature(&bytes).unwrap();
|
||||
let sb = Superblock::parse(&bytes, sig).unwrap();
|
||||
let addr = resolve_path_any(&bytes, &sb, "empty_vds").unwrap();
|
||||
let hdr =
|
||||
ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||
let dl_data = &hdr
|
||||
.messages
|
||||
.iter()
|
||||
.find(|m| m.msg_type == MessageType::DataLayout)
|
||||
.unwrap()
|
||||
.data;
|
||||
let mut layout =
|
||||
DataLayout::parse(dl_data, sb.offset_size, sb.length_size).unwrap();
|
||||
layout.resolve_vds_mappings(&bytes, sb.length_size).unwrap();
|
||||
|
||||
match &layout {
|
||||
DataLayout::Virtual { mappings, .. } => {
|
||||
assert_eq!(mappings.len(), 0, "expected empty mappings for zero-mapping VDS");
|
||||
}
|
||||
other => panic!("expected Virtual, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user