Skip to content

Tests and Benchmarks for regression - #85

Draft
yohannmarguier wants to merge 105 commits into
iterorganization:developfrom
yohannmarguier:develop
Draft

yohannmarguier wants to merge 105 commits into
iterorganization:developfrom
yohannmarguier:develop

Conversation

@yohannmarguier

Copy link
Copy Markdown

Markdowns in 'benchmarks' and 'tests' directories provide description of the coverage and the uses

yohannmarguier and others added 30 commits June 30, 2026 11:11
Implements Step 1 (the "scaffold slice") of TEST_STRATEGY.md: build the
machine on one example so every mechanism is proven before the matrix is
broadened. Binds only to the public C ABI (al_lowlevel.h + al_const.h) per
decision D1.

What lands:
- tests/contract/al_contract.h — reusable harness: RAII temp pulse dir,
  legacy-parameter URI builder, and typed round-trip helpers over
  al_write_data/al_read_data honoring the buffer-ownership contract
  (scalar caller-owned; char-array core-allocated + freed).
- test_roundtrip.cpp — TEST_P write->read round trip on HDF5 AND Memory;
  oracle is round-trip self-consistency, needing zero DD artifacts (D5).
- test_known_defects.cpp — the al_setvalue_*_parameter_plugin unregistered-name
  null-deref (FUNCTIONALITY_INVENTORY.md:410) encoded per D2: a DISABLED_
  test asserting the CORRECT contract (returns an error), plus a companion
  death test pinning today's crash to SIGSEGV specifically. The bug is not
  frozen as spec; the day it is fixed the xfail flips to pass.
- CMake wiring (D3): GoogleTest via find_package-or-FetchContent (pinned to
  the v1.15.2 commit), gtest_discover_tests with a "contract" label, and an
  AL_CONTRACT_SANITIZE toggle (D7) that instruments both the tests and libal
  with ASan/UBSan — non-blocking. The pure-C canary (testlowlevel_c.c) is
  retained unchanged.

Deferred (recorded): D7's dedicated sanitizer CI leg + suppressions file, and
the unit/integration label split, arrive with the broader matrix in later
steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Step 3 of TEST_STRATEGY.md §4 (the thin unit tier), building on the #2
scaffold. Binds only to the public C ABI per decision D1.

What lands:
- test_introspection.cpp — pure-lookup unit tests for Cluster 4
  (FUNCTIONALITY_INVENTORY.md:434-459), all intended-contract (D2):
  const2str/err2str on mapped ids; the silent "" for unmapped ids asserted
  as the deliberate contract (incl. the documented TIMERANGE_OP /
  FLEXBUFFERS_BACKEND map-coverage gaps, :75-79); the two maps proven
  distinct; getALVersion == the compiled AL_VERSION macro and dotted-numeric;
  getDDVersion == "!!DEPRECATED!!".
- test_version_sentinel.cpp — decision D5 tripwire: a fake DD version written
  to ids_properties/version_put/data_dictionary round-trips verbatim AND
  leaves getDDVersion() unchanged, proving the core interprets no DD version.
  Parametrized over Memory (hermetic) and HDF5 (on-disk).
- TRACEABILITY.md — decision D8 matrix; Cluster 4 + version-sentinel rows
  filled, scaffold rows seeded, remaining clusters listed as gaps for later
  build-order steps.
- CMakeLists.txt — wires both files into contract_tests. The finer CTest
  unit/integration LABEL split (D6) stays deferred as the scaffold recorded;
  the pure-lookup subset is addressable via `ctest -R Introspection`.

Per the issue's adapted TDD loop, each assertion was mutation-verified to fail
for the right reason (real value surfaced) before setting the true expectation
green. No new expected-fails; the crash-class defects remain in
test_known_defects.cpp.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Implements Step 2 of TEST_STRATEGY.md — the bulk of the storage contract —
building on the scaffold (issue #2) and binding only to the public C ABI (D1).

Fixture layer (al_contract.h):
- Deterministic synthetic-data generator over the datatype × shape space:
  synth_value/synth_buffer for CHAR/INTEGER/DOUBLE/COMPLEX and shape_for_rank
  scalar→7-D (MAXDIM), with values kept clear of the EMPTY sentinels so a
  scalar write is never silently dropped by data_has_non_zero_shape.
- Generic typed write_data/read_data honoring the two ABI ownership regimes
  (scalar caller-buffer; array core-allocated + freed).
- build_uri gains a FLEXBUFFERS branch: getURIBackend (al_context.cpp:280) has
  no flexbuffers case though the parse side accepts the scheme, so the URI is
  hand-built (documented, and tracked as an expected-fail).

Matrix (test_roundtrip_matrix.cpp):
- One TEST_P over Combine({HDF5,Memory,ASCII,Flexbuffers} × {CHAR,INTEGER,
  DOUBLE,COMPLEX} × ranks 0..7); each cell its own ctest entry. Oracle =
  round-trip self-consistency, zero DD artifacts (D5).
- Each cell classified once (D2/D4): RoundTrip (assert equality), Refused
  (paired-negative — CHAR>2D on HDF5/ASCII; every Flexbuffers cell, which
  accepts the write but refuses the read-back as a serializer), or KnownDefect.
- Known defects asserted as their correct contract and marked expected-fail:
  HDF5 crash on a CHAR scalar (DISABLED_ + paired death test), ASCII numeric
  corruption/abort at the MAXDIM boundary, and the flexbuffers URI-builder gap.

TRACEABILITY.md: al_read_data/al_write_data rows filled (D8).

Full contract suite: 139 passed, 4 known-defect cells skipped, 6 expected-fails
disabled; each expected-fail verified to produce a real red when enabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
TEST_STRATEGY.md §4 step 4 (Cluster E): slice / timerange /
al_list_filled_paths tested positive where the backend supports the
capability and paired-negative (documented refusal) where it does not, so
the supports* matrix is itself under test at the C ABI boundary (D1 — the
C++ Backend flags are asserted through their sole ABI-observable
consequence, accept-vs-refuse, since they cannot be linked directly).

Support columns were established empirically against the binary and follow
D2 (assert the real contract, not the issue's assumptions):
- Memory genuinely supports slice (full SLICE_OP handling in
  memory_backend.cpp; correct closest-slice values), so slice is positive
  on HDF5+Memory and paired-negative only on ASCII+Flexbuffers — not the
  issue's assumed Memory-negative.
- Refusal codes pinned specifically: timerange -> LOWLEVEL_ERR
  (al_lowlevel.cpp:1053), slice / list_filled_paths -> BACKEND_ERR,
  slice-read without an interp mode -> CONTEXT_ERR (al_context.cpp:347).

What lands:
- test_capability_gated.cpp — CapabilityMatrix TEST_P over the four
  always-on backends for timerange, slice read, slice-write-begin, and
  list_filled_paths; HDF5-only detail tests for the three interpolation
  modes (CLOSEST/PREVIOUS/LINEAR) and timerange with/without a resampling
  dtime grid, each with an exact value oracle (seeded via a single global
  write, so the read machinery is exercised independently of append).
- KNOWN DEFECT per D2: repeated WRITE+UNDEFINED_TIME slices do not
  accumulate in this build (only the last persists). Encoded as a
  DISABLED_ correct-contract test asserting both slices persist plus a
  current-behavior tripwire pinning today's single-slice result; write
  lifecycle ownership stays with issue #3.
- TRACEABILITY.md — Cluster E rows filled (covered + the append xfail);
  stale Cluster-E gap bullet removed.
- CMake: test_capability_gated.cpp added to the contract_tests target.

Full contract suite: 164 tests green (1 xfail disabled).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Correction of the .md to fit correctly in order of the processed issues.
What changed:
tests/contract/test_capability_gated.cpp — open_seeded() refactored from a int-returning helper using non-fatal EXPECT_* into a void open_seeded(int* pctx) using ASSERT_*, with every call site wrapped in ASSERT_NO_FATAL_FAILURE(open_seeded(&pctx)).

Why this fixes the defect:
previously, if the URI build / data-entry open / seed write failed, the test continued and the paired-negative branch (EXPECT_NE(s.code,0) / EXPECT_EQ(s.code, BACKEND_ERR)) could pass for the wrong reason — a broken seed looking identical to a genuine documented refusal. ASSERT_* inside a subroutine only aborts the subroutine, so ASSERT_NO_FATAL_FAILURE at the call site is what actually aborts the test on setup failure. Now a refusal assertion can only be reached with a genuinely-seeded entry. The comment, which previously claimed fatal handling it didn't provide, now matches the code.
Two scaffold (issue #2) acceptance items were never delivered; this closes both.
 1. ctest -L unit label split. The suite carried only the contract
label, so ctest -L unit selected nothing (PRD user story 29). Now two
disjoint gtest_discover_tests passes label each case by substrate
(decision D6): unit = pure lookups + in-RAM Memory + plugin ABI tests
(Introspection.*:*Memory*:KnownDefects*), integration = everything on-disk.
 Both also keep contract. Two gotchas handled: the LABELS list needs an
escaped ; to survive re-emission, and gtest's negative-filter syntax is one
 - then colon-separated (*:-a:b:c), not -a:-b:-c. Guarded by CMake ≥ 3.22
 (TEST_FILTER), falling back to the single contract label on the 3.21 floor.
 Verified: ctest -L unit = 49 hermetic tests (0 on-disk), runs in 0.20 s;
 integration = 126; contract = 175 (49 + 126, disjoint).

 2. Non-blocking sanitizer CI leg + suppressions file. Added
 .github/workflows/sanitizers.yml: a Linux continue-on-error job that
 configures -DAL_CONTRACT_SANITIZE=ON, builds, and runs the contract suite
 under ASan/UBSan/LSan, with tests/contract/sanitizer_suppressions.txt wired
 via LSAN_OPTIONS. Validated the (previously never-exercised)
 AL_CONTRACT_SANITIZE build path locally: it compiles, links ASan into the
 binary, and passes 100% (163 tests) under ASan/UBSan. The run excludes
 Death tests (deliberate crashes) and the CurrentlyCorrupts tripwires — the
 latter because the ASCII rank-7 DOUBLE/COMPLEX corruption is UB that ASan's
 memory layout masks (documented in-code and in the workflow).
…tinel

The sentinel test's real property is non-interpretation: writing a DD version
to version_put/data_dictionary must not change what getDDVersion() returns.
Compare the post-write value to the captured baseline (dd_before) rather than
to the "!!DEPRECATED!!" literal — the literal value is already pinned by
test_introspection.cpp::GetDDVersionReturnsDeprecatedSentinel, so this removes
the cross-test coupling and keeps the invariance check correct even if the
compiled sentinel ever changes. The one baseline assertion stays as the anchor.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Cover User Cluster 3 (plugin management) through the public C ABI, per
TEST_STRATEGY.md build-order step 5 and decision D2.

- test_plugin_fixture.cpp: a real, self-contained access_layer_plugin built
  as a loadable alcontract_plugin.so (the classic red-green fixture), so the
  register/bind/unbind/unregister state machine and the al_setvalue_* happy
  paths run end to end. Its setParameter logs the received value so tests can
  prove a value crossed the ABI, not just that the call returned success.
- test_plugins.cpp: the register/bind/unbind/unregister state machine + the
  documented quirks (register-twice throws, bind-unregistered throws,
  double-bind throws, unbind-unbound silent no-op), al_is_plugin_registered,
  all three al_setvalue_* variants reaching the plugin, and framework gating.
  Two defects pinned as xfail + paired current-behavior tripwire: the
  registered-but-never-bound plugin left un-destroyed on unregister, and the
  dlopen-failure swallowed by the NDEBUG-stripped assert.
- test_known_defects.cpp: add the double- and generic-variant setvalue
  null-deref death tests (SIGSEGV) + DISABLED_ correct-contract partners,
  joining the scaffold's int variant; share the unregistered-name constant.
- al_contract.h: hoist the shared kUnregisteredPluginName constant.
- CMakeLists.txt: build the plugin MODULE (POSIX-guarded), wire its dir/name
  via compile defs, add test_plugins.cpp, extend the unit label filter.
- TRACEABILITY.md: fill the Cluster 3 rows, extend the xfail bookkeeping
  table (now 11 pairs), mark Part 3 setParameter covered (indirect).

Empirical finding vs the inventory: the dlopen defect does not crash on this
NDEBUG build (the null handle reaches dlsym, whose null-check throws), so it
is a plain xfail, not a death test; the setvalue null-derefs genuinely
SIGSEGV and remain death tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
…le (issue #4)

Characterizes existing C-ABI behavior for arrays-of-structures (top-level +
nested write/iterate/read), al_delete_data at signal/structure/DATAOBJECT-root
granularity, al_get_occurrences, and the remaining pulse-lifecycle detail
(al_begin_dataentry_action modes, al_context_info, al_get_backendID,
al_close_pulse CLOSE/ERASE) across the always-on backend tier. Adds a
deterministic equilibrium-seed generator (scalar + timebase-carrying 2-D array
+ constraints AOS, oracle = content hash, no committed blobs) to exercise
realistic AOS/timebase/HDF5-tensorization shapes.

Empirically validated against the built library (not just source reading) per
TEST_STRATEGY.md's TDD discipline, which surfaced several defects invisible to
static analysis: ASCII's AOS read always reports size 0 (its READ_OP setup
drains the stream into a lookup map before the AOS-size cursor ever runs),
HDF5/Memory/ASCII/Flexbuffers each implement a disjoint, non-overlapping slice
of the three delete granularities, ERASE_PULSE is a no-op-equivalent to
CLOSE_PULSE on every always-on backend, ASCII's CREATE_PULSE silently
overwrites instead of refusing, and al_get_backendID accepts a wrong-type
context via an unchecked static_cast. Each genuine defect is pinned per
decision D2: a DISABLED_ correct-contract test paired with an always-run
current-behavior tripwire, never frozen as if it were the spec.

Fills the AOS / delete / occurrences / lifecycle rows in TRACEABILITY.md.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Add structured-data contract tests: AOS, delete, occurrences, lifecyc…
…sue #8)

Pins caller-frees ownership for al_build_uri_from_legacy_parameters's *uri
(the three other flagged APIs were already pinned by earlier issues),
resolves empirically that plugin-parameter dim/size is not bounded by
MAXDIM, and covers the remaining Plugin-author audience reachable through
the C ABI: low-level al_plugin_* reentry, write/read data interception via
a bound plugin, al_write_plugins_metadata, and al_bind/unbind_readback_plugins
(extending the in-repo test plugin fixture with an env-var-gated readback
capability). TRACEABILITY.md now has no blank rows; the two remaining gap
rows are marked terminal with an explicit rationale.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
- TEST_STRATEGY.md D2 now states the actual (and better) discipline: each expected-fail's FUNCTIONALITY_INVENTORY / issue citation lives in tests/contract/TRACEABILITY.md as the single source of truth, and test files carry a pointer to the matrix instead of repeating citations in-line.
- The three flagged test files (test_roundtrip_matrix.cpp, test_pulse_lifecycle.cpp, test_structured_data.cpp) each gained that one-line pointer in their header comment, so a reader landing on a DISABLED_ test knows exactly where its citation is.
Stands up a real, runnable MDSplus in an aarch64 Docker image
(docker/mdsplus/), bakes a DD-4.1.1 model tree, and proves one equilibrium
scalar survives a write->read round trip through the public C ABI against
it -- lighting up the whole characterization stack (build gate, runtime
skip, CTest label) before any behavioral breadth is attempted per issue #12.

- tests/contract/test_mdsplus.cpp: MdsplusTracerBullet.ScalarSurvivesWriteThenRead,
  reusing equilibrium_seed.h's real DD path/value. Compiles out entirely
  unless AL_CONTRACT_HAVE_MDSPLUS is defined; GTEST_SKIP()s at runtime when
  MDSPLUS_MODELS_PATH is unset (TEST_STRATEGY.md D4: absent/skipped, never
  failed).
- tests/contract/CMakeLists.txt: defines AL_CONTRACT_HAVE_MDSPLUS when
  AL_BACKEND_MDSPLUS=ON, and adds a new `mdsplus` CTest label. Also documents
  (without fixing, out of scope) a pre-existing gtest_discover_tests defect
  found while wiring this up: multi-element LABELS lists silently collapse to
  their first token, so `ctest -L unit`/`-L integration` currently select
  nothing -- the new mdsplus pass sidesteps it with a single-element list.
- docker/mdsplus/: Ubuntu 24.04 (not the issue's suggested 22.04 -- MDSplus
  publishes no arm64 .debs for 22.04/20.04, empirically confirmed) with
  MDSplus, a JDK for the model-tree compiler, and the DD-4.1.1 bake via
  pip-installed imas_data_dictionary. Verified end-to-end: image builds,
  model bakes, contract suite builds with AL_BACKEND_MDSPLUS=ON, and
  `ctest -L mdsplus` passes.
- tests/contract/TRACEABILITY.md: new Part 4 section (MDSplus backend,
  compile-guarded tier) with the four-status scaffolding and the tracer
  bullet's first row.
Adds MDSplus as a parameter/case to DataEntryModes, CapabilityMatrix,
Occurrences, and AosMatrix, reusing their existing test bodies, and
triages every resulting outcome into Part 4 of TRACEABILITY.md.

Empirically resolved against a real DD-4.1.1 model tree (docker/mdsplus/):
occurrence naming is "<ids>/<N>" (a third convention, same slash form as
ASCII); get_occurrences is real (gated on homogeneous_time being set);
list_filled_paths always throws (BACKEND_ERR); slice read/write genuinely
round-trip (supportsTimeDataInterpolation() is unconditionally true) while
timerange is refused (LOWLEVEL_ERR, supportsTimeRangeOperation() is
unconditionally false); and DataEntryModes' CREATE_PULSE guard matches
HDF5/Memory, not ASCII's defect.

AosMatrix and EquilibriumSeedMatrix hit the same divergence: MDSplus
resolves every path against its real DD-baked model tree, so their
synthetic/DD-agnostic field shapes ("elements"/"val", the seed's flat
profiles_1d/psi write, the generic constraints AOS) throw Node Not Found
at flush time. This is a legitimate storage-model difference, not a
defect, so AosMatrix's MDSplus case is skipped (via a new AosExpect::
Divergence, mirroring the existing KnownDefect skip) and MDSplus is left
out of EquilibriumSeedMatrix's backend list (like Flexbuffers already is,
for its own reason) rather than landing a permanently red test -- both are
documented in-place and in TRACEABILITY.md.

No genuine defect surfaced this round, so no new xfail pairs.
…ip logic

- EquilibriumSeedMatrix now takes an actual MDSplus instantiation (skipped
  in-place via GTEST_SKIP) instead of omitting it from kSeedBackends[],
  matching AosMatrix's divergence treatment and issue #12 Q5's explicit
  listing of this fixture as one MDSplus can join.
- Extract the repeated "skip if MDSPLUS_MODELS_PATH unset" block into a
  single AL_CONTRACT_SKIP_IF_MDSPLUS_UNCONFIGURED() macro in al_contract.h
  (a macro, not a function, since GTEST_SKIP()'s implicit return must
  unwind the calling SetUp()/TEST body directly); reuse it in
  test_mdsplus.cpp, test_pulse_lifecycle.cpp, test_capability_gated.cpp,
  and the new Occurrences test.
- Add `divergence` to TRACEABILITY.md's status vocabulary list (it was
  used as a live status without ever being defined there).
- Fix a stray escaped-slash in CMakeLists.txt's mdsplus TEST_FILTER that
  didn't match its own explanatory comment.
RoundTripMatrix sweeps {CHAR,INTEGER,DOUBLE,COMPLEX} x rank against synthetic
opaque paths MDSplus structurally can't join. test_mdsplus_real_paths.cpp
curates one real DD-4.1.1 path per (type, rank) cell instead, sourced by
walking build-mdsplus/_deps/data-dictionary-src/IDSDef.xml directly (the
imas-dd MCP tool wasn't available in this session), spanning multiple IDSs.

32 cells: 18 covered (13 plain leaf, 5 via 1-3 level AOS descent reusing
AosMatrix's proven idiom), 1 divergence (CHAR r0 against a real STR_0D node:
MDSplus's text node is inherently string-shaped, so it cleanly refuses a
dim=0 read-back instead of HDF5's crash on the same synthetic shape), 13
terminal-gap (DD 4.1.1 has no field of that type/rank anywhere, e.g. no
STR_2D+, no CPX_0D). Verified against a real MDSplus tree in docker/mdsplus/:
ctest -L mdsplus is green (45/45).
`al_delete_data`'s `path` is accepted by the ABI and dropped by the HDF5
backend: `HDF5Backend::deleteData` never forwards it and `HDF5Writer::
deleteData` has no parameter to receive it, so any non-empty path removes
the whole IDS occurrence and reports success.

Characterize that before touching it. The existing tripwire only observed
the sibling leaf; widen it to the two consequences issue #63 measured as
well — the occurrence's own `<ids>.h5` is deleted, and the master file is
left holding an external link to a file that no longer exists (observed
through `al_get_occurrences`, which enumerates those links live).

Confirmed red/green today: the three correct-contract assertions fail, the
tripwire passes.
Decision: `path` means what the ABI has always said it means — a non-empty
path deletes that node and its subtree only, an empty path addresses the
whole DATAOBJECT. Recorded in docs/adr/0001-al-delete-data-path-semantics.md
together with the per-backend matrix and why the cheaper alternative
(rejecting a non-empty path as a caller error) was rejected: it is not what
the header says, not what the other backends do, and it would fail every
non-slice `put()` and `delete_data()` through imas-python, whose traversal
issues one delete per DD leaf.

`HDF5Writer::deleteSubtree` selects the matching datasets out of the IDS
group's flat namespace of tensorized names — the mangled path, its `_SHAPE`
companion, and anything continuing with `&` (a structure child) or `[]` (an
AOS index). The continuation character is what keeps `time` from taking
`time_slice[]&x` with it. Buffered writes are flushed before anything is
unlinked, since HDF5DataSetHandler::close() frees write buffers without
writing them. The group listing is cached across a run of deletes, and
invalidated by anything that can add a dataset, so the HLI's leaf-by-leaf
traversal is not quadratic in the size of the IDS.

`deleteOccurrence` keeps the empty-path behavior and fixes two defects in
it: the master file's external link is now removed with the file it points
at (it used to be left dangling, so al_get_occurrences kept reporting an
occurrence whose file was gone), and a closed-but-present IDS file is
deleted instead of being silently reopened and leaked.

Tests: DeleteMatrix's HDF5 leaf/structure cells are enabled, the HDF5 xfail
pair retired, and the layout-specific consequences the backend-agnostic
matrix cannot see are pinned in Hdf5Delete.* — which files and master-file
links survive which granularity, same-prefix siblings, an absent path, and
a run of successive leaf deletes all taking effect. Verified against the
suite, the Debug leg, and ASan/UBSan.
The first cut matched dataset names by string prefix plus a continuation
character. That is right for every path the HLI issues, but it matches
nothing at all for a path that traverses an array of structures:
`outer/inner` mangles to `outer&inner` while the dataset is named
`outer[]&inner&leaf`. The call then deleted nothing and reported success —
in miniature, the same "argument accepted, ignored, success reported" state
this issue exists to remove. Confirmed by test before changing the rule.

Match per segment instead: each segment of the requested path matches a
dataset-name segment with or without its `[]` AOS suffix, and the name's
remaining segments are its subtree. Segment-wise still tells a real child
from a same-prefix stranger (`time` vs `time_slice[]&x`, `code` vs
`code_name`), and it gives an AOS-crossing path the one meaning the ABI
leaves available: `deleteData` receives an OperationContext, never an
arraystruct one, so no element can be named and the path can only mean
"that subtree in every element" — exactly what the tensorized layout holds.
Standards axis:
- The group listing now validates itself against the group's live link count
  instead of relying on four hand-placed invalidateGroupMembers() calls on
  the write paths. Only a write can add a link, and a write can only add, so
  an unchanged count means an unchanged set of names — which removes the
  latent coupling where any future dataset-creating path had to remember to
  invalidate.
- unlinkFromGroup() owns both the H5Ldelete loop and keeping the cache in
  step, so deleteSubtree no longer reassigns the writer's own vector through
  a returned reference.
- The delete error builds its message as a std::string rather than through a
  fixed-size buffer: a dataset name is a caller-supplied DD path, which the
  src/hdf5 `char error_message[200]` idiom cannot bound.
- Both H5 failures in the listing name their own function.
- Hdf5Delete's tests all go through the fixture now; the free observation
  helper that duplicated three of its members (and skipped make_legacy_tree,
  unlike every other on-disk path in the file) is gone.

Spec axis:
- al_lowlevel.h's per-backend summary said memory does "node and subtree"
  where only the AOS case is characterized, and omitted UDA entirely.
- The ADR records the two behaviour changes the issue did not itself measure
  — master-link removal on an empty-path delete, and deleting an IDS file
  that is closed but still on disk — rather than leaving them silent.

The one substantive fidelity finding, that a path below an AOS silently
matched nothing, was already fixed in the preceding commit; the review
confirms it independently.
Make HDF5 al_delete_data honor its `path` argument (issue #63)
`al_read_data` has always documented that a fieldpath may address a field
from the dataobject root with a prepended `/`, but `HDF5Reader::read_ND_Data`
honored only the relative half: it replaced `/` with `&` and then
unconditionally prepended the current AOS context's tensorized path, so an
already-rooted name was appended to the context it was meant to replace
(`time_slice[]&constraints&j_phi[]&&time_slice&constraints&j_phi&…`).
`H5Lexists` found nothing, the reader returned 0, and `al_plugin_read_data`
filled the caller's buffer with EMPTY_* while reporting code 0 — stored data
reported as absent, a silent wrong answer rather than an error.

Decision: an absolute path resolves **by path segment** against the AOS chain
the caller has open, cutting at the last matching AOS boundary. That answers
the two coupled questions together — which dataset name to look up (the
tensorized `[]` markers of the retained levels are kept) and which indices
still apply (only the retained levels', so a child's cursor is never applied
to an ancestor's shallower dataset, and the timed-AOS designation is dropped
with it). Recorded in docs/adr/0002-absolute-field-path-resolution.md with
the four rejected alternatives — chiefly plain de-slashing, which fixes only
root fields, and keeping the child's whole cursor, which returns a wrong
number instead of none. Segment-wise matching is the same rule ADR 0001 chose
for `al_delete_data`, for the same reason: it tells a real descendant from a
same-prefix stranger.

The resolution lives in `HDF5Utils::resolveAbsoluteFieldPath`, which reports
how many AOS levels the target still sits under; `read_ND_Data` truncates the
current indices to it. No DD-version knowledge, no schema lookup, no ABI
change. A target beyond an AOS the caller never opened has no cursor and no
schema to infer one from, so it resolves without that `[]`, is not found, and
travels the ordinary missing-data path — decided, and pinned by a test.

Scope is HDF5 reads, as issue #65 establishes: the suite writes through
relative spellings only, so it presumes nothing about writes, delete or
arraystruct opening.

Tests: Hdf5AbsoluteFieldPaths.* — both spellings of one address compared at
nonzero indices on both levels of a nested AOS (time_slice →
constraints/j_phi), for a scalar leaf, a CHAR array leaf, ancestor targets,
root targets and the operation context itself; the plugin twin
(al_plugin_read_data) asserted separately; cache/iteration stability, an
absent-data control, and the unopened-AOS limit. Ten of the thirteen fail on
the parent commit. Verified against the full suite, the Debug leg, ASan/UBSan
(no new findings) and the Python suite.
…-reads

Make HDF5 reads honor absolute field paths (issue #65)

@SimonPinches SimonPinches left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too large to review in one go, but here are a few initial comments.

pull_request:

env:
DD_VERSION: "4.1.1"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we see this version being updated following future DD releases? Or do we want DD_LATEST ?

python -c "import sys, pprint, cibuildwheel; pprint.pp(sys.path); pprint.pp(cibuildwheel)"
python -m cibuildwheel --output-dir wheelhouse --config-file pyproject.toml
env:
UDA_REF: "2.8.1" # keep in sync with windows_deps.env.UDA_REF

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not 2.9.3 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the filename not capture the exact model of laptop and perhaps the OS it runs, rather than who owns it?

hard-coded into the script or this doc:

```sh
salloc --exclusive --partition=<PARTITION> --time=00:30:00

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we scan over partitions = hardware generation?

Comment thread AGENTS.md

### Issue tracker

GitHub Issues on the `origin` fork `yohannmarguier/IMAS-Core` (via the `gh` CLI); external PRs are not a triage surface. See `docs/agents/issue-tracker.md`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be:

Suggested change
GitHub Issues on the `origin` fork `yohannmarguier/IMAS-Core` (via the `gh` CLI); external PRs are not a triage surface. See `docs/agents/issue-tracker.md`.
GitHub Issues on the `origin` fork `iterorganization/IMAS-Core` (via the `gh` CLI); external PRs are not a triage surface. See `docs/agents/issue-tracker.md`.

@olivhoenen olivhoenen left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @yohannmarguier, indeed this can't go in as one big PR, it needs to be structured better. At least we need to:

@olivhoenen

olivhoenen commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks @yohannmarguier for this substantial work! To make it reviewable, it has been split into smaller, single-purpose PRs that keep test and benchmark additions apart from bug fixes. Your commits are cherry-picked unchanged (-x), so you remain the author of each one, with original dates and messages and a link back to your fork.

Independent (based on develop)

Contract test suite (stacked on #93)

Bug fixes (each with its own regression test, stacked on #99)

Benchmarks

Left out: the laptop baseline JSON and the personal .gitignore entries (.idea, /graphify-out). The combined result of these PRs was checked against your fork's final tree: apart from those omissions and changes made on develop since, the only difference is one TRACEABILITY.md row that a merge on the fork had accidentally reverted.

Please merge them with merge or rebase commits, not squash, so authorship is preserved. This PR can be closed once the split is agreed.

🤖 Generated with Claude Code

@olivhoenen
olivhoenen marked this pull request as draft October 15, 2026 15:35

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants