Skip to content

Tests for S2 and the node ceiling, the group B deviations recorded, and MATLAB read-backs of old core saves (M4, M7) - #150

Merged
ww-mw merged 4 commits into
mainfrom
save-tests-and-old-save-read-backs
Oct 19, 2026
Merged

ww-mw merged 4 commits into
mainfrom
save-tests-and-old-save-read-backs

Conversation

@ww-mw

@ww-mw ww-mw commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Four items of group H's lane L4: tests, docs and a MATLAB record. No src/ change, so dist/ is unchanged. No existing test assertion changed.

B-05: tests for S2's untested parts (6d4e9f5)

Ruling. Save decisions D1 (S2): "Key an edited entry's Element children by MODEL-NODE IDENTITY ... never by position. Compare Name-keyed P/Field children three-way." D12 addendum: the save gate refuses a save where "every child whose bytes changed must be one the model changed, besides Name and LastMod" does not hold.

What was wrong. Six mutants survived the whole suite: the merge's read-back guard (BinarySlddSerializer.ts:322, again === line ? merged : null), the layout argument of spliceEntryObject (:474), and the four arms of keptPartsOf (EntryMerge.ts:605, :623, :626, :639). Root cause: nothing exercised them. With the guard instrumented, no test and no fuzz over every binary fixture reached it.

Fix (tests only, in test/binaryEditKeepsUntouched.test.ts). The guard needed no seam. Take a struct entry whose Name line was moved after its Value by hand (theme 1 input), then add a field named Name. Read again, the merge pairs with no entry, so the guard writes the entry from the model. Without the guard, the next edit of an untouched sibling respells it (1.0E+6 becomes 1000000.0). New cases:

  1. a merge that, read again on its own, is not the model's text (kills the :322 mutant);
  2. spliceEntryObject lays out a new bus element in the chunk's own style, CRLF with two spaces (:474);
  3. to 6. keptPartsViolations flags a child written twice or with no name (:605), an unchanged child not written (:623; a control shows that dropping the edited Min is not flagged), a property the reader does not read that is not kept, D13 (:626), and a bus with more or fewer elements than the model (:639).

Fail before. We applied each mutant alone to the full suite. Each one fails exactly its own new case, so before this commit all six survived. One weaker guard still survives: checking only that the merge reads back as some entry (again !== null). The new case covers a merge that reads back as no entry; no input was found that reads back as a different entry.

B-10: the .mat node ceiling is set back after its build (49e0b2f)

Ruling. Group F, group 3: "a node-count ceiling with a source warning now; lazy expansion later."

What was wrong. If building = outer is deleted from buildUnderCeiling's finally (matNodeCeiling.ts:58-71), or moved out of it, the suite still passes. Root cause: no test builds anything after a ceiling is used up or after a build throws.

Fix (test only, in test/matNodeCeiling.test.ts). A second .mat file cannot show the leak, because it arms its own ceiling. What a ceiling left armed would cut is anything built outside one: MAT streams inside a dictionary, and a model workspace. The case opens sparse_text.sldd, sparse_binary.sldd and sparse_ws.slx three times and checks each opens whole (same node count, MAT-stream rows, no _cutByCeiling):

  • alone;
  • after a 21-node ceiling is used up on a .mat (12 containers cut);
  • after the real buildUnderCeiling around the real MatNode._addVariables uses up its ceiling and then throws. The case also checks that the throw reaches the caller.

Fail before. Deleting the line fails at the check after the used-up build: sparse_text.sldd opens with 197 nodes, not 254. Moving it into the try fails at the check after the throwing build. Moving it before building = own fails at the check after the used-up build, like deleting the line. On the full suite, the delete and into-the-try mutants each fail only the new case; the before-building = own mutant was run on this test file only.

Left out. No file we found throws out of the ceiling: every variable is caught one at a time. The one input that does throw is a host parse with no dimensions, and it breaks MatVariable's type, so it was not used.

B-23: group B's kept display conventions recorded (221fcc5)

Ruling. Group B, 2026-10-15: "if our display makes more sense than MATLAB, keep ours. For example, to be consistent, we always use <m x n className> for summary, then keep it. Only fix our display that could cause confusion to user." Also: "Kept: P9."

What was wrong. test/parity/matlab/DESIGN.md:107-113 still said these conventions were "pending the maintainer's rulings". The empty-numeric row (now line 44) said every int-backed enumeration's empty shows [ ]. A probe found that true only for text dictionaries. A binary dictionary or a .mat gives <1x0 DispLevel> and <0x0 DispColor>.

Fix (docs only). The empty-numeric row now covers text dictionaries only. Lines 107-113 are replaced by the quoted ruling, the list of what was fixed instead (P1-P8 and P10 of the 2026-10-16 list), and a table of what is kept. Each row gives our text, MATLAB's browser text and the reason. The rows are Q1, the study's P1 (precision), H1, H2, H4, H6, true/false, doubled quotes, comma-separated cells, P9, and the binary/.mat int-enum empty. H5 is P7, which was fixed.

Tests. None new: the row says "none (docs)". Existing tests already pin every kept item except P9, and H2 (no separate edit text), which is the absence of a feature and has no specific test. No fixture in this repo holds a .mat VariantExpression.

Left out, for the maintainer. DESIGN.md:52 says an empty OnOffSwitchState is <0x0 OnOffSwitchState>. Binary and .mat show the full name, matlab.lang.OnOffSwitchState, and the P3 test pins that. Recommendation: narrow the row to text and record the long name as kept. The other option is a display change that would flip that pin.

B-27: MATLAB read-backs of old core saves (M4, M7) and of D15's binary-to-text copies (3a85679)

Ruling. Group F: "build them under their existing rulings: ... the M1-M9 MATLAB checks, the read-backs". Save decisions: "Still open: M4, M7 (record only)."

What was missing. No truth file existed for M4 (dynamic properties in a save from before #93), for M7 (empty struct entries in a save from before 90caf06), or for D15's binary-to-text paste of a mixed Signal array.

What was built (all under test/parity/).

  • MATLAB R2027a (27.1.0.3409468) wrote oldsaves/old_saves_r2027a_{binary,text}.sldd from make_old_saves_r2027a.m. They hold shapes the repo fixtures lack: an AUTOSAR config set with 5 dynamic properties, a config set with SSC.SimscapeCC, a mixed [DerivedSignal, Signal] array, a base-first Signal array, a struct holding empty-struct fields, and empty structs (0x2, 2x0, 0x0x2, 1x0{a,b}; 0x0 empty structs are also in dispcat_bin.sldd and cases.sldd).
  • We built core at 50549bd^, at 69664b0^ and at 90caf06^. Each tsc output was identical to that commit's dist. With each build we made an untouched save of the three originals, giving 9 old saves, committed because those builds are not in the tree. A stated deviation from the row: 69664b0^ was added to separate the dynamic-attribute loss from An object array keeps each element's own class and keys, and a dynamic property keeps its Source and PropertyType #93's element-class loss.
  • matlab/probe_old_saves_readback.m, its truth file matlab/old_saves_readback.truth.json, matlab/old_saves.mjs (it makes the saves and the pastes), and oldSaves.truth.test.ts (16 cases).

MATLAB truth.

  • M4: a dynamic property saved as an ordinary one reads back as a DynamicProperty with its value, so that loss is harmless. A save from before An object array keeps each element's own class and keys, and a dynamic property keeps its Source and PropertyType #93 is not. It wrote each element of a mixed array as the first element's class, so MATLAB reads the config sets differently, the AUTOSAR options are missing, and the Signal array comes back as two elements of the first class.
  • M7: a struct with no elements, written around an <Element>, reads in its Dimension's shape with no warning. That writer also wrote no <Field>, so the declared fields are lost in the file.
  • Core now reads these old saves as MATLAB does wherever it shows or writes them. Its text copy of them reads in MATLAB as the old save does, with one exception that MATLAB's own text twin shares: the 0x0x2 struct pasted into text reads back as 0x0 (pinned by the test).
  • D15: core's text copies of the mixed arrays read with each element's own class, and the AUTOSAR options come back as DynamicProperties.

Fail before. No truth file existed (ENOENT). The 9 old saves reproduce from their commits' committed dist in content, not byte for byte (zip timestamps differ), so the test pins the committed files' hashes. We applied three mutants, each to src and then reverted: D15's base rule off, the reader dropping _property_source, and a struct with no elements read as 1x1. Each fails 1, 1 and 2 cases.

Not changed. Core's model still holds the stray <Element> as one element. No output path uses it and MATLAB agrees with every output, so under "record only" nothing changed.

Gates at the head (3a85679)

  • rm -rf dist && npm run build: tree clean.
  • npm run verify: exit 0; 296 files passed, 1 skipped; 10211 tests passed, 27 skipped.
  • With DEX_READBACKS_CORPUS set: readbacks.truth, m9MergedSaves.truth, oldSaves.truth and dictionaryXmlFastDifferential pass, 109 of 109. No live MATLAB test reaches these changes. The B-27 probe was run directly in R2027a.

Not in this PR: B-28 (smol-toml), also in this lane, is blocked on registry access.

Implements the maintainer's rulings, as audited in group H (2026-10-19).

ww-mw added 4 commits October 18, 2026 22:28
…ts rules

S2's merge of an edited binary entry had six parts the suite never
exercised: removing any one of them left every test passing. Each now has
a case in binaryEditKeepsUntouched.test.ts, and each case fails with its
part removed and with nothing else:

- the read-back guard in BinarySlddSerializer.mergedEntry: a struct entry
  whose Name line a hand edit moved after its Value gains a field named
  Name. Merged, that field would come before the entry's own Name line,
  and the reader would pair the merge with no entry. The entry is written
  from the model whole, and the saved chunk reopens with every entry
  still paired with its bytes;
- the layout spliceEntryObject passes on: a bus element added in a CRLF,
  two-space chunk is spliced as the same text as in the LF, four-space
  one, in the chunk's layout;
- keptPartsViolations' rules for a child written twice or with no name,
  a child the model did not change left out, a property the reader does
  not read dropped or changed (D13), and a bus written with more or fewer
  elements than the model holds.

Tests only; no source change.
MatNode.fromParsed arms the node ceiling for one .mat file's tree, and
buildUnderCeiling sets it back in a finally. Nothing tested that: deleting
the set-back, or moving it out of the finally, passed the whole suite.

The new case opens a text dictionary, a binary dictionary and a model
workspace, each holding values read out of MAT streams, before and after a
.mat file whose ceiling is used up, and again after a build that throws once
its ceiling is used up. Each must open as it did, with all its rows. With the
ceiling left armed they open with their sparse arrays and cells shown without
contents.

Test only, no change to src.
DESIGN.md still said whether the rest of the display follows MATLAB was
pending the maintainer's rulings. The group B ruling of 2026-10-15 settled
it: keep this project's display where it makes more sense, and fix only a
display that could mislead. Replace that paragraph with the ruling and the
kept list (the summary spelling, full precision, a Parameter's row, no
separate edit text, a 1x1 cell's braces, the length guard, true/false,
doubled quotes, comma-separated cells, a .mat VariantExpression's recorded
class, and the binary and .mat int-backed enumeration empty), each with
MATLAB's browser text and the reason.

Narrow the empty-numeric row: an int-backed enumeration's empty is [ ] as a
text dictionary holds it; a binary dictionary's and a .mat's keep their
summary.
… mixed-array copies into text

M4 and M7 were the last MATLAB read-backs of the 2026-10-13 save-fidelity plan
still open, and D15's binary-to-text reopen had no truth file. Each core before
#93 and before 90caf06 wrote every entry of a save from its model, so a file it
saved is still on a user's disk: what do MATLAB R2027a, and core now, read of it?

No binary fixture held a dynamic property, so make_old_saves_r2027a.m had MATLAB
write one dictionary, in both formats, of the shapes none holds: an AUTOSAR
config set (a Simulink.STFCustomTargetCC with five dynamic properties), a config
set with an SSC.SimscapeCC attached, a dexdata.DerivedSignal beside a
Simulink.Signal (either first, as an NdHolder's Kids in Other Data: a dictionary
refuses a Signal array as an entry's value), and structs of no element in five
shapes, one with fields, and as a struct's fields.

old_saves.mjs made untouched saves of that file and of the two fixtures holding
a struct entry of no element (dispcat_bin.sldd, cases.sldd) with core built at
50549bd^, at 69664b0^ (#93 less the commit that kept a dynamic property's Source
and PropertyType, so that M4 is asked about alone) and at 90caf06^. The nine
saves are committed in test/parity/oldsaves/, since the builds are not in the
tree. It also makes seven pastes with core as it is now into the text twin: each
mixed-array entry, all four together, and the empty structs from the original and
from an old save. probe_old_saves_readback.m had MATLAB open every save and paste
and a copy of its original, and wrote old_saves_readback.truth.json.

What MATLAB read, with no error or warning anywhere:
- M4: a dynamic property written as an ordinary one reads back as the dynamic
  property it was, with its value. Harmless. A save before #93 is not: that core
  wrote every element of a mixed array as the first element's class (the config
  set's components all Simulink.SolverCC, its target as a Simulink.CodeAppCC), so
  MATLAB reads both config sets otherwise, none of the AUTOSAR options exists,
  and the Signal array is two of its first element's class.
- M7: a struct of no element written around an <Element> reads in the shape its
  Dimension states, with no element. But that writer wrote none of the fields it
  declares, so a 1x0 struct with fields a and b reads with none: lost in the file.
- Core now reads each such struct as MATLAB does wherever it shows or writes it
  (its size, no row, no field, regenerated self-closed), and its copy of one into
  a text dictionary reads in MATLAB as the old save does. So no source changes.
- D15: core's text copies of the mixed Signal arrays read as a Simulink.Signal
  array of each element's own class, the config sets' Components as the original,
  SSC.SimscapeCC included, and the AUTOSAR options, which the text form holds
  among the ordinary properties, as the dynamic properties they were.
- The same old saves show other losses, each fixed since: dispcat_bin.sldd's
  shaped empty cells, chars and strings, and before #93 its enumeration values.

oldSaves.truth.test.ts (16 cases) checks that the old saves and their originals
are the files MATLAB read, makes the pastes again from source and asks that each
is still the one MATLAB was asked about, holds MATLAB's answers, and asks that
core reads each old save's dynamic properties and empty structs as MATLAB does.
With no truth file it fails to open it. With D15's base-class rule taken out, the
dynamic property's Source dropped by the reader, or a struct of no element read
as 1x1 when an <Element> is there, it fails. No existing test changes.
@ww-mw
ww-mw merged commit 31b8ac3 into main Oct 19, 2026
4 checks passed
@ww-mw
ww-mw deleted the save-tests-and-old-save-read-backs branch October 19, 2026 04:15
ww-mw added a commit that referenced this pull request Oct 19, 2026
Brings in #150 and #152. The parity README keeps both sides' new probe rows; dist is rebuilt from the merged sources.
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.

1 participant