h5rs tools, browser reader, libhdf5 header checks, plugin filters, concurrency benchmark #14
+4
-2
@@ -130,8 +130,10 @@
|
|||||||
files); metadata space is one figure, not broken down.
|
files); metadata space is one figure, not broken down.
|
||||||
- `h5rs diff [-r] [-q] [-d D] [-p R] A B [OBJ1 [OBJ2]]` compares objects,
|
- `h5rs diff [-r] [-q] [-d D] [-p R] A B [OBJ1 [OBJ2]]` compares objects,
|
||||||
kinds, datatypes, shapes, attributes, values and link targets; exit
|
kinds, datatypes, shapes, attributes, values and link targets; exit
|
||||||
status 0/1/2 as h5diff's. Objects that cannot be compared count as a
|
status 0/1/2 as h5diff's. Every path is compared, including every name
|
||||||
difference (h5diff exits 0 for them), and NaN equals NaN.
|
of a hard-linked object and the members of a hard-linked group. Objects
|
||||||
|
that cannot be compared count as a difference (h5diff exits 0 for them),
|
||||||
|
and NaN equals NaN.
|
||||||
- `h5rs check [--data] FILE` is a structural validator: it walks every
|
- `h5rs check [--data] FILE` is a structural validator: it walks every
|
||||||
object, parses every header message, verifies the checksums of every
|
object, parses every header message, verifies the checksums of every
|
||||||
version 2+ structure it meets (superblock, object headers and
|
version 2+ structure it meets (superblock, object headers and
|
||||||
|
|||||||
@@ -145,10 +145,14 @@ $ h5rs diff -p 0.01 a.h5 b.h5 # |a - b| / |a| > 1% is a difference
|
|||||||
Exit status: 0 no differences, 1 differences, 2 error — the same as h5diff's
|
Exit status: 0 no differences, 1 differences, 2 error — the same as h5diff's
|
||||||
on the cases `diff_exit_codes_match_h5diff` runs. Compared: which objects
|
on the cases `diff_exit_codes_match_h5diff` runs. Compared: which objects
|
||||||
exist, their kinds, datatypes and shapes, attribute sets and values, dataset
|
exist, their kinds, datatypes and shapes, attribute sets and values, dataset
|
||||||
values, and soft/external link targets. Two differences from h5diff, on
|
values, and soft/external link targets. Every path is compared: an object
|
||||||
purpose: objects that cannot be compared (different shapes or datatype
|
hard-linked under two names is compared under both, with everything below
|
||||||
classes) count as a difference (h5diff warns and exits 0), and two NaNs are
|
it, so a file that shares one object between two names equals a file that
|
||||||
equal.
|
stores two identical copies. Differences from h5diff, on purpose: objects
|
||||||
|
that cannot be compared (different shapes or datatype classes) count as a
|
||||||
|
difference (h5diff warns and exits 0); two NaNs are equal; and the members
|
||||||
|
of a group reached by a second hard link are compared (h5diff lists them in
|
||||||
|
one file only and exits 1).
|
||||||
|
|
||||||
## `h5rs check`
|
## `h5rs check`
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use std::cell::OnceCell;
|
use std::cell::OnceCell;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use clawhdf5_format::attribute::AttributeMessage;
|
use clawhdf5_format::attribute::AttributeMessage;
|
||||||
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
use clawhdf5_format::dataspace::{Dataspace, DataspaceType};
|
||||||
@@ -9,7 +10,7 @@ use clawhdf5_format::datatype::Datatype;
|
|||||||
use clawhdf5_format::object_header::ObjectHeader;
|
use clawhdf5_format::object_header::ObjectHeader;
|
||||||
|
|
||||||
use crate::cli::{Args, Out};
|
use crate::cli::{Args, Out};
|
||||||
use crate::h5::{H5, Kind, LinkKind};
|
use crate::h5::{H5, Kind, Link, LinkKind};
|
||||||
use crate::value::{self, Decoder, Value};
|
use crate::value::{self, Decoder, Value};
|
||||||
|
|
||||||
pub const USAGE: &str = "\
|
pub const USAGE: &str = "\
|
||||||
@@ -177,25 +178,13 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
} else {
|
} else {
|
||||||
format!("/{base}")
|
format!("/{base}")
|
||||||
};
|
};
|
||||||
let mut entries = BTreeMap::new();
|
let entries = match collect(h5, addr) {
|
||||||
let walk = h5.walk_from(addr, "", &mut |it| {
|
Ok(e) => e,
|
||||||
let e = match (it.link.map(|l| &l.kind), it.header) {
|
Err(e) => {
|
||||||
(Some(LinkKind::Soft(t)), _) => Entry::Soft(t.clone()),
|
writeln!(out.e, "h5rs diff: {f}: {e}")?;
|
||||||
(Some(LinkKind::External { file, path }), _) => {
|
return Ok(2);
|
||||||
Entry::External(file.clone(), path.clone())
|
}
|
||||||
}
|
};
|
||||||
(Some(LinkKind::UserDefined(t)), _) => Entry::UserDefined(*t),
|
|
||||||
(_, Some(Err(e))) => Entry::Broken(e.to_string()),
|
|
||||||
(_, Some(Ok(h))) => Entry::Obj(it.addr.unwrap_or(0), Kind::of(h)),
|
|
||||||
// A second hard link to an object already compared.
|
|
||||||
(_, None) => return,
|
|
||||||
};
|
|
||||||
entries.insert(it.path.to_string(), e);
|
|
||||||
});
|
|
||||||
if let Err(e) = walk {
|
|
||||||
writeln!(out.e, "h5rs diff: {f}: {e}")?;
|
|
||||||
return Ok(2);
|
|
||||||
}
|
|
||||||
sides.push(Side {
|
sides.push(Side {
|
||||||
h5,
|
h5,
|
||||||
label: f.clone(),
|
label: f.clone(),
|
||||||
@@ -248,6 +237,88 @@ pub fn run(args: &mut Args, out: &mut Out) -> std::io::Result<i32> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One object as the path walk sees it: what its paths compare as, and
|
||||||
|
/// (for a group) its links.
|
||||||
|
struct Node {
|
||||||
|
entry: Entry,
|
||||||
|
links: Vec<Link>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Every path below the object at `start`, relative to it (`""` is `start`
|
||||||
|
/// itself). Each hard link is its own path, so an object linked under two
|
||||||
|
/// names is compared under both, with everything below it: two files that
|
||||||
|
/// hold the same values are equal whether one shares an object between
|
||||||
|
/// names and the other stores copies. A hard link back to an ancestor (a
|
||||||
|
/// cycle) is recorded, not descended into.
|
||||||
|
fn collect(h5: &H5, start: u64) -> crate::h5::Result<BTreeMap<String, Entry>> {
|
||||||
|
let mut cache: HashMap<u64, Rc<Node>> = HashMap::new();
|
||||||
|
let mut node = |addr: u64| -> Rc<Node> {
|
||||||
|
cache
|
||||||
|
.entry(addr)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
Rc::new(match h5.header(addr) {
|
||||||
|
Err(e) => Node {
|
||||||
|
entry: Entry::Broken(e.to_string()),
|
||||||
|
links: Vec::new(),
|
||||||
|
},
|
||||||
|
Ok(h) => match Kind::of(&h) {
|
||||||
|
Kind::Group => match h5.links(&h) {
|
||||||
|
Ok(links) => Node {
|
||||||
|
entry: Entry::Obj(addr, Kind::Group),
|
||||||
|
links,
|
||||||
|
},
|
||||||
|
Err(e) => Node {
|
||||||
|
entry: Entry::Broken(format!("links: {e}")),
|
||||||
|
links: Vec::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
k => Node {
|
||||||
|
entry: Entry::Obj(addr, k),
|
||||||
|
links: Vec::new(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
let mut entries = BTreeMap::new();
|
||||||
|
// (address, relative path, addresses of the groups above it)
|
||||||
|
let mut stack: Vec<(u64, String, Rc<Vec<u64>>)> =
|
||||||
|
vec![(start, String::new(), Rc::new(Vec::new()))];
|
||||||
|
while let Some((addr, path, above)) = stack.pop() {
|
||||||
|
if entries.len() >= crate::h5::MAX_OBJECTS {
|
||||||
|
return Err(crate::h5::Error::new(format!(
|
||||||
|
"more than {} paths; stopped walking",
|
||||||
|
crate::h5::MAX_OBJECTS
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let n = node(addr);
|
||||||
|
entries.insert(path.clone(), n.entry.clone());
|
||||||
|
if n.links.is_empty() || above.contains(&addr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut chain = (*above).clone();
|
||||||
|
chain.push(addr);
|
||||||
|
let chain = Rc::new(chain);
|
||||||
|
for l in &n.links {
|
||||||
|
let child = format!("{path}/{}", l.name);
|
||||||
|
match &l.kind {
|
||||||
|
LinkKind::Hard(a) => stack.push((*a, child, chain.clone())),
|
||||||
|
LinkKind::Soft(t) => {
|
||||||
|
entries.insert(child, Entry::Soft(t.clone()));
|
||||||
|
}
|
||||||
|
LinkKind::External { file, path } => {
|
||||||
|
entries.insert(child, Entry::External(file.clone(), path.clone()));
|
||||||
|
}
|
||||||
|
LinkKind::UserDefined(t) => {
|
||||||
|
entries.insert(child, Entry::UserDefined(*t));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
fn kind_word(k: Kind) -> &'static str {
|
fn kind_word(k: Kind) -> &'static str {
|
||||||
match k {
|
match k {
|
||||||
Kind::Group => "group",
|
Kind::Group => "group",
|
||||||
|
|||||||
@@ -109,6 +109,23 @@ small("attr.h5", attr=True)
|
|||||||
small("reshaped.h5", shape=(4, 3))
|
small("reshaped.h5", shape=(4, 3))
|
||||||
small("int.h5", dtype="i4")
|
small("int.h5", dtype="i4")
|
||||||
|
|
||||||
|
# One object under two names (a hard link) against two separate copies.
|
||||||
|
with h5py.File(os.path.join(out, "hardlinked.h5"), "w") as f:
|
||||||
|
f["x"] = np.arange(5)
|
||||||
|
f["y"] = f["x"]
|
||||||
|
g = f.create_group("g")
|
||||||
|
g["d"] = np.arange(3)
|
||||||
|
g.create_group("s")["e"] = np.arange(2)
|
||||||
|
f["h"] = g
|
||||||
|
for name, last in (("copied.h5", 1), ("copied_changed.h5", 9)):
|
||||||
|
with h5py.File(os.path.join(out, name), "w") as f:
|
||||||
|
f["x"] = np.arange(5)
|
||||||
|
f["y"] = np.arange(5)
|
||||||
|
for gname in ("g", "h"):
|
||||||
|
g = f.create_group(gname)
|
||||||
|
g["d"] = np.arange(3)
|
||||||
|
g.create_group("s")["e"] = np.array([0, last if gname == "h" else 1])
|
||||||
|
|
||||||
with h5py.File(os.path.join(out, "userblock.h5"), "w", userblock_size=1024) as f:
|
with h5py.File(os.path.join(out, "userblock.h5"), "w", userblock_size=1024) as f:
|
||||||
f["d"] = np.arange(10)
|
f["d"] = np.arange(10)
|
||||||
|
|
||||||
|
|||||||
@@ -380,6 +380,42 @@ fn diff_exit_codes_match_h5diff() {
|
|||||||
assert!(s.contains("2 difference(s) found"), "{s}");
|
assert!(s.contains("2 difference(s) found"), "{s}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An object hard-linked under two names is compared under both, with
|
||||||
|
/// everything below it, so a file that shares one object between two names
|
||||||
|
/// equals a file that stores two identical copies.
|
||||||
|
#[test]
|
||||||
|
fn diff_compares_every_hard_link_path() {
|
||||||
|
let Some(f) = generate() else { return };
|
||||||
|
let (linked, copied, changed) = (
|
||||||
|
f.p("hardlinked.h5"),
|
||||||
|
f.p("copied.h5"),
|
||||||
|
f.p("copied_changed.h5"),
|
||||||
|
);
|
||||||
|
// A hard-linked dataset: h5diff agrees (exit 0).
|
||||||
|
for (a, b) in [(&linked, &copied), (&copied, &linked)] {
|
||||||
|
let o = h5rs(&["diff", a, b, "/y"]);
|
||||||
|
assert_eq!(code(&o), 0, "{a} {b} /y: {}", stdout(&o));
|
||||||
|
if tool_available("h5diff") {
|
||||||
|
assert_eq!(code(&run("h5diff", &[a, b, "/y"])), 0, "h5diff /y");
|
||||||
|
}
|
||||||
|
// The whole file, including a hard-linked group's members (h5diff
|
||||||
|
// does not descend a group's second name, so it reports those
|
||||||
|
// members as present in one file only and exits 1).
|
||||||
|
let o = h5rs(&["diff", a, b]);
|
||||||
|
assert_eq!(code(&o), 0, "{a} {b}: {}", stdout(&o));
|
||||||
|
assert!(!stdout(&o).contains("exists only"), "{}", stdout(&o));
|
||||||
|
}
|
||||||
|
// A value under the second name is compared, not skipped.
|
||||||
|
let o = h5rs(&["diff", "-r", &linked, &changed]);
|
||||||
|
assert_eq!(code(&o), 1, "{}", stdout(&o));
|
||||||
|
assert!(
|
||||||
|
stdout(&o).contains("dataset: </h/s/e> and </h/s/e>"),
|
||||||
|
"{}",
|
||||||
|
stdout(&o)
|
||||||
|
);
|
||||||
|
assert!(!stdout(&o).contains("</g/s/e>"), "{}", stdout(&o));
|
||||||
|
}
|
||||||
|
|
||||||
/// Where h5rs deliberately differs from h5diff: objects that cannot be
|
/// Where h5rs deliberately differs from h5diff: objects that cannot be
|
||||||
/// compared (another shape, another datatype class) are a difference.
|
/// compared (another shape, another datatype class) are a difference.
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user