diff --git a/test/binaryEditKeepsUntouched.test.ts b/test/binaryEditKeepsUntouched.test.ts index 2f85ef69..041bb88a 100644 --- a/test/binaryEditKeepsUntouched.test.ts +++ b/test/binaryEditKeepsUntouched.test.ts @@ -19,11 +19,13 @@ // (D1); // * an edit and its undo give back the bytes with only the LastMod line new (D2); // * what cannot be merged with certainty is written from the model whole, as every edited -// entry was: markup the scan cannot read, a duplicate name, an origin left in the text; +// entry was: markup the scan cannot read, a duplicate name, an origin left in the text, and a +// merge that, read again on its own, is not the model's text; // * no origin the writers spell for the merge reaches a saved file; // * the merge of a 5 MB entry costs a small multiple of writing it from the model; -// * and the three functions a host that splices chunk text needs: spliceEntryObject, -// keptPartsViolations and readEntryFragment. +// * and the three functions a host that splices chunk text needs: spliceEntryObject, which +// lays what it writes from the model out as the chunk is, keptPartsViolations, each rule it +// holds a write to, and readEntryFragment. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; @@ -448,6 +450,48 @@ describe('what cannot be merged with certainty is written from the model whole', expect(out).toBe(regenerated(entry)); expect(out).not.toContain('dex-core-origin'); }); + + it('a merge that, read again on its own, is not the model\'s text: here, one that reads as no entry', () => { + // A struct entry with its Name line moved after its Value, as a hand edit may leave it, and a + // sibling MATLAB spelled `1.0E+6`. The reader reads both, and pairs each with its bytes: the + // first `

{ + const lines = object.split('\n'); + const name = lines.splice(1, 1)[0]; + lines.splice(lines.length - 2, 0, name); + return lines.join('\n'); + }; + const value = '

\n \n

1.0E+6

\n \n

'; + const xml = chunk(moved(entryXml('s', 'u-s', value)) + entryXml('t', 'u-t', '

1.0E+6

')); + expect(objectOf(xml, 's').indexOf('

c.name)).toEqual(['a', 'Name']); + const saved = savedChunk(session, root); + expect(objectOf(saved, 's')).toBe(regenerated(s)); + expect(objectOf(saved, 's')).not.toContain('1.0E+6'); + expect(objectOf(saved, 't')).toBe(objectOf(xml, 't')); + // Which keeps the saved chunk one the reader pairs: reopened, t, renamed, still keeps its + // bytes. Had the merge been written, no entry of the reopened chunk would have had any. + const again = fromChunk(saved); + expect(entrySource.entrySourceOf(entryNamed(again.root, 's'))?.fragment).toBe(objectOf(saved, 's')); + expect(entryNamed(again.root, 't').setProperty('Name', 'tRenamed')).toBe(true); + expect(objectOf(savedChunk(again.session, again.root), 'tRenamed')).toBe(renamedAndStamped(objectOf(xml, 't'), 'tRenamed')); + }); }); function binaryDictionaries(): string[] { @@ -586,6 +630,28 @@ describe('for a host that splices chunk text (D12)', () => { }); } + it('spliceEntryObject lays out what it writes from the model as the chunk is: a bus element added, CRLF and two spaces', () => { + // The same bus, in the fixture's LF and four spaces and in CRLF and two spaces, given the same + // new element: the element is written from the model, which writes LF and four spaces, so only + // the chunk's own layout, read off it, makes the two splices one text in two layouts. + const { xml: lf } = open(fixture('bus_types_binary.sldd')); + const spliced = (xml: string): string => { + const { session, root } = fromChunk(xml); + const bus = entryNamed(root, 'btBus'); + expect(session.addChildTo(bus.id)).not.toBeNull(); + expect(bus.children.length).toBe(5); + const out = core.spliceEntryObject(xml, bus); + expect(out).not.toBeNull(); + return out!; + }; + const inLf = spliced(lf); + const inCrlf = spliced(crlf(lf)); + expect(objectOf(inLf, 'btBus').split('').length - 1).toBe(5); + expect(inLf).not.toContain('\r'); + expect(inCrlf).toBe(crlf(inLf)); + expect(inCrlf.replace(/\r\n/g, '')).not.toContain('\n'); + }); + it('spliceEntryObject answers null for an entry the chunk does not hold', () => { const { root, xml } = open(fixture('cellarr_binary.sldd')); expect(core.spliceEntryObject(xml, entries(root)[0], 'noSuchEntry')).toBeNull(); @@ -614,6 +680,75 @@ describe('for a host that splices chunk text (D12)', () => { expect(core.keptPartsViolations(entry, regenerated(entry))).toEqual(['P[Value] (open tag)']); }); + // Each rule keptPartsViolations holds a write to, on a write that breaks only that one. A + // Simulink.Parameter with its Min edited, whose siblings the model did not change. + const parameter = (): { entry: any; written: string } => { + const value = [ + '

', + ' ', + '

1.0E-5

', + '

-1.0E-5

', + '

1.0E+10

', + '

kept

', + ' ', + '

', + ].join('\n'); + const { session, root } = fromChunk(chunk(entryXml('p', 'u-p', value))); + const entry = entryNamed(root, 'p'); + expect(entry.setProperty('Min', '-2')).toBe(true); + const written = objectOf(savedChunk(session, root), 'p'); + expect(written).toContain('

-2.0

'); + expect(core.keptPartsViolations(entry, written)).toEqual([]); + return { entry, written }; + }; + const MAX = '\n

1.0E+10

'; + const DESCRIPTION = '\n

kept

'; + + it('keptPartsViolations: a child written twice, or with no name, where the model writes each once', () => { + const { entry, written } = parameter(); + const twice = written.replace(DESCRIPTION, DESCRIPTION + DESCRIPTION); + expect(core.keptPartsViolations(entry, twice)).toEqual(['P[Value]/Element[0]: a child of it is written twice or cannot be named']); + const unnamed = written.replace(DESCRIPTION, DESCRIPTION + '\n

kept

'); + expect(core.keptPartsViolations(entry, unnamed)).toEqual(['P[Value]/Element[0]: a child of it is written twice or cannot be named']); + }); + + it('keptPartsViolations: a child the model did not change, not written', () => { + const { entry, written } = parameter(); + expect(written).toContain(MAX); + expect(core.keptPartsViolations(entry, written.replace(MAX, ''))).toEqual(['P[Value]/Element[0]/P[Max] (not written)']); + // The one the model changed may go: what is written there is the model's to say. + expect(core.keptPartsViolations(entry, written.replace('\n

-2.0

', ''))).toEqual([]); + }); + + it('keptPartsViolations: a property the reader does not read, not kept (D13)', () => { + const unread = '\n

kept as read

'; + const xml = chunk(entryXml('a', 'u-a', '

1.0E+6

').replace('\n

{ + const { xml } = open(fixture('bus_types_binary.sldd')); + const { session, root } = fromChunk(xml); + const bus = entryNamed(root, 'btBus'); + expect(session.deleteNodeById(bus.children[1].id)).toBe(true); + expect(bus.children.length).toBe(3); + const written = objectOf(savedChunk(session, root), 'btBus'); + expect(core.keptPartsViolations(bus, written)).toEqual([]); + const at = written.lastIndexOf(''); + const last = written.slice(written.lastIndexOf('\n', at), written.indexOf('', at) + ''.length); + expect(last).toContain('

matrix

'); + const path = 'P[Value]/Element[0]/P[Elements_internal]'; + expect(core.keptPartsViolations(bus, written.replace(last, ''))).toEqual([`${path}: written with 2 elements where the model holds 3`]); + expect(core.keptPartsViolations(bus, written.replace(last, last + last))).toEqual([`${path}: written with 4 elements where the model holds 3`]); + }); + it('readEntryFragment rebuilds an entry with the bytes it came from, as an undo that rebuilds from text does', () => { const { session, root, xml } = openOlderSave(fixture('cellarr_binary.sldd')); const old = entryNamed(root, 'cObjArr'); diff --git a/test/matNodeCeiling.test.ts b/test/matNodeCeiling.test.ts index 575123ff..6e73b3e2 100644 --- a/test/matNodeCeiling.test.ts +++ b/test/matNodeCeiling.test.ts @@ -11,15 +11,17 @@ // // Every case opens the file through MatNode.fromParsed, the path addMatSource takes, twice: // once under the ceiling the case is about and once with no ceiling it can reach, which is the -// tree every file opened as before. What a cut tree shows is graded against that one. +// tree every file opened as before. What a cut tree shows is graded against that one. The last +// case opens other files after a .mat one, to see that the ceiling was armed for that file alone. import { describe, it, expect } from 'vitest'; import { readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { deflateSync } from 'node:zlib'; -import { createSession } from '../src/index.js'; +import { createSession, ingest } from '../src/index.js'; import MatNode from '../src/datamodel/node/container/MatNode.js'; import MatlabVariableNode from '../src/datamodel/node/data/MatlabVariableNode.js'; +import { buildUnderCeiling } from '../src/datamodel/node/data/matNodeCeiling.js'; import { parseMat } from '../src/datamodel/parser/MatParser.js'; import { encodeMatStream } from '../src/datamodel/parser/MatWriter.js'; import { MAT_NODE_CEILING } from '../src/datamodel/display/DisplayConvention.js'; @@ -368,4 +370,44 @@ describe('a .mat file past its node ceiling', () => { const value = JSON.parse(copied(big)).value; expect([value._dimensions, value._elements.length, value._elements[299_999]]).toEqual([[1, 300_000], 300_000, { v: 299_999 }]); }); + + it('is armed only while a .mat file is built: what opens after one used up, or after a build that threw, is whole', () => { + // matNodeCeiling arms the ceiling for one .mat file's tree and sets it back in a finally. + // Nothing else is built under it: a text dictionary's cdata and a binary one's hex, each a + // value read out of a MAT stream, and a model's workspace. Each holds sparse arrays, built + // with their rows as a .mat file's are. A ceiling left armed, used up, would build them + // with none. + // Each opened on its own: how many nodes, how many of them a MAT stream's value built with + // rows, and which have none (`_cutByCeiling`). + const others = (): [string, number, number, string[]][] => + ['sparse_text.sldd', 'sparse_binary.sldd', 'sparse_ws.slx'].map((name) => { + const u8 = new Uint8Array(readFileSync(fileURLToPath(new URL('fixtures/sparse/' + name, import.meta.url)))); + const source: Any = ingest(createSession(), u8.buffer as ArrayBuffer, { filename: name }); + const nodes = nodesUnder(source); + const withRows = nodes.filter((n) => n._matVar && n.children.length > 0).length; + return [name, nodes.length, withRows, nodes.filter((n) => n._cutByCeiling).map((n) => n.matlabPath)]; + }); + const before = others(); + for (const [name, , withRows, cut] of before) { + expect([name, withRows > 0, cut]).toEqual([name, true, []]); + } + + // A .mat file whose ceiling is used up: everything after its first variable has no rows. + const { root, warnings } = open(everyKind(), 21); + expect([shownWithoutContents(root).length, warnings.map((w) => w.code)]).toEqual([12, ['part-not-expanded']]); + expect(others()).toEqual(before); + + // A build that throws after it used up its ceiling: the throw reaches the caller, and the + // ceiling is not left armed. + const parsed = parseMat(everyKind()); + const thrown = (): unknown => + buildUnderCeiling(21, () => { + const node = new MatNode('threw.mat'); + MatNode._addVariables(node, parsed); + expect(shownWithoutContents(node).length).toBe(12); + throw new Error('the build failed'); + }); + expect(thrown).toThrow('the build failed'); + expect(others()).toEqual(before); + }); }); diff --git a/test/parity/matlab/DESIGN.md b/test/parity/matlab/DESIGN.md index 69587c23..8bc4a6fd 100644 --- a/test/parity/matlab/DESIGN.md +++ b/test/parity/matlab/DESIGN.md @@ -41,7 +41,7 @@ can. | case | display | note | |---|---|---| -| empty numeric, any class, shape and complexity | `[ ]` | MATLAB's Workspace browser's own text, space inside (`mat2str` says `[]`). An int-backed enumeration's empty too: MATLAB counts it numeric. Not a sparse double's: see the next row | +| empty numeric, any class, shape and complexity | `[ ]` | MATLAB's Workspace browser's own text, space inside (`mat2str` says `[]`). An int-backed enumeration's empty too, as a text dictionary holds it: MATLAB counts it numeric. A binary dictionary's and a `.mat`'s is not `[ ]`: see the kept list below. Not a sparse double's: see the next row | | empty sparse double | `<0x0 sparse double>`, `<2x0 sparse double>` | **a deliberate deviation from MATLAB**, whose browser shows `[ ]` (measured at 0x0 and 2x0). See "The sparse empty keeps its summary" below | | empty cell | `{}` at 0x0, `<0x3 cell>` at any other empty shape | the browser's `{}` and `{0×3 cell}` | | a text dictionary's `"value": null` | `[ ]`, a 1x0 double, with no parse warning | what MATLAB reads the entry back as; MATLAB writes `null` for a value it could not save in a text dictionary (a 2x2x2 string), and opens the file again without complaint. It read as `0` (P8 of the 2026-10-16 list) | @@ -104,13 +104,35 @@ logical-backed enumeration. A complex scalar is its own class (`int16`, not `double`), and so is each element of a complex array. getWorkspaceDisplay's own Class (`complex double`) is a different spelling; the browser's is the one shown. -**Only the rows above that cite the browser follow it so far.** Whether the rest -follow MATLAB is pending the maintainer's rulings on numeric precision (P1), -the summary's spelling (Q1: MATLAB writes `0×3 string`, `{0×3 cell}`, with no -angle brackets), and H1-H6 (the Parameter row, the edit seed, 1x1 cells, char -control characters, length bounds), in the 2026-10-15 display study -(data-explorer-vscode `docs/deep-work/2026-10-15-decided-items/display-study/`, -`proposed-open-questions.md` and `not-followable-*.md`). +**The rest differs from the browser on purpose, as recorded deviations.** The +maintainer's ruling of 2026-10-15 (evening, group B, display alignment): "if our +display makes more sense than MATLAB, keep ours. For example, to be consistent, we +always use `` for summary, then keep it. Only fix our display that +could cause confusion to user." A display that could mislead (a wrong class, size, +shape or value, a value that looks like a different one, or `null` / `unknown` where +the file holds the information) was fixed: P1-P8 and P10 of the 2026-10-16 list. +The rows below are kept. The browser's side of each was measured in R2027a by the +2026-10-15 display study (data-explorer-vscode +`docs/deep-work/2026-10-15-decided-items/display-study/`, `not-followable-*.md` and +`vs-matlab.md`). Q1, P1 and H1-H6 are that study's ids, so the study's P1 is +precision, not the 2026-10-16 list's P1. + +| kept | here | MATLAB's Workspace browser | why | +|---|---|---|---| +| Q1, a summary's spelling | `<1x11 int8>`, `<0x3 cell>` | `1×11 int8`, `{0×3 cell}` | one spelling for every summary. The angle brackets are the italic signal, and the host withholds the in-place editor on them | +| P1 (the study's), precision | full JS round-trip precision, `3.141592653589793` | the user's `format`, short by default: `3.1416`, so `pi` and `3.14159` look the same, and `[1e-10 1]` is `[0,1]` | the host filters and compares on the cell text | +| H1, a Parameter's entry row | its value: `500`, `DispColor.Green` | `1×1 Parameter`, whatever the value is | a Parameter with a scalar value has no Value row of its own, so its entry cell is the only place the value shows and is edited | +| H2, the edit text | none of its own: the cell text seeds the editor | a separate text, in `format long` (`FormatDataUtils.getDisplayEditValue`) | the cell is already a valid literal at full precision | +| H4, a 1x1 cell | `{1}` | `1` | the browser tells `{1}` from `1` only by the Class column, which the host hides by default | +| H6, the length guard | a summary past `SUMMARY_MAX_CHARS = 1000` (see "Threshold rule") | the whole text, up to 99,999 characters | a runaway guard against a 100 KB grid cell | +| logical | `true` / `false` | `1` / `0` | | +| quotes in char and string | doubled: `'it''s'`, `"say ""hi"""` | as they are: `'it's'`, `"say "hi""` | the browser's text is no literal; this one is | +| cell elements | comma-separated: `{1, 'two', [3 4]}` | space-separated: `{1 'two' [3 4]}` | | +| P9 of the 2026-10-16 list, a `.mat` `Simulink.VariantExpression` | `<1x1 Simulink.Variant>`, Class `Simulink.Variant` | Class `Simulink.VariantExpression` | the class name the `.mat` itself records, and the class's documented former name. A dictionary's reads as a VariantExpression, with its condition. No fixture of this repo holds one | +| an int-backed enumeration's empty, from a binary dictionary or a `.mat` | `<1x0 DispLevel>`, `<0x0 DispColor>` | `[ ]` | read there as an enumeration value (an ObjectNode), it keeps the summary every empty object array has. It names the class and the size, so nothing on it misleads. A text dictionary's is `[ ]` (the first row above) | + +H3 is the evidence for the study's P1. H5, control characters, is not kept: it is P7 +of the 2026-10-16 list, fixed (the char row above). ### A property sheet is not a table cell diff --git a/test/parity/matlab/README.md b/test/parity/matlab/README.md index bb7f1205..fe163d3b 100644 --- a/test/parity/matlab/README.md +++ b/test/parity/matlab/README.md @@ -126,6 +126,7 @@ directory, because `Simulink.data.dictionary.open` rejects a relative one with | `probe_parameter_value.m` | which values does a `Simulink.Parameter` refuse as its `Value`, at the top level and as a field of a struct Value, and with which message? And which structural edits inside it (removing or adding a field or an array element) leave a Value it refuses? (C-160, C-042, A2: core mirrors the answers at edit time) | `$PARAM_VALUE_OUT` or `tempdir/paramvalue` | `parameter_value_truth.json` (written by `27.1.0.3400455 (R2027a) Prerelease`) | | `probe_save_readbacks.m` + `save_readbacks.mjs` | does MATLAB reopen what core SAVES as what it was: a config set regenerated and pasted into text (M1, D15), enumeration values and empty ones (M2), an emptied and a new bus and the two spellings older saves gave one (M3)? The `.mjs` makes the saves through core's API; graded by `../readbacks.truth.test.ts`. M1 needs two corpus files (`DEX_READBACKS_CORPUS`). Ends `SAVE_READBACKS OK` | `$SAVE_READBACKS_OUT` | `save_readbacks.truth.json` (here), written by `27.1.0.3409468 (R2027a) Prerelease` | | `probe_m9_merged_reopen.m` + `m9_merged_saves.mjs` | does MATLAB reopen S2's merged saves of a binary dictionary (an edited entry saved as its bytes, with only what the edit changed written from the model) and read every entry as it should (M9)? In every compressed-binary fixture under `test/fixtures` and `test/parity/artifacts`, and in a copy of each as an earlier core save left it (1x1 cells spelled `Dimension="1*1"`, so that the merge differs from writing the entry whole), one entry of each kind is renamed, has its Description set, and where it is cheap a bus element's Description or a struct field edited, each through `session.editProperty` and saved by `serializeSource` into a file of its own. MATLAB opens each save and a copy of its original: every unedited entry must read as in the original (class, `isequaln`, `fullsig`), the edited one as the original with the same edit made in MATLAB, with no error or warning. Opens only the copies and the saves in `$M9_OUT`; graded by `../m9MergedSaves.truth.test.ts`. Ends `M9_MERGED_REOPEN OK` | `$M9_OUT` | `m9_merged_saves.truth.json` (here), written by `27.1.0.3409468 (R2027a) Prerelease` | +| `probe_old_saves_readback.m` + `old_saves.mjs` | what does MATLAB read of a binary dictionary core saved before two of its fixes, and of what core copies now from a binary dictionary into a text one (M4, M7, D15)? `../oldsaves/` holds untouched saves, by core built at `50549bd^`, `69664b0^` and `90caf06^`, of every binary fixture holding a dynamic property or a struct entry of no element, and of `old_saves_r2027a_binary.sldd` (`make_old_saves_r2027a.m`: an AUTOSAR config set, a config set holding an `SSC.SimscapeCC`, a `dexdata.DerivedSignal` beside a `Simulink.Signal`, and empty structs of five shapes); the `.mjs` makes them once per build (`save`), and the pastes into the text twin with this checkout (`cases`). MATLAB opens each save and paste and a copy of its original: every entry as in the original, each dynamic property by `get_param`, each empty struct's shape. Opens only the files in `$OLD_SAVES_OUT`; graded by `../oldSaves.truth.test.ts`. Ends `OLD_SAVES_READBACK OK` | `$OLD_SAVES_OUT` | `old_saves_readback.truth.json` (here), written by `27.1.0.3409468 (R2027a) Prerelease` | | `probe_fieldless_struct_readback.m` + `fieldless_struct_readback.mjs` | does MATLAB read back core's one-stream spelling of a field-less `repmat(struct(),1e4,1e4)` (a 132-byte cdata in text, a 72-byte hex stream in binary), as an entry, a struct field and a cell's element, where its own spelling is 1e8 elements? (F1-9; graded by `../../fieldlessStructReadback.test.ts`). Ends `FIELDLESS_STRUCT_READBACK OK` | `$FIELDLESS_OUT` | `fieldless_struct_readback.truth.json` (here), from `../../fixtures/mcos/fieldless_1e4x1e4.mat` (MATLAB's `save`), written by `27.1.0.3409468 (R2027a) Prerelease` | | `probe_text_empty_readback.m` | how does MATLAB read back a classed or shaped EMPTY out of a text dictionary, at an entry, a cell, a struct field, a Parameter's Value and an object's property, in each candidate spelling; and how does it spell one there itself? (1A-TEXT, CELL-EMPTIES-87, D8; graded by `test/textEmptyReadback.test.ts`). Ends `TEXT_EMPTY_READBACK OK` | `$TEXT_EMPTY_OUT` or `tempdir/textempty` | `text_empty_readback.truth.json` (here) and `../../fixtures/text_empty_readback_binary.sldd`, written by `27.1.0.3409468 (R2027a) Prerelease` | | `probe_display.m` + `probe_display_catalog.m` | what does MATLAB's Workspace browser show in its Value, Size and Class columns for 283 values, and how do the same values read back from a `.mat` and both dictionary flavours? (the 2026-10-15 display ruling: Value and Class follow the browser). Ends `DONE t=…` | `$DISPLAY_OUT` or `tempdir/dexdisp` | `display/`: `browser_short.truth.json` and `meta.truth.json` (the browser's own column code, `ClassCol` / `ValueCol` / `ObjectValueSummary`, written by `27.1.0.3400455 (R2027a) Prerelease`), and `dispcat.mat`, `dispcat_text.sldd`, `dispcat_bin.sldd`, cut to the values the display tests read by `display/trim-display-fixtures.mjs` | diff --git a/test/parity/matlab/old_saves.mjs b/test/parity/matlab/old_saves.mjs new file mode 100644 index 00000000..a343ba36 --- /dev/null +++ b/test/parity/matlab/old_saves.mjs @@ -0,0 +1,277 @@ +// Copyright 2026 The MathWorks, Inc. +// +// The core half of probe_old_saves_readback.m: what MATLAB R2027a is asked to reopen about +// saves core made before two fixes of the 2026-10-13 save-fidelity plan (its M4 and M7), and +// about copies core makes now from a binary dictionary into a text one. +// +// OLD SAVES. Before #93 (50549bd) a binary dictionary's reader dropped the Source="dynamic" +// and PropertyType="..." MATLAB writes on the options a custom or AUTOSAR target adds to its +// Simulink.STFCustomTargetCC, so a save wrote each as an ordinary property (M4); it also named +// a mixed-class object array's elements all after the first. Before 90caf06 the writer of a +// struct entry wrote a struct of no element around an `` it never had, and without the +// `` of the fields it declares (M7). Every save then wrote every entry from the +// model, edited or not. A file saved by such a core is still on a user's disk; the question is +// what MATLAB, and core now, read of it. +// +// The saves were made once, each an untouched save (session.serializeSource with nothing +// edited) of every original oldSaveOriginals names, by core built at each commit BUILDS names, +// and are committed beside the originals in test/parity/oldsaves/ (the builds are not in the +// tree, so a test cannot make them again): +// node test/parity/matlab/old_saves.mjs save +// once per build, from a checkout of that commit with dist built. 'pre69664b0' is #93 less its +// last commit: it keeps the element classes and drops the dynamic attributes, so that M4 is +// asked about alone. +// +// PASTES. pastesOf copies entries as a host copies one (the entry's serialize(), handed to the +// target section's parseEntry under a new name and uuid) into old_saves_r2027a_text.sldd, +// MATLAB's own text twin of the binary original, and saves the text: +// - from the binary original, each entry holding a mixed-class object array (since D15, +// 83cc70a, read under the base class MATLAB's text format names): a user's subclass of +// Simulink.Signal beside a Simulink.Signal, and two config sets' Components, one of them +// holding the AUTOSAR target's dynamic properties; one save per entry and one with all four; +// - the struct entries of no element, and the struct entry holding two, from the binary +// original and from its pre90caf06 save, as core reads each now: one save of each. +// The probe's inputs are written by +// node test/parity/matlab/old_saves.mjs cases +// with this checkout's dist built: the pastes, the old saves and their originals' copies, and +// cases.json. test/parity/oldSaves.truth.test.ts makes the pastes again from source and asks +// that each is still the one MATLAB was asked about. +// +// `cases` (cases.json, for the probe): one row per old save ({kind: 'old save', build, fixture, +// file, original, sha, dynamic, emptyStructs}) and one per paste ({kind: 'paste', file, +// original, source, pasted: [{section, from, to}], dynamic, sha}). `dynamic` lists every dynamic +// property the original (or the pasted entry's source) holds, as it holds it ({entry, to, name, +// value}), and `emptyStructs` every struct of no element at an entry ({entry, dims, fields}), +// so that the probe can ask MATLAB about each. +import { createHash } from 'node:crypto'; +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, relative } from 'node:path'; +import { strFromU8, unzipSync } from 'fflate'; + +const CHUNK = 'data/chunk0.xml'; +const ab = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength); +const sha = (data) => createHash('sha256').update(data).digest('hex'); + +/** The builds, each named for the commit it is the parent of. */ +export const BUILDS = { pre93: '50549bd^', pre69664b0: '69664b0^', pre90caf06: '90caf06^' }; +/** The originals' home, and the saves', under test/. */ +export const OLD_SAVES_DIR = 'parity/oldsaves'; +export const GENERATED = `${OLD_SAVES_DIR}/old_saves_r2027a_binary.sldd`; +export const TEXT_TWIN = `${OLD_SAVES_DIR}/old_saves_r2027a_text.sldd`; + +/** Each `…` of a chunk, as [name, object], in order. */ +export function objectsOf(xml) { + const out = []; + let at = 0; + for (;;) { + const s = xml.indexOf('', at); + if (s < 0) return out; + const e = xml.indexOf('', s) + ''.length; + const object = xml.slice(s, e); + const m = /

([^<]*)<\/P>/.exec(object); + out.push([m ? m[1] : '', object]); + at = e; + } +} + +/** + * What a binary dictionary's chunk spells of the two shapes: every dynamic property (a `

` + * with Source="dynamic"), and every struct at an entry's Value whose Dimension holds no + * element, with the fields it declares and whether it is written around an ``. + */ +export function shapesOf(bytes) { + const xml = strFromU8(unzipSync(bytes)[CHUNK]); + const dynamic = []; + const emptyStructs = []; + for (const [entry, object] of objectsOf(xml)) { + for (const m of object.matchAll(/

([^<]*)<\/P>/g)) { + dynamic.push({ entry, name: m[1], value: m[2] }); + } + const value = /\n( *)

/.exec(object); + if (value && value[2].split('*').some((n) => n === '0')) { + const body = value[3] ? '' : object.slice(value.index + value[0].length, object.indexOf(`\n${value[1]}

`, value.index)); + emptyStructs.push({ + entry, + dims: value[2].split('*').map(Number), + fields: [...body.matchAll(/^ *$/gm)].map((f) => f[1]), + aroundElement: //.test(body), + }); + } + } + return { dynamic, emptyStructs }; +} + +/** The `

` line of each named property of an entry, as a chunk spells it, or null: how a save wrote it. */ +export function linesOf(bytes, props) { + const objects = new Map(objectsOf(strFromU8(unzipSync(bytes)[CHUNK]))); + return props.map(({ entry, name }) => { + const line = new RegExp(`

]*>[^<]*

`).exec(objects.get(entry) ?? ''); + return line ? line[0] : null; + }); +} + +/** + * The originals of the old saves: every compressed-binary .sldd under test/fixtures and + * test/parity/artifacts that holds a dynamic property or a struct of no element at an entry, + * and old_saves_r2027a_binary.sldd, which make_old_saves_r2027a.m wrote for the shapes none of + * them holds; as [{ rel, bytes }], in path order. + */ +export function oldSaveOriginals(testDir) { + const out = []; + const walk = (dir) => { + for (const name of readdirSync(dir).sort()) { + const path = join(dir, name); + if (statSync(path).isDirectory()) { + walk(path); + } else if (name.endsWith('.sldd')) { + const bytes = new Uint8Array(readFileSync(path)); + if (bytes[0] === 0x50 && bytes[1] === 0x4b) { + const { dynamic, emptyStructs } = shapesOf(bytes); + if (dynamic.length + emptyStructs.length > 0) { + out.push({ rel: relative(testDir, path).split('\\').join('/'), bytes }); + } + } + } + } + }; + for (const sub of ['fixtures', join('parity', 'artifacts')]) { + walk(join(testDir, sub)); + } + out.push({ rel: GENERATED, bytes: new Uint8Array(readFileSync(join(testDir, GENERATED))) }); + return out; +} + +const fileTag = (rel) => rel.split('/').pop().replace(/\.sldd$/, '').replace(/[^A-Za-z0-9]+/g, '_'); +/** The name of a build's save of an original, and of the original's copy beside it. */ +export const saveName = (build, rel) => `${build}_${fileTag(rel)}.sldd`; +export const originalName = (rel) => `orig_${fileTag(rel)}.sldd`; + +/** An untouched save of `bytes`, by `core` ({ createSession, ingest }): what a save by that core wrote of every entry. */ +export function untouchedSave(core, bytes, filename) { + const session = core.createSession(); + const root = core.ingest(session, ab(bytes), { filename }); + return session.serializeSource(root.name).bytes; +} + +/** The old-save rows of cases.json, for the saves in `dir` (as saveName names them) of `originals`. */ +export function oldSaveCases(originals, dir) { + const cases = []; + for (const build of Object.keys(BUILDS)) { + for (const { rel, bytes } of originals) { + const saved = new Uint8Array(readFileSync(join(dir, saveName(build, rel)))); + const { dynamic, emptyStructs } = shapesOf(bytes); + cases.push({ + kind: 'old save', + build, + fixture: rel, + file: saveName(build, rel), + original: originalName(rel), + sha: { save: sha(saved), original: sha(bytes) }, + dynamic: dynamic.map(({ entry, name, value }) => ({ entry, to: entry, name, value })), + emptyStructs: emptyStructs.map(({ entry, dims, fields }) => ({ entry, dims, fields })), + }); + } + } + return cases; +} + +/** The entries of the binary original that hold a mixed-class object array, by section. */ +export const MIXED = [ + { section: 'other', from: 'holdsMixed' }, + { section: 'other', from: 'holdsBase' }, + { section: 'config', from: 'csAutosar' }, + { section: 'config', from: 'csSimscape' }, +]; +/** The struct entries of no element in the binary original, and the one holding two such fields. */ +export const EMPTIES = ['sE00', 'sE02', 'sE20', 'sE002', 'sE10ab', 'sHolds'].map((from) => ({ section: 'design', from })); +/** The old save the empties are also pasted from: one that wrote them around an ``, and nothing else otherwise. */ +export const EMPTIES_OLD_SAVE = saveName('pre90caf06', GENERATED); + +/** + * The pastes, as { files, cases }. `sources` names each source's bytes: the binary original + * (`orig_old_saves_r2027a_binary.sldd`) and EMPTIES_OLD_SAVE; `text` is the text twin's. + */ +export function pastesOf(core, sources, text) { + const { createSession, ingest } = core; + const ORIGINAL = originalName(GENERATED); + const sets = [ + ...MIXED.map((p) => [ORIGINAL, [p]]), + [ORIGINAL, MIXED], + [ORIGINAL, EMPTIES], + [EMPTIES_OLD_SAVE, EMPTIES], + ]; + const files = {}; + const cases = []; + sets.forEach(([sourceName, set], k) => { + const session = createSession(); + const source = ingest(session, ab(sources[sourceName]), { filename: sourceName }); + const host = ingest(session, strFromU8(text), { filename: originalName(TEXT_TWIN) }); + const pasted = []; + for (const { section, from } of set) { + const entry = source.getSection(section).children.find((e) => e.name === from); + const payload = JSON.parse(JSON.stringify(entry.serialize())); + const to = `${from}Pasted`; + const uuid = `b27b27b2-0000-4000-8000-${String((k + 1) * 100 + pasted.length + 1).padStart(12, '0')}`; + host.getSection(section).parseEntry({ ...payload, name: to, metadata: { ...payload.metadata, uuid } }); + pasted.push({ section, from, to }); + } + const saved = session.serializeSource(host.name).text; + const file = `paste_${k + 1}.sldd`; + files[file] = saved; + const dynamic = shapesOf(sources[sourceName]).dynamic.flatMap((d) => { + const p = pasted.find((x) => x.section === 'config' && x.from === d.entry); + return p ? [{ entry: d.entry, to: p.to, name: d.name, value: d.value }] : []; + }); + cases.push({ kind: 'paste', file, original: originalName(TEXT_TWIN), source: sourceName, pasted, dynamic, sha: sha(saved) }); + }); + return { files, cases }; +} + +/** The two sources of the pastes, from the tree. */ +export function pasteSources(testDir) { + return { + [originalName(GENERATED)]: new Uint8Array(readFileSync(join(testDir, GENERATED))), + [EMPTIES_OLD_SAVE]: new Uint8Array(readFileSync(join(testDir, OLD_SAVES_DIR, EMPTIES_OLD_SAVE))), + }; +} + +// As a script. +if (process.argv[1] && import.meta.url === new URL('file://' + process.argv[1]).href) { + const { mkdirSync, writeFileSync, copyFileSync } = await import('node:fs'); + const { fileURLToPath } = await import('node:url'); + const here = fileURLToPath(new URL('.', import.meta.url)); + const testDir = join(here, '..', '..'); + const [mode, ...rest] = process.argv.slice(2); + if (mode === 'save') { + const [checkout, build, outDir] = rest; + if (!BUILDS[build] || !outDir) throw new Error(`usage: node old_saves.mjs save <${Object.keys(BUILDS).join('|')}> `); + const core = await import(join(checkout, 'dist', 'index.js')); + mkdirSync(outDir, { recursive: true }); + for (const { rel, bytes } of oldSaveOriginals(testDir)) { + writeFileSync(join(outDir, saveName(build, rel)), untouchedSave(core, bytes, rel.split('/').pop())); + } + console.log('saved', build); + } else if (mode === 'cases') { + const [outDir] = rest; + if (!outDir) throw new Error('usage: node old_saves.mjs cases '); + const dist = join(testDir, '..', 'dist'); + const core = await import(join(dist, 'index.js')); + await import(join(dist, 'datamodel', 'node', 'data', 'NodeClassMap.js')); + const { files, cases } = pastesOf(core, pasteSources(testDir), new Uint8Array(readFileSync(join(testDir, TEXT_TWIN)))); + mkdirSync(outDir, { recursive: true }); + for (const [name, data] of Object.entries(files)) writeFileSync(join(outDir, name), data); + copyFileSync(join(testDir, TEXT_TWIN), join(outDir, originalName(TEXT_TWIN))); + const originals = oldSaveOriginals(testDir); + for (const { rel, bytes } of originals) { + writeFileSync(join(outDir, originalName(rel)), bytes); + for (const build of Object.keys(BUILDS)) { + copyFileSync(join(testDir, OLD_SAVES_DIR, saveName(build, rel)), join(outDir, saveName(build, rel))); + } + } + const old = oldSaveCases(originals, join(testDir, OLD_SAVES_DIR)); + writeFileSync(join(outDir, 'cases.json'), JSON.stringify([...old, ...cases], null, 2) + '\n'); + console.log('wrote', Object.keys(files).length, 'pastes and', old.length + cases.length, 'cases to', outDir); + } else { + throw new Error('usage: node old_saves.mjs save|cases ...'); + } +} diff --git a/test/parity/matlab/old_saves_readback.truth.json b/test/parity/matlab/old_saves_readback.truth.json new file mode 100644 index 00000000..e7baa058 --- /dev/null +++ b/test/parity/matlab/old_saves_readback.truth.json @@ -0,0 +1,2142 @@ +{ + "version": "27.1.0.3409468 (R2027a) Prerelease", + "release": "2027a", + "date": "2026-10-18", + "originals": [ + { + "file": "orig_dispcat_bin.sldd", + "opened": true, + "warnings": [], + "entries": 76, + "notSelfEqual": [], + "shapes": [ + { + "key": "design|c_newline", + "shape": "char[1 3]" + }, + { + "key": "design|c_nul", + "shape": "char[1 3]" + }, + { + "key": "design|c_tab", + "shape": "char[1 3]" + }, + { + "key": "design|cell_1", + "shape": "cell[1 1]" + }, + { + "key": "design|cell_of_param", + "shape": "cell[1 1]" + }, + { + "key": "design|dx_cell_0x0", + "shape": "cell[0 0]" + }, + { + "key": "design|dx_cell_0x3", + "shape": "cell[0 3]" + }, + { + "key": "design|dx_cell_1x0", + "shape": "cell[1 0]" + }, + { + "key": "design|dx_char_0x0", + "shape": "char[0 0]" + }, + { + "key": "design|dx_char_0x2", + "shape": "char[0 2]" + }, + { + "key": "design|dx_char_1x0", + "shape": "char[1 0]" + }, + { + "key": "design|dx_complex_0x0", + "shape": "double[0 0]" + }, + { + "key": "design|dx_complex_1x0", + "shape": "double[1 0]" + }, + { + "key": "design|dx_complex_int16", + "shape": "int16[1 1]" + }, + { + "key": "design|dx_complex_int16_1x0", + "shape": "int16[1 0]" + }, + { + "key": "design|dx_complex_scalar", + "shape": "double[1 1]" + }, + { + "key": "design|dx_complex_vector", + "shape": "double[1 2]" + }, + { + "key": "design|dx_double_0x0", + "shape": "double[0 0]" + }, + { + "key": "design|dx_double_0x0x2", + "shape": "double[0 0 2]" + }, + { + "key": "design|dx_double_0x1", + "shape": "double[0 1]" + }, + { + "key": "design|dx_double_0x3", + "shape": "double[0 3]" + }, + { + "key": "design|dx_double_1x0", + "shape": "double[1 0]" + }, + { + "key": "design|dx_enum_0x0", + "shape": "matlab.lang.OnOffSwitchState[0 0]" + }, + { + "key": "design|dx_enum_1x0", + "shape": "matlab.lang.OnOffSwitchState[1 0]" + }, + { + "key": "design|dx_int8_0x3", + "shape": "int8[0 3]" + }, + { + "key": "design|dx_logical_1x0", + "shape": "logical[1 0]" + }, + { + "key": "design|dx_param_complex_1x0", + "shape": "Simulink.Parameter[1 1]" + }, + { + "key": "design|dx_param_empty", + "shape": "Simulink.Parameter[1 1]" + }, + { + "key": "design|dx_single_0x0", + "shape": "single[0 0]" + }, + { + "key": "design|dx_sparse_0x0", + "shape": "double[0 0]" + }, + { + "key": "design|dx_sparse_2x0", + "shape": "double[2 0]" + }, + { + "key": "design|dx_string_0x0", + "shape": "string[0 0]" + }, + { + "key": "design|dx_string_1x0", + "shape": "string[1 0]" + }, + { + "key": "design|dx_string_empty_1x1", + "shape": "string[1 1]" + }, + { + "key": "design|dx_struct_0x0", + "shape": "struct[0 0]{}" + }, + { + "key": "design|dx_struct_0x0_fields", + "shape": "struct[0 0]{a}" + }, + { + "key": "design|dx_struct_1x0", + "shape": "struct[1 0]{a}" + }, + { + "key": "design|e_logical_0x0", + "shape": "logical[0 0]" + }, + { + "key": "design|e_logical_0x3", + "shape": "logical[0 3]" + }, + { + "key": "design|e_single_complex_1x0", + "shape": "single[1 0]" + }, + { + "key": "design|e_sparse_logical_0x0", + "shape": "logical[0 0]" + }, + { + "key": "design|e_uint64_0x0", + "shape": "uint64[0 0]" + }, + { + "key": "design|en_i32_empty", + "shape": "DispLevel[1 0]" + }, + { + "key": "design|en_i32_high", + "shape": "DispLevel[1 1]" + }, + { + "key": "design|en_i32_vec", + "shape": "DispLevel[1 2]" + }, + { + "key": "design|en_off", + "shape": "matlab.lang.OnOffSwitchState[1 1]" + }, + { + "key": "design|en_on", + "shape": "matlab.lang.OnOffSwitchState[1 1]" + }, + { + "key": "design|en_onoff_vec", + "shape": "matlab.lang.OnOffSwitchState[1 3]" + }, + { + "key": "design|en_sl_empty", + "shape": "DispColor[0 0]" + }, + { + "key": "design|en_sl_red", + "shape": "DispColor[1 1]" + }, + { + "key": "design|en_sl_vec", + "shape": "DispColor[1 2]" + }, + { + "key": "design|enumdef", + "shape": "Simulink.data.dictionary.EnumTypeDefinition[1 1]" + }, + { + "key": "design|fi_vec", + "shape": "embedded.fi[1 3]" + }, + { + "key": "design|i8_1x10", + "shape": "int8[1 10]" + }, + { + "key": "design|i8_1x11", + "shape": "int8[1 11]" + }, + { + "key": "design|l_1x10", + "shape": "logical[1 10]" + }, + { + "key": "design|l_1x11", + "shape": "logical[1 11]" + }, + { + "key": "design|p_enum", + "shape": "Simulink.Parameter[1 1]" + }, + { + "key": "design|sp_complex", + "shape": "double[1 2]" + }, + { + "key": "design|sp_complex_scalar", + "shape": "double[1 1]" + }, + { + "key": "design|sp_logical", + "shape": "logical[1 3]" + }, + { + "key": "design|sp_logical_scalar", + "shape": "logical[1 1]" + }, + { + "key": "design|sp_scalar", + "shape": "double[1 1]" + }, + { + "key": "design|sp_vec", + "shape": "double[1 3]" + }, + { + "key": "design|str_0x3", + "shape": "string[0 3]" + }, + { + "key": "design|str_newline", + "shape": "string[1 1]" + }, + { + "key": "design|z_1x11", + "shape": "double[1 11]" + }, + { + "key": "design|z_int16", + "shape": "int16[1 1]" + }, + { + "key": "design|z_int16_vec", + "shape": "int16[1 2]" + }, + { + "key": "design|z_int8_vec", + "shape": "int8[1 2]" + }, + { + "key": "design|z_scalar", + "shape": "double[1 1]" + }, + { + "key": "design|z_single", + "shape": "single[1 1]" + }, + { + "key": "design|z_uint8", + "shape": "uint8[1 1]" + }, + { + "key": "design|z_vector", + "shape": "double[1 2]" + }, + { + "key": "other|cat_ab", + "shape": "categorical[1 2]" + }, + { + "key": "other|tbl_2x2", + "shape": "table[2 2]" + } + ] + }, + { + "file": "orig_cases.sldd", + "opened": true, + "warnings": [], + "entries": 73, + "notSelfEqual": [], + "shapes": [ + { + "key": "design|aBus", + "shape": "Simulink.Bus[1 1]" + }, + { + "key": "design|aLookup", + "shape": "Simulink.LookupTable[1 1]" + }, + { + "key": "design|aParam", + "shape": "Simulink.Parameter[1 1]" + }, + { + "key": "design|aSignal", + "shape": "Simulink.Signal[1 1]" + }, + { + "key": "design|aVariant", + "shape": "Simulink.VariantVariable[1 1]" + }, + { + "key": "design|bigD", + "shape": "double[1 1]" + }, + { + "key": "design|boolF", + "shape": "logical[1 1]" + }, + { + "key": "design|boolT", + "shape": "logical[1 1]" + }, + { + "key": "design|boolVec", + "shape": "logical[1 3]" + }, + { + "key": "design|cell2x3", + "shape": "cell[2 3]" + }, + { + "key": "design|cellEmpty", + "shape": "cell[0 0]" + }, + { + "key": "design|cellFlat", + "shape": "cell[1 3]" + }, + { + "key": "design|cellNd", + "shape": "cell[2 3 2]" + }, + { + "key": "design|cellNest", + "shape": "cell[1 2]" + }, + { + "key": "design|charStr", + "shape": "char[1 4]" + }, + { + "key": "design|colVec", + "shape": "double[3 1]" + }, + { + "key": "design|cplxScalar", + "shape": "double[1 1]" + }, + { + "key": "design|cplxVec", + "shape": "double[1 2]" + }, + { + "key": "design|eleven", + "shape": "double[1 11]" + }, + { + "key": "design|emptyD", + "shape": "double[0 0]" + }, + { + "key": "design|exactly10", + "shape": "double[1 10]" + }, + { + "key": "design|hugeChar", + "shape": "char[1 1500]" + }, + { + "key": "design|i16Vec", + "shape": "int16[1 3]" + }, + { + "key": "design|i64Unsafe", + "shape": "int64[1 1]" + }, + { + "key": "design|i64Vec", + "shape": "int64[1 3]" + }, + { + "key": "design|infN", + "shape": "double[1 1]" + }, + { + "key": "design|infP", + "shape": "double[1 1]" + }, + { + "key": "design|kp", + "shape": "double[1 1]" + }, + { + "key": "design|long30", + "shape": "double[1 30]" + }, + { + "key": "design|longChar", + "shape": "char[1 300]" + }, + { + "key": "design|mat2x3", + "shape": "double[2 3]" + }, + { + "key": "design|maxU64", + "shape": "uint64[1 1]" + }, + { + "key": "design|max_int16", + "shape": "int16[1 1]" + }, + { + "key": "design|max_int32", + "shape": "int32[1 1]" + }, + { + "key": "design|max_int64", + "shape": "int64[1 1]" + }, + { + "key": "design|max_int8", + "shape": "int8[1 1]" + }, + { + "key": "design|max_uint16", + "shape": "uint16[1 1]" + }, + { + "key": "design|max_uint32", + "shape": "uint32[1 1]" + }, + { + "key": "design|max_uint8", + "shape": "uint8[1 1]" + }, + { + "key": "design|min_int16", + "shape": "int16[1 1]" + }, + { + "key": "design|min_int32", + "shape": "int32[1 1]" + }, + { + "key": "design|min_int64", + "shape": "int64[1 1]" + }, + { + "key": "design|min_int8", + "shape": "int8[1 1]" + }, + { + "key": "design|min_uint16", + "shape": "uint16[1 1]" + }, + { + "key": "design|min_uint32", + "shape": "uint32[1 1]" + }, + { + "key": "design|min_uint64", + "shape": "uint64[1 1]" + }, + { + "key": "design|min_uint8", + "shape": "uint8[1 1]" + }, + { + "key": "design|nanV", + "shape": "double[1 1]" + }, + { + "key": "design|nd2x3x2", + "shape": "double[2 3 2]" + }, + { + "key": "design|negD", + "shape": "double[1 1]" + }, + { + "key": "design|nonFinVec", + "shape": "double[1 5]" + }, + { + "key": "design|rowVec", + "shape": "double[1 3]" + }, + { + "key": "design|s_int16", + "shape": "int16[1 1]" + }, + { + "key": "design|s_int32", + "shape": "int32[1 1]" + }, + { + "key": "design|s_int64", + "shape": "int64[1 1]" + }, + { + "key": "design|s_int8", + "shape": "int8[1 1]" + }, + { + "key": "design|s_uint16", + "shape": "uint16[1 1]" + }, + { + "key": "design|s_uint32", + "shape": "uint32[1 1]" + }, + { + "key": "design|s_uint64", + "shape": "uint64[1 1]" + }, + { + "key": "design|s_uint8", + "shape": "uint8[1 1]" + }, + { + "key": "design|scalarD", + "shape": "double[1 1]" + }, + { + "key": "design|scalarS", + "shape": "single[1 1]" + }, + { + "key": "design|strArray", + "shape": "string[1 3]" + }, + { + "key": "design|strMat", + "shape": "string[2 3]" + }, + { + "key": "design|strScalar", + "shape": "string[1 1]" + }, + { + "key": "design|struct1x3", + "shape": "struct[1 3]{a,b}" + }, + { + "key": "design|struct2x3", + "shape": "struct[2 3]{a}" + }, + { + "key": "design|structEmpty", + "shape": "struct[0 0]{}" + }, + { + "key": "design|structNd", + "shape": "struct[2 3 2]{a}" + }, + { + "key": "design|structNest", + "shape": "struct[1 1]{a}" + }, + { + "key": "design|structScalar", + "shape": "struct[1 1]{a,b}" + }, + { + "key": "design|tinyD", + "shape": "double[1 1]" + }, + { + "key": "design|u64Vec", + "shape": "uint64[1 3]" + } + ] + }, + { + "file": "orig_old_saves_r2027a_binary.sldd", + "opened": true, + "warnings": [], + "entries": 10, + "notSelfEqual": [ + "config|csAutosar" + ], + "shapes": [ + { + "key": "design|sE00", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sE002", + "shape": "struct[0 0 2]{}" + }, + { + "key": "design|sE02", + "shape": "struct[0 2]{}" + }, + { + "key": "design|sE10ab", + "shape": "struct[1 0]{a,b}" + }, + { + "key": "design|sE20", + "shape": "struct[2 0]{}" + }, + { + "key": "design|sHolds", + "shape": "struct[1 1]{x,e,none}" + }, + { + "key": "other|holdsBase", + "shape": "NdHolder[1 1]" + }, + { + "key": "other|holdsMixed", + "shape": "NdHolder[1 1]" + }, + { + "key": "config|csAutosar", + "shape": "Simulink.ConfigSet[1 1]" + }, + { + "key": "config|csSimscape", + "shape": "Simulink.ConfigSet[1 1]" + } + ] + }, + { + "file": "orig_old_saves_r2027a_text.sldd", + "opened": true, + "warnings": [], + "entries": 10, + "notSelfEqual": [ + "config|csAutosar" + ], + "shapes": [ + { + "key": "design|sE00", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sE002", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sE02", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sE10ab", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sE20", + "shape": "struct[0 0]{}" + }, + { + "key": "design|sHolds", + "shape": "struct[1 1]{x,e,none}" + }, + { + "key": "other|holdsBase", + "shape": "NdHolder[1 1]" + }, + { + "key": "other|holdsMixed", + "shape": "NdHolder[1 1]" + }, + { + "key": "config|csAutosar", + "shape": "Simulink.ConfigSet[1 1]" + }, + { + "key": "config|csSimscape", + "shape": "Simulink.ConfigSet[1 1]" + } + ] + } + ], + "cases": [ + { + "kind": "old save", + "build": "pre93", + "fixture": "fixtures/display/dispcat_bin.sldd", + "file": "pre93_dispcat_bin.sldd", + "original": "orig_dispcat_bin.sldd", + "sha": { + "save": "c3c34f9ace2a1aec5bc41babdd58a8855ccec482803e09abd3f1ed50e7c4cce7", + "original": "043c5c7bc7c136e96284580dae7401e394b0e1bc3f338ae0043e7a94020ff1f1" + }, + "dynamic": [], + "emptyStructs": [ + { + "entry": "dx_struct_0x0", + "dims": [ + 0, + 0 + ], + "fields": [] + }, + { + "entry": "dx_struct_0x0_fields", + "dims": [ + 0, + 0 + ], + "fields": [ + "a" + ] + }, + { + "entry": "dx_struct_1x0", + "dims": [ + 1, + 0 + ], + "fields": [ + "a" + ] + } + ], + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 76, + "same": 62, + "differ": [ + "design|dx_cell_0x3: isequaln 0, sig 0: cell[0 0]{} | wanted cell[0 3]{}", + "design|dx_cell_1x0: isequaln 0, sig 0: cell[0 0]{} | wanted cell[1 0]{}", + "design|dx_char_0x2: isequaln 0, sig 0: char[0 0]='' | wanted char[0 2]=''", + "design|dx_char_1x0: isequaln 0, sig 0: char[0 0]='' | wanted char[1 0]=''", + "design|dx_string_0x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[0 0]{}", + "design|dx_string_1x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[1 0]{}", + "design|dx_struct_0x0_fields: isequaln 0, sig 0: struct[0 0]<>{} | wanted struct[0 0]{}", + "design|dx_struct_1x0: isequaln 0, sig 0: struct[1 0]<>{} | wanted struct[1 0]{}", + "design|en_i32_high: isequaln 0, sig 0: DispLevel[0 0]=[] | wanted DispLevel[1 1]=High", + "design|en_i32_vec: isequaln 0, sig 0: DispLevel[0 0]=[] | wanted DispLevel[1 2]=Low,High", + "design|en_sl_red: isequaln 0, sig 0: DispColor[0 0]=[] | wanted DispColor[1 1]=Red", + "design|en_sl_vec: isequaln 0, sig 0: DispColor[0 0]=[] | wanted DispColor[1 2]=Red,Blue", + "design|p_enum: isequaln 0, sig 0: Simulink.Parameter[1 1]{Value:DispColor[0 0]=[],Complexity:char[1 4]='real',Dimensions:double[1 2]=0,0,CoderInfo:Simulink.CoderInfo[1 1]{StorageClass:char[1 4]='Auto',TypeQualifier:char[0 0]='',Identifier:char[0 0]='',Alignment:int32[1 1]=-1,CustomStorageClass:char[1 7]='Default',CustomAttributes:SimulinkCSC.AttribClass_Simulink_Default[1 1]{disp:SimulinkCSC.AttribClass_Simulink_Default {} | wanted struct[1 0]{}", + "design|sHolds: isequaln 0, sig 0: struct[1 1]{x:double[1 1]=1,e:struct[0 0]<>{},none:struct[0 0]<>{}} | wanted struct[1 1]{x:double[1 1]=1,e:struct[0 0]{},none:struct[0 0]<>{}}", + "other|holdsBase: isequaln 0, sig 1: ',Min:double[0 0]=[],Max:double[0 0]=[],Unit:char[0 0]=''},Tag:char[1 4]='base'} | wanted ',Min:double[0 0]=[],Max:double[0 0]=[],Unit:char[0 0]=''},Tag:char[1 4]='base'}", + "other|holdsMixed: isequaln 0, sig 0: NdHolder[1 1]{Kids:dexdata.DerivedSignal[1 2]{Routing:char[1 3]='bus',Priority:int32[1 1]=5,Dimensions:double[1 1]=-1,DimensionsMode:char[1 4]='auto',Complexity:char[1 4]='auto',SampleTime:double[1 1]=-1,SamplingMode:char[1 4]='auto',InitialValue:char[0 0]='',LoggingInfo:Simulink.LoggingInfo[1 1]{DataLogging:logical[1 1]=false,NameMode:logical[1 1]=false,LoggingName:char[0 0]='',DecimateData:logic ...(1951 chars) | wanted NdHolder[1 1]{Kids:Simulink.Signal[1 2]{Dimensions:double[1 1]=-1,DimensionsMode:char[1 4]='auto',Complexity:char[1 4]='auto',SampleTime:double[1 1]=-1,SamplingMode:char[1 4]='auto',InitialValue:char[0 0]='',LoggingInfo:Simulink.LoggingInfo[1 1]{DataLogging:logical[1 1]=false,NameMode:logical[1 1]=false,LoggingName:char[0 0]='',DecimateData:logical[1 1]=false,Decimation:double[1 1]=2,LimitDataPoin ...(1853 chars)", + "config|csAutosar: isequaln 1, sig 0: mponents:Simulink.ConfigComponent[9 1]{Name:char[1 6]='Solver',Description:char[1 41]='Simulink Coverage Configuration Component',Components:double[0 0]=[],Name:char[1 18]='Data Import/Export',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 12]='Optimization',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 11]='Diagnostics',Description:char[0 0]='',Components:double[0 ...(1090 chars) | wanted mponents:Simulink.ConfigComponent[9 1]{Name:char[1 6]='Solver',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 18]='Data Import/Export',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 12]='Optimization',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 11]='Diagnostics',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 23]='Hardware Implement ...(6172 chars)", + "config|csSimscape: isequaln 0, sig 0: ,Path:char[0 0]='',Description:char[0 0]='',Components:Simulink.ConfigComponent[9 1]{Name:char[1 6]='Solver',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 18]='Data Import/Export',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 12]='Optimization',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 11]='Diagnostics',Description:char[0 0]='',Components:doub ...(1094 chars) | wanted ,Path:char[0 0]='',Description:char[0 0]='',Components:Simulink.ConfigComponent[10 1]{Name:char[1 6]='Solver',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 18]='Data Import/Export',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 12]='Optimization',Description:char[0 0]='',Components:double[0 0]=[],Name:char[1 11]='Diagnostics',Description:char[0 0]='',Components:dou ...(1466 chars)" + ] + }, + "dynamic": [ + { + "entry": "csAutosar", + "name": "AutosarMatrixIOAsArray", + "original": "off", + "save": "", + "dynamicInSave": false + }, + { + "entry": "csAutosar", + "name": "AutosarCompilerAbstraction", + "original": "off", + "save": "", + "dynamicInSave": false + }, + { + "entry": "csAutosar", + "name": "AutosarMaxShortNameLength", + "original": "128", + "save": "", + "dynamicInSave": false + }, + { + "entry": "csAutosar", + "name": "DisableAUTOSARRoutinesHostLibrary", + "original": "off", + "save": "", + "dynamicInSave": false + }, + { + "entry": "csAutosar", + "name": "AutosarSchemaVersion", + "original": "R23-11", + "save": "", + "dynamicInSave": false + } + ], + "emptyStructs": [ + { + "entry": "sE00", + "shape": "struct[0 0]{}" + }, + { + "entry": "sE002", + "shape": "struct[0 0 2]{}" + }, + { + "entry": "sE02", + "shape": "struct[0 2]{}" + }, + { + "entry": "sE10ab", + "shape": "struct[1 0]{}" + }, + { + "entry": "sE20", + "shape": "struct[2 0]{}" + } + ], + "pasted": [], + "error": "" + } + }, + { + "kind": "old save", + "build": "pre69664b0", + "fixture": "fixtures/display/dispcat_bin.sldd", + "file": "pre69664b0_dispcat_bin.sldd", + "original": "orig_dispcat_bin.sldd", + "sha": { + "save": "470a691f7d0eaaa0fe4bc98d60c22c355dfe7167607aa9efeb95bc4dd948347e", + "original": "043c5c7bc7c136e96284580dae7401e394b0e1bc3f338ae0043e7a94020ff1f1" + }, + "dynamic": [], + "emptyStructs": [ + { + "entry": "dx_struct_0x0", + "dims": [ + 0, + 0 + ], + "fields": [] + }, + { + "entry": "dx_struct_0x0_fields", + "dims": [ + 0, + 0 + ], + "fields": [ + "a" + ] + }, + { + "entry": "dx_struct_1x0", + "dims": [ + 1, + 0 + ], + "fields": [ + "a" + ] + } + ], + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 76, + "same": 62, + "differ": [ + "design|dx_cell_0x3: isequaln 0, sig 0: cell[0 0]{} | wanted cell[0 3]{}", + "design|dx_cell_1x0: isequaln 0, sig 0: cell[0 0]{} | wanted cell[1 0]{}", + "design|dx_char_0x2: isequaln 0, sig 0: char[0 0]='' | wanted char[0 2]=''", + "design|dx_char_1x0: isequaln 0, sig 0: char[0 0]='' | wanted char[1 0]=''", + "design|dx_string_0x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[0 0]{}", + "design|dx_string_1x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[1 0]{}", + "design|dx_struct_0x0_fields: isequaln 0, sig 0: struct[0 0]<>{} | wanted struct[0 0]{}", + "design|dx_struct_1x0: isequaln 0, sig 0: struct[1 0]<>{} | wanted struct[1 0]{}", + "design|en_i32_high: isequaln 0, sig 0: DispLevel[0 0]=[] | wanted DispLevel[1 1]=High", + "design|en_i32_vec: isequaln 0, sig 0: DispLevel[0 0]=[] | wanted DispLevel[1 2]=Low,High", + "design|en_sl_red: isequaln 0, sig 0: DispColor[0 0]=[] | wanted DispColor[1 1]=Red", + "design|en_sl_vec: isequaln 0, sig 0: DispColor[0 0]=[] | wanted DispColor[1 2]=Red,Blue", + "design|p_enum: isequaln 0, sig 0: Simulink.Parameter[1 1]{Value:DispColor[0 0]=[],Complexity:char[1 4]='real',Dimensions:double[1 2]=0,0,CoderInfo:Simulink.CoderInfo[1 1]{StorageClass:char[1 4]='Auto',TypeQualifier:char[0 0]='',Identifier:char[0 0]='',Alignment:int32[1 1]=-1,CustomStorageClass:char[1 7]='Default',CustomAttributes:SimulinkCSC.AttribClass_Simulink_Default[1 1]{disp:SimulinkCSC.AttribClass_Simulink_Default {} | wanted struct[1 0]{}", + "design|sHolds: isequaln 0, sig 0: struct[1 1]{x:double[1 1]=1,e:struct[0 0]<>{},none:struct[0 0]<>{}} | wanted struct[1 1]{x:double[1 1]=1,e:struct[0 0]{},none:struct[0 0]<>{}}" + ] + }, + "dynamic": [ + { + "entry": "csAutosar", + "name": "AutosarMatrixIOAsArray", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarCompilerAbstraction", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarMaxShortNameLength", + "original": "128", + "save": "128", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "DisableAUTOSARRoutinesHostLibrary", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarSchemaVersion", + "original": "R23-11", + "save": "R23-11", + "dynamicInSave": true + } + ], + "emptyStructs": [ + { + "entry": "sE00", + "shape": "struct[0 0]{}" + }, + { + "entry": "sE002", + "shape": "struct[0 0 2]{}" + }, + { + "entry": "sE02", + "shape": "struct[0 2]{}" + }, + { + "entry": "sE10ab", + "shape": "struct[1 0]{}" + }, + { + "entry": "sE20", + "shape": "struct[2 0]{}" + } + ], + "pasted": [], + "error": "" + } + }, + { + "kind": "old save", + "build": "pre90caf06", + "fixture": "fixtures/display/dispcat_bin.sldd", + "file": "pre90caf06_dispcat_bin.sldd", + "original": "orig_dispcat_bin.sldd", + "sha": { + "save": "aaa3809dab7124b097a092f2110db92a187aaae6158ac46c25823defa5f9379f", + "original": "043c5c7bc7c136e96284580dae7401e394b0e1bc3f338ae0043e7a94020ff1f1" + }, + "dynamic": [], + "emptyStructs": [ + { + "entry": "dx_struct_0x0", + "dims": [ + 0, + 0 + ], + "fields": [] + }, + { + "entry": "dx_struct_0x0_fields", + "dims": [ + 0, + 0 + ], + "fields": [ + "a" + ] + }, + { + "entry": "dx_struct_1x0", + "dims": [ + 1, + 0 + ], + "fields": [ + "a" + ] + } + ], + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 76, + "same": 68, + "differ": [ + "design|dx_cell_0x3: isequaln 0, sig 0: cell[0 0]{} | wanted cell[0 3]{}", + "design|dx_cell_1x0: isequaln 0, sig 0: cell[0 0]{} | wanted cell[1 0]{}", + "design|dx_char_1x0: isequaln 0, sig 0: char[0 0]='' | wanted char[1 0]=''", + "design|dx_string_0x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[0 0]{}", + "design|dx_string_1x0: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[1 0]{}", + "design|dx_struct_0x0_fields: isequaln 0, sig 0: struct[0 0]<>{} | wanted struct[0 0]{}", + "design|dx_struct_1x0: isequaln 0, sig 0: struct[1 0]<>{} | wanted struct[1 0]{}", + "design|str_0x3: isequaln 0, sig 0: string[1 1]{\"\"} | wanted string[0 3]{}" + ] + }, + "dynamic": [], + "emptyStructs": [ + { + "entry": "dx_struct_0x0", + "shape": "struct[0 0]{}" + }, + { + "entry": "dx_struct_0x0_fields", + "shape": "struct[0 0]{}" + }, + { + "entry": "dx_struct_1x0", + "shape": "struct[1 0]{}" + } + ], + "pasted": [], + "error": "" + } + }, + { + "kind": "old save", + "build": "pre90caf06", + "fixture": "parity/artifacts/binary/cases.sldd", + "file": "pre90caf06_cases.sldd", + "original": "orig_cases.sldd", + "sha": { + "save": "fd4e8e3c5cae301bb2d0666f9dfd962e783b012da2becf51f2028eded902894a", + "original": "ac37f72df80b998b05f40fe615fb05b84f19712d07fbf46a618b06c5b317b435" + }, + "dynamic": [], + "emptyStructs": { + "entry": "structEmpty", + "dims": [ + 0, + 0 + ], + "fields": [] + }, + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 73, + "same": 73, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [ + { + "entry": "structEmpty", + "shape": "struct[0 0]{}" + } + ], + "pasted": [], + "error": "" + } + }, + { + "kind": "old save", + "build": "pre90caf06", + "fixture": "parity/oldsaves/old_saves_r2027a_binary.sldd", + "file": "pre90caf06_old_saves_r2027a_binary.sldd", + "original": "orig_old_saves_r2027a_binary.sldd", + "sha": { + "save": "227c527959d33d47b8b71454f74985f6a85debfa73f9db48263c1ee43e15fbb9", + "original": "f3f744bd51813831b3c679aa8a93c5c4e3e31be355ed5f484c6fd39edb977692" + }, + "dynamic": [ + { + "entry": "csAutosar", + "to": "csAutosar", + "name": "AutosarMatrixIOAsArray", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosar", + "name": "AutosarCompilerAbstraction", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosar", + "name": "AutosarMaxShortNameLength", + "value": "128.0" + }, + { + "entry": "csAutosar", + "to": "csAutosar", + "name": "DisableAUTOSARRoutinesHostLibrary", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosar", + "name": "AutosarSchemaVersion", + "value": "R23-11" + } + ], + "emptyStructs": [ + { + "entry": "sE00", + "dims": [ + 0, + 0 + ], + "fields": [] + }, + { + "entry": "sE002", + "dims": [ + 0, + 0, + 2 + ], + "fields": [] + }, + { + "entry": "sE02", + "dims": [ + 0, + 2 + ], + "fields": [] + }, + { + "entry": "sE10ab", + "dims": [ + 1, + 0 + ], + "fields": [ + "a", + "b" + ] + }, + { + "entry": "sE20", + "dims": [ + 2, + 0 + ], + "fields": [] + } + ], + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 10, + "same": 8, + "differ": [ + "design|sE10ab: isequaln 0, sig 0: struct[1 0]<>{} | wanted struct[1 0]{}", + "design|sHolds: isequaln 0, sig 0: struct[1 1]{x:double[1 1]=1,e:struct[0 0]<>{},none:struct[0 0]<>{}} | wanted struct[1 1]{x:double[1 1]=1,e:struct[0 0]{},none:struct[0 0]<>{}}" + ] + }, + "dynamic": [ + { + "entry": "csAutosar", + "name": "AutosarMatrixIOAsArray", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarCompilerAbstraction", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarMaxShortNameLength", + "original": "128", + "save": "128", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "DisableAUTOSARRoutinesHostLibrary", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosar", + "name": "AutosarSchemaVersion", + "original": "R23-11", + "save": "R23-11", + "dynamicInSave": true + } + ], + "emptyStructs": [ + { + "entry": "sE00", + "shape": "struct[0 0]{}" + }, + { + "entry": "sE002", + "shape": "struct[0 0 2]{}" + }, + { + "entry": "sE02", + "shape": "struct[0 2]{}" + }, + { + "entry": "sE10ab", + "shape": "struct[1 0]{}" + }, + { + "entry": "sE20", + "shape": "struct[2 0]{}" + } + ], + "pasted": [], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_1.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": { + "section": "other", + "from": "holdsMixed", + "to": "holdsMixedPasted" + }, + "dynamic": [], + "sha": "be48b525f16c2cd582cc00993b3a372ab4214ff19b91850817a22e52c93f4f89", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [], + "pasted": [ + { + "to": "holdsMixedPasted", + "same": true, + "why": "", + "shape": "NdHolder[1 1]", + "kids": "Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}", + "sourceKids": "Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_2.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": { + "section": "other", + "from": "holdsBase", + "to": "holdsBasePasted" + }, + "dynamic": [], + "sha": "5261643b7b4b81dbe1ec6e049a78d9a584b53d78ff98ba60215a2b8bbee9ac04", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [], + "pasted": [ + { + "to": "holdsBasePasted", + "same": true, + "why": "", + "shape": "NdHolder[1 1]", + "kids": "Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}", + "sourceKids": "Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_3.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": { + "section": "config", + "from": "csAutosar", + "to": "csAutosarPasted" + }, + "dynamic": [ + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarMatrixIOAsArray", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarCompilerAbstraction", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarMaxShortNameLength", + "value": "128.0" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "DisableAUTOSARRoutinesHostLibrary", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarSchemaVersion", + "value": "R23-11" + } + ], + "sha": "b4a26ff8826b2a1f6450ae943ef4df7831e3694acf9c79b44ea9ea758e53741d", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [ + { + "entry": "csAutosarPasted", + "name": "AutosarMatrixIOAsArray", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarCompilerAbstraction", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarMaxShortNameLength", + "original": "128", + "save": "128", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "DisableAUTOSARRoutinesHostLibrary", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarSchemaVersion", + "original": "R23-11", + "save": "R23-11", + "dynamicInSave": true + } + ], + "emptyStructs": [], + "pasted": [ + { + "to": "csAutosarPasted", + "same": true, + "why": "", + "shape": "Simulink.ConfigSet[1 1]", + "kids": "Simulink.ConfigComponent[9 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp}", + "sourceKids": "Simulink.ConfigComponent[9 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp}" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_4.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": { + "section": "config", + "from": "csSimscape", + "to": "csSimscapePasted" + }, + "dynamic": [], + "sha": "faac406a16d238fb6f083290eda776a30a5dc1c90a63f0e42619fa0078fbdb67", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [], + "pasted": [ + { + "to": "csSimscapePasted", + "same": true, + "why": "", + "shape": "Simulink.ConfigSet[1 1]", + "kids": "Simulink.ConfigComponent[10 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp,SSC.SimscapeCC}", + "sourceKids": "Simulink.ConfigComponent[10 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp,SSC.SimscapeCC}" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_5.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": [ + { + "section": "other", + "from": "holdsMixed", + "to": "holdsMixedPasted" + }, + { + "section": "other", + "from": "holdsBase", + "to": "holdsBasePasted" + }, + { + "section": "config", + "from": "csAutosar", + "to": "csAutosarPasted" + }, + { + "section": "config", + "from": "csSimscape", + "to": "csSimscapePasted" + } + ], + "dynamic": [ + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarMatrixIOAsArray", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarCompilerAbstraction", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarMaxShortNameLength", + "value": "128.0" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "DisableAUTOSARRoutinesHostLibrary", + "value": "off" + }, + { + "entry": "csAutosar", + "to": "csAutosarPasted", + "name": "AutosarSchemaVersion", + "value": "R23-11" + } + ], + "sha": "a900b4ac24796f3a8c4f725ccb4b41945a7d8d712fe3ba7c5cb67becdbf7fdb2", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": true, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [ + { + "entry": "csAutosarPasted", + "name": "AutosarMatrixIOAsArray", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarCompilerAbstraction", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarMaxShortNameLength", + "original": "128", + "save": "128", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "DisableAUTOSARRoutinesHostLibrary", + "original": "off", + "save": "off", + "dynamicInSave": true + }, + { + "entry": "csAutosarPasted", + "name": "AutosarSchemaVersion", + "original": "R23-11", + "save": "R23-11", + "dynamicInSave": true + } + ], + "emptyStructs": [], + "pasted": [ + { + "to": "holdsMixedPasted", + "same": true, + "why": "", + "shape": "NdHolder[1 1]", + "kids": "Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}", + "sourceKids": "Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}" + }, + { + "to": "holdsBasePasted", + "same": true, + "why": "", + "shape": "NdHolder[1 1]", + "kids": "Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}", + "sourceKids": "Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}" + }, + { + "to": "csAutosarPasted", + "same": true, + "why": "", + "shape": "Simulink.ConfigSet[1 1]", + "kids": "Simulink.ConfigComponent[9 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp}", + "sourceKids": "Simulink.ConfigComponent[9 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp}" + }, + { + "to": "csSimscapePasted", + "same": true, + "why": "", + "shape": "Simulink.ConfigSet[1 1]", + "kids": "Simulink.ConfigComponent[10 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp,SSC.SimscapeCC}", + "sourceKids": "Simulink.ConfigComponent[10 1]{Simulink.SolverCC,Simulink.DataIOCC,Simulink.OptimizationCC,Simulink.DebuggingCC,Simulink.HardwareCC,Simulink.ModelReferenceCC,Simulink.SFSimCC,Simulink.RTWCC,SlCovCC.ConfigComp,SSC.SimscapeCC}" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_6.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "orig_old_saves_r2027a_binary.sldd", + "pasted": [ + { + "section": "design", + "from": "sE00", + "to": "sE00Pasted" + }, + { + "section": "design", + "from": "sE02", + "to": "sE02Pasted" + }, + { + "section": "design", + "from": "sE20", + "to": "sE20Pasted" + }, + { + "section": "design", + "from": "sE002", + "to": "sE002Pasted" + }, + { + "section": "design", + "from": "sE10ab", + "to": "sE10abPasted" + }, + { + "section": "design", + "from": "sHolds", + "to": "sHoldsPasted" + } + ], + "dynamic": [], + "sha": "c5538b5710fb679deaa27182d20d7ab969cbc887b052a19c825b058e26566fc2", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [], + "pasted": [ + { + "to": "sE00Pasted", + "same": true, + "why": "", + "shape": "struct[0 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE02Pasted", + "same": true, + "why": "", + "shape": "struct[0 2]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE20Pasted", + "same": true, + "why": "", + "shape": "struct[2 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE002Pasted", + "same": false, + "why": "isequaln 1, sig 0: struct[0 0]<>{} | wanted struct[0 0 2]<>{}", + "shape": "struct[0 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE10abPasted", + "same": true, + "why": "", + "shape": "struct[1 0]{a,b}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sHoldsPasted", + "same": true, + "why": "", + "shape": "struct[1 1]{x,e,none}", + "kids": "", + "sourceKids": "" + } + ], + "error": "" + } + }, + { + "kind": "paste", + "file": "paste_7.sldd", + "original": "orig_old_saves_r2027a_text.sldd", + "source": "pre90caf06_old_saves_r2027a_binary.sldd", + "pasted": [ + { + "section": "design", + "from": "sE00", + "to": "sE00Pasted" + }, + { + "section": "design", + "from": "sE02", + "to": "sE02Pasted" + }, + { + "section": "design", + "from": "sE20", + "to": "sE20Pasted" + }, + { + "section": "design", + "from": "sE002", + "to": "sE002Pasted" + }, + { + "section": "design", + "from": "sE10ab", + "to": "sE10abPasted" + }, + { + "section": "design", + "from": "sHolds", + "to": "sHoldsPasted" + } + ], + "dynamic": [], + "sha": "2ce0350c00d68c2e0b61906ee6755f941cf2cb58d2f539298010d0066cd2592f", + "matlab": { + "opened": true, + "warnings": [], + "asOriginal": false, + "entries": { + "checked": 10, + "same": 10, + "differ": [] + }, + "dynamic": [], + "emptyStructs": [], + "pasted": [ + { + "to": "sE00Pasted", + "same": true, + "why": "", + "shape": "struct[0 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE02Pasted", + "same": true, + "why": "", + "shape": "struct[0 2]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE20Pasted", + "same": true, + "why": "", + "shape": "struct[2 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE002Pasted", + "same": false, + "why": "isequaln 1, sig 0: struct[0 0]<>{} | wanted struct[0 0 2]<>{}", + "shape": "struct[0 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sE10abPasted", + "same": true, + "why": "", + "shape": "struct[1 0]{}", + "kids": "", + "sourceKids": "" + }, + { + "to": "sHoldsPasted", + "same": true, + "why": "", + "shape": "struct[1 1]{x,e,none}", + "kids": "", + "sourceKids": "" + } + ], + "error": "" + } + } + ] +} diff --git a/test/parity/matlab/probe_old_saves_readback.m b/test/parity/matlab/probe_old_saves_readback.m new file mode 100644 index 00000000..f9e90fb3 --- /dev/null +++ b/test/parity/matlab/probe_old_saves_readback.m @@ -0,0 +1,454 @@ +% What does MATLAB read of a binary dictionary core saved before two of its fixes, and of what +% core copies now from a binary dictionary into a text one? (M4 and M7 of the 2026-10-13 +% save-fidelity plan, the last of its MATLAB read-backs, and D15's binary-to-text reopen.) +% +% For each row of cases.json, which old_saves.mjs writes: +% +% an old save, an untouched save of a binary fixture by core built before #93 ('pre93'), +% before #93's last commit ('pre69664b0') or before 90caf06 ('pre90caf06'), each of which +% wrote every entry from its model: +% (1) every entry reads as it does in the original: the same class, isequaln, and the same +% `sig` (wbcompare.m's fullsig, which here also names the fields of a struct, so that a +% struct of no element keeps them in its signature); an entry the original is not +% isequaln to itself in, read twice, is held to its class and sig alone (`notSelfEqual`: +% a config set, a handle, is equal to no other); +% (2) each dynamic property the original holds (M4), as get_param reads it of the config +% set in the original and in the save, and whether the save's target component holds it +% as a dynamic property (findprop gives a matlab.metadata.DynamicProperty); +% (3) each struct of no element the original holds at an entry (M7), as MATLAB reads it in +% the save: its class, size and field names; +% (4) and whether the save opens and reads with no error or warning; +% a paste, core's copy of entries of a binary dictionary (old_saves_r2027a_binary.sldd, or its +% pre90caf06 save) into old_saves_r2027a_text.sldd, MATLAB's text twin of it, under new names: +% (1) and (4) for every entry of the text twin; (2) for each pasted config set; and +% (5) each pasted entry as the entry it was copied from reads (class, isequaln, sig; a +% config set's Name aside, which is its entry's name), its shape, and its object array's +% class and the class of each element in order, beside the source's. +% +% Opens only files in OLD_SAVES_OUT: the saves and the pastes, and the copies of the originals +% old_saves.mjs writes beside them. Nothing is saved: every dictionary is closed with +% discardChanges and Simulink.data.dictionary.closeAll('-discard'). +% +% node test/parity/matlab/old_saves.mjs cases /tmp/oldsaves +% env OLD_SAVES_OUT=/tmp/oldsaves mw -using Bmain matlab -nodesktop \ +% -batch "run('$PWD/test/parity/matlab/probe_old_saves_readback.m')" +% cp /tmp/oldsaves/old_saves_readback.truth.json test/parity/matlab/ +% +% Prints OLD_SAVES_READBACK OK on its last line. Output: old_saves_readback.truth.json, {version, +% release, date, originals, cases}: `originals` one row per original or source ({file, opened, +% warnings, entries, notSelfEqual, shapes}), `shapes` each entry's class, size and, for a +% struct, field names; `cases` each row of cases.json, with `matlab`: {opened, warnings, +% asOriginal, entries: {checked, same, differ}, dynamic: [{entry, name, original, save, +% dynamicInSave}], emptyStructs: [{entry, shape}], pasted: [{to, same, why, shape, kids, +% sourceKids}], error}. Messages name files without the directory. +% test/parity/oldSaves.truth.test.ts reads it. + +outdir = getenv('OLD_SAVES_OUT'); +if isempty(outdir), error('set OLD_SAVES_OUT to the directory old_saves.mjs wrote'); end +here = fileparts(mfilename('fullpath')); +fixtures = fullfile(here, '..', '..', 'fixtures'); +addpath(fullfile(fixtures, 'mcos')); % GearMode, Widget, WidgetPart, NdHolder, +dexdata +addpath(fullfile(fixtures, 'display')); % DispLevel, DispWidget +warning('off', 'backtrace'); +try + Simulink.defineIntEnumType('DispColor', {'Red', 'Green', 'Blue'}, [1 2 3]); % as probe_display.m +catch +end +Simulink.data.dictionary.closeAll('-discard'); +fprintf('VERSION %s\n', version); + +cases = jsondecode(fileread(fullfile(outdir, 'cases.json'))); +if isstruct(cases), cases = num2cell(cases); end +originals = {}; +read = containers.Map(); +for i = 1:numel(cases) + c = cases{i}; + t0 = tic; + if ~read.isKey(c.original) + r = readAll(fullfile(outdir, c.original)); + again = readAll(fullfile(outdir, c.original)); + r.notSelfEqual = {}; + for k = 1:numel(r.keys) + a = r.vals(r.keys{k}); + if again.vals.isKey(r.keys{k}) && isempty(a.error) + b = again.vals(r.keys{k}); + if ~isequaln(a.value, b.value) || ~strcmp(a.sig, b.sig) + r.notSelfEqual{end+1} = r.keys{k}; + end + end + end + read(c.original) = r; + originals{end+1} = struct('file', c.original, 'opened', r.opened, 'warnings', {r.warnings}, 'entries', numel(r.keys), ... + 'notSelfEqual', {r.notSelfEqual}, 'shapes', {shapesOf(r)}); %#ok + fprintf('ORIGINAL %-40s opened=%d entries=%d warnings=%d notSelfEqual=%s %s\n', c.original, r.opened, numel(r.keys), numel(r.warnings), strjoin(r.notSelfEqual, ','), r.error); + end + base = read(c.original); + m = struct('opened', false, 'warnings', {{}}, 'asOriginal', false, 'entries', struct(), 'dynamic', {{}}, 'emptyStructs', {{}}, 'pasted', {{}}, 'error', ''); + try + got = readAll(fullfile(outdir, c.file)); + m.opened = got.opened; + m.warnings = got.warnings; + if ~got.opened, error('oldsaves:open', '%s', got.error); end + % (1) every entry of the original + differ = {}; + nsame = 0; + for k = 1:numel(base.keys) + key = base.keys{k}; + if ~got.vals.isKey(key) + differ{end+1} = [key ': not in the save']; %#ok + continue + end + [same, why] = sameValue(got.vals(key), base.vals(key), ismember(key, base.notSelfEqual)); + nsame = nsame + same; + if ~same, differ{end+1} = [key ': ' why]; end %#ok + end + pastedKeys = {}; + from = base; + if strcmp(c.kind, 'paste') + p = c.pasted; + if isstruct(p), p = num2cell(p); end + pastedKeys = reshape(cellfun(@(x) [x.section '|' x.to], p, 'UniformOutput', false), 1, []); + % (5) the pasted entry, as the entry it was copied from reads + if ~read.isKey(c.source), read(c.source) = readAll(fullfile(outdir, c.source)); end + from = read(c.source); + for k = 1:numel(p) + to = [p{k}.section '|' p{k}.to]; + key = [p{k}.section '|' p{k}.from]; + row = struct('to', p{k}.to, 'same', false, 'why', '', 'shape', '', 'kids', '', 'sourceKids', ''); + if ~got.vals.isKey(to) + row.why = 'not in the save'; + elseif ~from.vals.isKey(key) + row.why = 'not in the source'; + else + g = got.vals(to); + % A config set's Name is its entry's: the paste's is the new one. + if isa(g.value, 'Simulink.ConfigSet') + g.value.Name = from.vals(key).value.Name; + g.sig = safesig(g.value); + end + [row.same, row.why] = sameValue(g, from.vals(key), true); + row.shape = g.shape; + row.kids = arrayOf(g.value); + row.sourceKids = arrayOf(from.vals(key).value); + end + m.pasted{end+1} = row; + end + end + extra = setdiff(got.keys, [base.keys, pastedKeys]); + for k = 1:numel(extra), differ{end+1} = [extra{k} ': only in the save']; end %#ok + m.entries = struct('checked', numel(base.keys), 'same', nsame, 'differ', {differ}); + % (2) each dynamic property, as get_param reads it in the original (or the pasted entry's + % source) and in the save + d = c.dynamic; + if isstruct(d), d = num2cell(d); end + for k = 1:numel(d) + key = ['config|' d{k}.entry]; + to = ['config|' d{k}.to]; + m.dynamic{end+1} = struct('entry', d{k}.to, 'name', d{k}.name, 'original', paramOf(from, key, d{k}.name), 'save', paramOf(got, to, d{k}.name), ... + 'dynamicInSave', isDynamic(got, to, d{k}.name)); + end + if strcmp(c.kind, 'old save') + % (3) each struct of no element, as MATLAB reads it in the save + e = c.emptyStructs; + if isstruct(e), e = num2cell(e); end + for k = 1:numel(e) + key = ['design|' e{k}.entry]; + shape = ''; + if got.vals.isKey(key), shape = got.vals(key).shape; end + m.emptyStructs{end+1} = struct('entry', e{k}.entry, 'shape', shape); + end + end + m.asOriginal = got.opened && numel(got.warnings) == numel(base.warnings) && isempty(differ) && ... + all(cellfun(@(x) x.same, m.pasted)); + catch err + m.error = strrep(err.message, newline, ' '); + end + Simulink.data.dictionary.closeAll('-discard'); + c.matlab = m; + cases{i} = c; + fprintf('%-4s %-40s %5.1fs %s\n', tern(m.asOriginal, 'OK', 'NOT'), c.file, toc(t0), summary(m)); +end +for i = 1:numel(cases) + cases{i}.matlab.error = unrooted(cases{i}.matlab.error, outdir); + cases{i}.matlab.warnings = unrooted(cases{i}.matlab.warnings, outdir); +end +for i = 1:numel(originals) + originals{i}.warnings = unrooted(originals{i}.warnings, outdir); +end +truth = struct('version', version, 'release', version('-release'), 'date', char(datetime('now', 'Format', 'yyyy-MM-dd')), ... + 'originals', {originals}, 'cases', {cases}); +fid = fopen(fullfile(outdir, 'old_saves_readback.truth.json'), 'w', 'n', 'UTF-8'); +fprintf(fid, '%s\n', jsonencode(truth, 'PrettyPrint', true)); +fclose(fid); +fprintf('OLD_SAVES_READBACK OK\n'); + +function x = unrooted(x, outdir) + for root = {['/private' outdir], outdir} + x = strrep(x, [root{1} filesep], ''); + end +end + +% ---- every entry of a dictionary, read once, with what the open and the reads warned ----------- +function r = readAll(path) + Simulink.data.dictionary.closeAll('-discard'); + r = struct('opened', false, 'warnings', {{}}, 'error', '', 'keys', {{}}, 'vals', containers.Map()); + lastwarn(''); + dd = []; + try + printed = evalc('dd = Simulink.data.dictionary.open(path);'); + r.warnings = noted(r.warnings, printed); + r.opened = true; + for s = {'design', 'other', 'config'} + sec = getSection(dd, sectionName(s{1})); + entries = find(sec); %#ok + for k = 1:numel(entries) + key = [s{1} '|' entries(k).Name]; + v = struct('class', '', 'value', {[]}, 'sig', '', 'shape', '', 'error', ''); + try + x = []; + printed = evalc('x = getValue(entries(k));'); + r.warnings = noted(r.warnings, printed); + v.class = class(x); + v.value = x; + v.sig = safesig(x); + v.shape = shapeOf(x); + catch err + v.error = err.identifier; + r.warnings{end+1} = sprintf('%s: %s', key, strrep(err.message, newline, ' ')); + end + r.keys{end+1} = key; + r.vals(key) = v; + end + end + catch err + r.error = strrep(err.message, newline, ' '); + end + [msg, id] = lastwarn; + if ~isempty(msg), r.warnings{end+1} = sprintf('lastwarn %s: %s', id, strrep(msg, newline, ' ')); end + if ~isempty(dd) + try, discardChanges(dd); close(dd); catch, end + end + Simulink.data.dictionary.closeAll('-discard'); +end + +% Each entry's shape, as {key, shape}, of a dictionary readAll read. +function out = shapesOf(r) + out = {}; + for k = 1:numel(r.keys) + out{end+1} = struct('key', r.keys{k}, 'shape', r.vals(r.keys{k}).shape); %#ok + end +end + +% A value's class and size, and for a struct its field names: `struct[0 2]{a,b}`. +function s = shapeOf(x) + s = [class(x) mat2str(size(x))]; + if isstruct(x), s = [s '{' joinparts(fieldnames(x)') '}']; end +end + +% The object array a pasted value holds, as its class, size and each element's class: an +% NdHolder's Kids, a config set's Components. +function s = arrayOf(x) + s = ''; + try + if isa(x, 'Simulink.ConfigSet') + a = x.Components; + elseif isprop(x, 'Kids') + a = x.Kids; + else + return + end + s = [class(a) mat2str(size(a)) '{' joinparts(arrayfun(@(k) {class(a(k))}, 1:numel(a))) '}']; + catch err + s = ['<' err.identifier '>']; + end +end + +% A config set's parameter, as get_param reads it, as text; or what get_param threw. +function s = paramOf(r, key, name) + s = ''; + if ~r.vals.isKey(key), return, end + v = r.vals(key); + try + x = get_param(v.value, name); + if ischar(x), s = x; else, s = mat2str(x); end + catch err + s = ['<' err.identifier '>']; + end +end + +% Whether a config set's target component holds the property as a dynamic one, as MATLAB adds +% the options of a target file. +function b = isDynamic(r, key, name) + b = false; + if ~r.vals.isKey(key), return, end + try + t = r.vals(key).value.getComponent('Code Generation').getComponent('Target'); + b = isa(findprop(t, name), 'matlab.metadata.DynamicProperty'); + catch + end +end + +% The same class, isequal (NaN equal to itself), and the same leaf-by-leaf signature; or the +% same error. `bySignature`, for a value the original is not isequaln to itself in: the class and +% the signature alone. +function [same, why] = sameValue(got, want, bySignature) + if nargin < 3, bySignature = false; end + why = ''; + if ~isempty(got.error) || ~isempty(want.error) + same = strcmp(got.error, want.error); + if ~same, why = sprintf('error %s, not %s', got.error, want.error); end + return + end + if ~strcmp(got.class, want.class) + same = false; + why = sprintf('class %s, not %s', got.class, want.class); + return + end + eq = bySignature || isequaln(got.value, want.value); + sg = strcmp(got.sig, want.sig); + same = eq && sg; + if ~same + why = sprintf('isequaln %d, sig %d: %s | wanted %s', eq, sg, trunc(firstDiff(got.sig, want.sig)), trunc(firstDiff(want.sig, got.sig))); + end +end + +% A signature from the first character where it differs from another, with a little before it. +function s = firstDiff(a, b) + n = min(numel(a), numel(b)); + k = find(a(1:n) ~= b(1:n), 1); + if isempty(k), k = n + 1; end + s = a(max(1, k - 80):end); +end + +function w = noted(w, printed) + printed = strtrim(printed); + if ~isempty(printed), w{end+1} = strrep(printed, newline, ' '); end +end + +function s = sectionName(key) + switch key + case 'design', s = 'Design Data'; + case 'other', s = 'Other Data'; + case 'config', s = 'Configurations'; + otherwise, error('oldsaves:section', 'no section %s', key); + end +end + +function s = summary(m) + if ~isempty(m.error), s = ['ERROR ' m.error]; return, end + s = sprintf('entries %d/%d warnings=%d', m.entries.same, m.entries.checked, numel(m.warnings)); + if ~isempty(m.entries.differ), s = [s ' | ' strjoin(m.entries.differ, '; ')]; end + for k = 1:numel(m.dynamic) + d = m.dynamic{k}; + if ~strcmp(d.original, d.save), s = sprintf('%s | %s.%s %s -> %s', s, d.entry, d.name, d.original, d.save); end + end + for k = 1:numel(m.emptyStructs), s = sprintf('%s | %s %s', s, m.emptyStructs{k}.entry, m.emptyStructs{k}.shape); end + for k = 1:numel(m.pasted), s = sprintf('%s | pasted %s same=%d %s %s', s, m.pasted{k}.to, m.pasted{k}.same, m.pasted{k}.kids, m.pasted{k}.why); end + if ~isempty(m.warnings), s = [s ' | ' strjoin(m.warnings, '; ')]; end +end + +function s = tern(b, y, n) + if b, s = y; else, s = n; end +end + +function s = trunc(s) + if numel(s) > 400, s = [s(1:400) ' ...(' num2str(numel(s)) ' chars)']; end +end + +function s = safesig(x) + try + s = fullsig(x); + catch err + s = ['']; + end +end + +function s = joinparts(c) + if isempty(c) || ~iscell(c), s = ''; else, s = strjoin(c, ','); end +end + +% wbcompare.m's fullsig and valstr, copied, with one change: a struct's signature names its +% fields first, so that a struct of no element that lost them differs from one that has them. +function s = fullsig(v, depth) + if nargin < 2, depth = 0; end + s = [class(v) mat2str(size(v))]; + if depth > 12 + s = [s '/DEPTHCAP']; + return + end + parts = {}; + if isstruct(v) + fn = fieldnames(v); + s = [s '<' joinparts(fn') '>']; + for i = 1:numel(v) + for f = 1:numel(fn) + parts{end+1} = [fn{f} ':' fullsig(v(i).(fn{f}), depth + 1)]; %#ok + end + end + elseif iscell(v) + flat = reshape(v, 1, []); + for i = 1:numel(flat) + parts{end+1} = fullsig(flat{i}, depth + 1); %#ok + end + elseif ischar(v) + s = [s '=''' reshape(v, 1, []) '''']; + return + elseif isstring(v) + parts = arrayfun(@(x) {['"' char(x) '"']}, reshape(v, 1, [])); + s = [s '{' joinparts(parts) '}']; + return + elseif isnumeric(v) || islogical(v) + if ~isreal(v), s = [s '/cplx']; end + if issparse(v), s = [s '/sparse']; end + s = [s '=' valstr(v)]; + return + elseif isenum(v) + parts = arrayfun(@(x) {char(x)}, reshape(v, 1, [])); + s = [s '{' joinparts(parts) '}']; + return + elseif isobject(v) || isa(v, 'handle') + p = properties(v); + gotone = false; + for i = 1:numel(v) + for j = 1:numel(p) + try + parts{end+1} = [p{j} ':' fullsig(v(i).(p{j}), depth + 1)]; %#ok + gotone = true; + catch ME + parts{end+1} = [p{j} ':<' ME.identifier '>']; %#ok + end + end + end + if ~gotone && numel(v) == 1 + try + txt = formattedDisplayText(v, 'SuppressMarkup', true); + parts{end+1} = ['disp:' strtrim(regexprep(char(txt), '\s+', ' '))]; + catch + parts{end+1} = 'disp:'; + end + end + else + return + end + s = [s '{' joinparts(parts) '}']; +end + +function s = valstr(v) + if isempty(v) + s = '[]'; + return + end + if issparse(v) + [ii, jj, xx] = find(v); + s = ['nz:' joinparts(arrayfun(@(a, b) {sprintf('%d,%d', a, b)}, ii', jj')) '=' valstr(full(reshape(xx, 1, [])))]; + return + end + flat = reshape(v, 1, []); + if isinteger(v) || islogical(v) + s = joinparts(arrayfun(@(x) {char(string(x))}, flat)); + elseif isreal(flat) + s = joinparts(arrayfun(@(x) {sprintf('%.17g', x)}, flat)); + else + s = joinparts(arrayfun(@(x) {sprintf('%.17g%+.17gi', real(x), imag(x))}, flat)); + end +end diff --git a/test/parity/oldSaves.truth.test.ts b/test/parity/oldSaves.truth.test.ts new file mode 100644 index 00000000..9fe0d356 --- /dev/null +++ b/test/parity/oldSaves.truth.test.ts @@ -0,0 +1,337 @@ +// Copyright 2026 The MathWorks, Inc. +// +// What MATLAB R2027a read back of binary dictionaries core saved before two of its fixes (M4 and +// M7 of the 2026-10-13 save-fidelity plan, the last of its MATLAB read-backs), and of what core +// copies now from a binary dictionary into a text one (D15's binary-to-text reopen); held to what +// core reads and writes now. +// +// The old saves, in test/parity/oldsaves/, are untouched saves of every binary fixture holding a +// dynamic property or a struct entry of no element, and of old_saves_r2027a_binary.sldd +// (make_old_saves_r2027a.m, for the shapes no other fixture holds), made by core built at +// 50549bd^ ('pre93', before #93), at 69664b0^ ('pre69664b0', #93 less the commit that kept a +// dynamic property's Source and PropertyType) and at 90caf06^ ('pre90caf06', before the struct +// of no element was written as MATLAB writes it). Each of those cores wrote every entry of a save +// from its model. matlab/old_saves.mjs made them, and makes the pastes; +// matlab/probe_old_saves_readback.m had MATLAB open each and wrote what it read to +// matlab/old_saves_readback.truth.json. +// +// What MATLAB read: +// - M4: a dynamic property written as an ordinary one (no Source="dynamic", no PropertyType) is +// read 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-class object array as the first element's class, so +// a config set's components were all written as a Simulink.SolverCC (the target component as a +// Simulink.CodeAppCC), and MATLAB reads a config set without the AUTOSAR target's options, and +// a user's subclass of Simulink.Signal beside a Simulink.Signal as two of the first's class. +// - M7: a struct of no element written around an `` is read in the shape its Dimension +// states, with no element and no warning; but that writer wrote none of the fields it declares, +// so a 1x0 struct with fields a and b reads as a 1x0 struct with none. Lost in the file. +// - Other losses of those cores, fixed since, show in the same saves (the shaped empties and +// enumeration values of display/dispcat_bin.sldd); they are listed here, not asked about. +// - D15: core's copy into a text dictionary of a mixed Signal array and of a config set's +// Components (one holding an SSC.SimscapeCC, one the AUTOSAR target's dynamic properties, which +// the text form holds among the ordinary ones) reads in MATLAB as the binary original does. +// +// The test makes the pastes again from the package's source and asks that each is still the one +// MATLAB was asked about, checks that the old saves and their originals are the files MATLAB +// read, and that core reads each old save as MATLAB does, where MATLAB and the file agree. +import { describe, it, expect } from 'vitest'; +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { createSession, ingest } from '../../src/index.js'; +import { regenerateEntryXml } from '../../src/datamodel/parser/BinarySlddSerializer.js'; +import { + BUILDS, + EMPTIES_OLD_SAVE, + OLD_SAVES_DIR, + TEXT_TWIN, + linesOf, + objectsOf, + pasteSources, + pastesOf, + shapesOf, +} from './matlab/old_saves.mjs'; +import { strFromU8, unzipSync } from 'fflate'; +import '../../src/datamodel/node/data/NodeClassMap.js'; + +type Dynamic = { entry: string; name: string; original: string; save: string; dynamicInSave: boolean }; +type Pasted = { to: string; same: boolean; why: string; shape: string; kids: string; sourceKids: string }; +type Matlab = { + opened: boolean; + warnings: string[] | string; + asOriginal: boolean; + entries: { checked: number; same: number; differ: string[] | string }; + dynamic: Dynamic[] | Dynamic; + emptyStructs: { entry: string; shape: string }[] | { entry: string; shape: string }; + pasted: Pasted[] | Pasted; + error: string; +}; +type Case = { + kind: 'old save' | 'paste'; + build?: string; + fixture?: string; + file: string; + original: string; + source?: string; + sha: { save: string; original: string } | string; + pasted?: { section: string; from: string; to: string }[] | { section: string; from: string; to: string }; + dynamic: { entry: string; to: string; name: string; value: string }[] | { entry: string; to: string; name: string; value: string }; + emptyStructs?: { entry: string; dims: number[]; fields: string[] | string }[] | { entry: string; dims: number[]; fields: string[] | string }; + matlab: Matlab; +}; +type Original = { file: string; opened: boolean; warnings: string[] | string; entries: number; notSelfEqual: string[] | string; shapes: { key: string; shape: string }[] }; + +const at = (rel: string) => fileURLToPath(new URL(rel, import.meta.url)); +const TEST_DIR = at('..'); +const TRUTH: { version: string; release: string; originals: Original[]; cases: Case[] } = JSON.parse( + readFileSync(at('./matlab/old_saves_readback.truth.json'), 'utf8'), +); +/** MATLAB writes a one-element array as the element, and an empty one as []: every list as a list. */ +const list = (x: T[] | T | undefined): T[] => (x === undefined ? [] : ([] as T[]).concat(x)); +const sha = (b: Uint8Array | string) => createHash('sha256').update(b).digest('hex'); +const ab = (u8: Uint8Array) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer; +const OLD = TRUTH.cases.filter((c) => c.kind === 'old save'); +const PASTES = TRUTH.cases.filter((c) => c.kind === 'paste'); +const oldSave = (build: string, fixture: string) => OLD.find((c) => c.build === build && c.fixture === fixture)!; +const GENERATED = `${OLD_SAVES_DIR}/old_saves_r2027a_binary.sldd`; +const savedBytes = (file: string) => new Uint8Array(readFileSync(at(`../${OLD_SAVES_DIR}/${file}`))); +const differKeys = (c: Case) => list(c.matlab.entries.differ).map((d) => d.slice(0, d.indexOf(': '))); + +/** An old save, opened by core as it is now. */ +function openSave(file: string) { + const session = createSession(); + return ingest(session, ab(savedBytes(file)), { filename: file }) as any; +} +const entryOf = (root: any, section: string, name: string) => root.getSection(section).children.find((e: any) => e.name === name); + +/** The value core holds for a named property anywhere in a value, and whether its element names a Source for it. */ +function propertyIn(value: unknown, name: string): { value: unknown; source: unknown } | null { + if (!value || typeof value !== 'object') return null; + const o = value as Record; + if (o._properties && typeof o._properties === 'object' && name in o._properties) { + return { value: o._properties[name], source: o._property_source?.[name] ?? null }; + } + for (const v of Object.values(o)) { + const found = propertyIn(v, name); + if (found) return found; + } + return null; +} + +describe("MATLAB's read-back of core's older binary saves (M4, M7) and of its binary-to-text copies (D15)", () => { + it("the truth is MATLAB R2027a's, and every save opened and read with no error or warning", () => { + expect(TRUTH.release).toBe('2027a'); + expect(OLD.length).toBe(Object.keys(BUILDS).length * 3); + expect(PASTES.length).toBe(7); + expect(TRUTH.cases.filter((c) => c.matlab.error !== '' || !c.matlab.opened || list(c.matlab.warnings).length > 0).map((c) => c.file)).toEqual([]); + expect(list(TRUTH.originals).filter((o) => !o.opened || list(o.warnings).length > 0).map((o) => o.file)).toEqual([]); + }); + + it('the old saves in the tree are the files MATLAB read, of the originals it read them against', () => { + for (const c of OLD) { + const s = c.sha as { save: string; original: string }; + expect([c.file, sha(savedBytes(c.file))]).toEqual([c.file, s.save]); + const original = at(`../${c.fixture}`); + if (existsSync(original)) { + expect([c.fixture, sha(new Uint8Array(readFileSync(original)))]).toEqual([c.fixture, s.original]); + } + } + }); + + it('core still makes each paste MATLAB was asked about', () => { + const { cases } = pastesOf({ createSession, ingest }, pasteSources(TEST_DIR), new Uint8Array(readFileSync(at(`../${TEXT_TWIN}`)))); + const asked = (c: Case) => [c.file, c.source, list(c.pasted), list(c.dynamic), c.sha]; + expect(cases.map(asked)).toEqual(PASTES.map(asked)); + }); + + describe('M4: a dynamic property a save wrote as an ordinary one', () => { + const dynamic = shapesOf(new Uint8Array(readFileSync(at(`../${GENERATED}`)))).dynamic; + + it("is how the save before #93's last commit spells the AUTOSAR target's options, where the original and a later save keep Source and PropertyType", () => { + expect(dynamic.map((d: { name: string }) => d.name)).toEqual([ + 'AutosarMatrixIOAsArray', + 'AutosarCompilerAbstraction', + 'AutosarMaxShortNameLength', + 'DisableAUTOSARRoutinesHostLibrary', + 'AutosarSchemaVersion', + ]); + for (const build of ['pre93', 'pre69664b0']) { + for (const line of linesOf(savedBytes(oldSave(build, GENERATED).file), dynamic)) { + expect(line).toMatch(/^

[^<]*<\/P>$/); + } + } + for (const line of linesOf(savedBytes(oldSave('pre90caf06', GENERATED).file), dynamic)) { + expect(line).toMatch(/^

[^<]*<\/P>$/); + } + }); + + it('MATLAB reads as the dynamic property it was, with its value, and the config set as the original', () => { + for (const build of ['pre69664b0', 'pre90caf06']) { + const c = oldSave(build, GENERATED); + expect(list(c.matlab.dynamic).map((d) => [d.name, d.save, d.dynamicInSave])).toEqual(list(c.matlab.dynamic).map((d) => [d.name, d.original, true])); + expect(differKeys(c).filter((k) => k.startsWith('config|'))).toEqual([]); + } + }); + + it('core reads each with the value MATLAB reads; as an ordinary property, there being nothing in the file to say it is not', () => { + const values = new Map(list(oldSave('pre69664b0', GENERATED).matlab.dynamic).map((d) => [d.name, d.save])); + for (const [build, source] of [ + ['pre69664b0', null], + ['pre90caf06', 'dynamic'], + ] as const) { + const value = entryOf(openSave(oldSave(build, GENERATED).file), 'config', 'csAutosar').serialize().value; + for (const { name } of dynamic) { + const read = propertyIn(JSON.parse(JSON.stringify(value)), name)!; + expect([name, String(read.value), (read.source as { source?: string } | null)?.source ?? null]).toEqual([name, values.get(name), source]); + } + } + }); + + it("but a save before #93 wrote a mixed array's elements as the first's class: MATLAB reads the config sets and the Signal arrays otherwise, and no AUTOSAR option", () => { + const c = oldSave('pre93', GENERATED); + expect(list(c.matlab.dynamic).map((d) => [d.name, d.save, d.dynamicInSave])).toEqual(dynamic.map((d: { name: string }) => [d.name, '', false])); + expect(differKeys(c).filter((k) => !k.startsWith('design|'))).toEqual(['other|holdsBase', 'other|holdsMixed', 'config|csAutosar', 'config|csSimscape']); + // What that save wrote: the objects' classes in each entry, in order, the char cells aside. + const classes = (bytes: Uint8Array, entry: string) => + [...new Map(objectsOf(strFromU8(unzipSync(bytes)['data/chunk0.xml']))).get(entry)!.matchAll(//g)] + .map((m) => m[1]) + .filter((k) => k !== 'char' && !/^Simulink(\.LoggingInfo|\.CoderInfo|CSC\.)/.test(k)); + const original = new Uint8Array(readFileSync(at(`../${GENERATED}`))); + expect(classes(original, 'holdsMixed')).toEqual(['NdHolder', 'dexdata.DerivedSignal', 'Simulink.Signal']); + expect(classes(savedBytes(c.file), 'holdsMixed')).toEqual(['NdHolder', 'dexdata.DerivedSignal', 'dexdata.DerivedSignal']); + expect(classes(original, 'csAutosar').slice(9, 11)).toEqual(['Simulink.CodeAppCC', 'Simulink.STFCustomTargetCC']); + expect(classes(savedBytes(c.file), 'csAutosar').slice(1, 11)).toEqual([...Array(8).fill('Simulink.SolverCC'), 'Simulink.CodeAppCC', 'Simulink.CodeAppCC']); + expect(classes(savedBytes(oldSave('pre69664b0', GENERATED).file), 'csAutosar')).toEqual(classes(original, 'csAutosar')); + }); + }); + + describe('M7: a struct entry of no element a save wrote around an , without its fields', () => { + const BUILT = ['pre93', 'pre69664b0', 'pre90caf06']; + const FIXTURES = ['fixtures/display/dispcat_bin.sldd', 'parity/artifacts/binary/cases.sldd', GENERATED]; + + it('is how each of those saves spells every one of them, where the original states its fields', () => { + for (const build of BUILT) { + for (const fixture of FIXTURES) { + const c = oldSave(build, fixture); + const original = list(c.emptyStructs); + const saved = shapesOf(savedBytes(c.file)).emptyStructs; + expect(saved.map((s: { entry: string; dims: number[]; fields: string[]; aroundElement: boolean }) => [s.entry, s.dims, s.fields, s.aroundElement])).toEqual( + original.map((s) => [s.entry, s.dims, [], true]), + ); + } + } + const declared = FIXTURES.flatMap((f) => list(oldSave('pre90caf06', f).emptyStructs).filter((s) => list(s.fields).length > 0).map((s) => [s.entry, list(s.fields)])); + expect(declared).toEqual([ + ['dx_struct_0x0_fields', ['a']], + ['dx_struct_1x0', ['a']], + ['sE10ab', ['a', 'b']], + ]); + }); + + it('MATLAB reads in the shape its Dimension states, with no element; the fields are lost, and nothing else of it', () => { + for (const build of BUILT) { + for (const fixture of FIXTURES) { + const c = oldSave(build, fixture); + expect(list(c.matlab.emptyStructs).map((s) => [s.entry, s.shape])).toEqual(list(c.emptyStructs).map((s) => [s.entry, `struct[${s.dims.join(' ')}]{}`])); + const lost = list(c.emptyStructs) + .filter((s) => list(s.fields).length > 0) + .map((s) => `design|${s.entry}`); + const fine = list(c.emptyStructs) + .filter((s) => list(s.fields).length === 0) + .map((s) => `design|${s.entry}`); + expect(differKeys(c).filter((k) => lost.includes(k))).toEqual(lost); + expect(differKeys(c).filter((k) => fine.includes(k))).toEqual([]); + } + // A struct entry's field of no element went out the same way: sHolds.e, a 0x0 struct with field a. + const holds = list(oldSave(build, GENERATED).matlab.entries.differ).find((d) => d.startsWith('design|sHolds: ')); + expect(holds).toContain('e:struct[0 0]<>{}'); + expect(holds).toContain('wanted struct[1 1]{x:double[1 1]=1,e:struct[0 0]{}'); + } + }); + + it('core reads each as MATLAB does: in the shape the Dimension states, with no row and no field, and writes it so', () => { + for (const build of BUILT) { + for (const fixture of FIXTURES) { + const c = oldSave(build, fixture); + const root = openSave(c.file); + for (const s of list(c.matlab.emptyStructs)) { + const entry = entryOf(root, 'design', s.entry); + const value = JSON.parse(JSON.stringify(entry.serialize().value)); + const dims = s.shape.slice('struct['.length, s.shape.indexOf(']')).split(' ').map(Number); + expect([s.entry, entry.displayValue, entry.children.length, value._dimensions, value._fields]).toEqual([s.entry, `<${dims.join('x')} struct>`, 0, dims, []]); + expect(regenerateEntryXml(entry)).toContain(`

`); + } + } + } + }); + + it("and core's copy of each into a text dictionary reads in MATLAB as the old save does; a 0x0x2 as 0x0, as a copy of the original and MATLAB's own text save of it read", () => { + const fromOld = PASTES.find((c) => c.source === EMPTIES_OLD_SAVE)!; + const fromOriginal = PASTES.find((c) => list(c.pasted)[0]?.from === 'sE00' && c.source !== EMPTIES_OLD_SAVE)!; + for (const c of [fromOld, fromOriginal]) { + expect(differKeys(c)).toEqual([]); + expect(list(c.matlab.pasted).filter((p) => !p.same).map((p) => [p.to, p.shape])).toEqual([['sE002Pasted', 'struct[0 0]{}']]); + } + expect(list(fromOld.matlab.pasted).map((p) => [p.to, p.shape])).toEqual([ + ['sE00Pasted', 'struct[0 0]{}'], + ['sE02Pasted', 'struct[0 2]{}'], + ['sE20Pasted', 'struct[2 0]{}'], + ['sE002Pasted', 'struct[0 0]{}'], + ['sE10abPasted', 'struct[1 0]{}'], + ['sHoldsPasted', 'struct[1 1]{x,e,none}'], + ]); + const twin = list(TRUTH.originals).find((o) => o.file === 'orig_old_saves_r2027a_text.sldd')!; + expect(list(twin.shapes).find((s) => s.key === 'design|sE002')!.shape).toBe('struct[0 0]{}'); + }); + }); + + it("the other losses of those saves, each fixed since, are a shaped empty's shape and an enumeration value", () => { + // dispcat_bin.sldd holds every shaped empty: a cell, char or string of no element lost its + // extents (each since kept, in one form at an entry, a property and a cell), and before #93 + // an enumeration value at an entry its enumeral (kept since 146cb64), and a 0x2 char its shape. + const shaped = ['dx_cell_0x3', 'dx_cell_1x0', 'dx_char_1x0', 'dx_string_0x0', 'dx_string_1x0', 'str_0x3']; + const enums = ['dx_char_0x2', 'en_i32_high', 'en_i32_vec', 'en_sl_red', 'en_sl_vec', 'p_enum']; + const m7 = ['dx_struct_0x0_fields', 'dx_struct_1x0']; + const dispcat = 'fixtures/display/dispcat_bin.sldd'; + for (const [build, keys] of [ + ['pre93', [...shaped, ...enums, ...m7]], + ['pre69664b0', [...shaped, ...enums, ...m7]], + ['pre90caf06', [...shaped, ...m7]], + ] as const) { + expect([build, [...differKeys(oldSave(build, dispcat))].sort()]).toEqual([build, keys.map((k) => `design|${k}`).sort()]); + expect([build, differKeys(oldSave(build, 'parity/artifacts/binary/cases.sldd'))]).toEqual([build, []]); + } + }); + + describe("D15: core's copy of a mixed-class object array from a binary dictionary into a text one", () => { + const pasted = (to: string) => PASTES.flatMap((c) => list(c.matlab.pasted).filter((p) => p.to === to)); + + it('reads in MATLAB as the binary original, every other entry as it was', () => { + for (const c of PASTES.filter((c) => c.source !== EMPTIES_OLD_SAVE && list(c.pasted)[0]?.from !== 'sE00')) { + expect([c.file, c.matlab.asOriginal, differKeys(c)]).toEqual([c.file, true, []]); + } + }); + + it("a user's subclass of Simulink.Signal beside a Simulink.Signal, either first, as a Simulink.Signal array of each element's own class", () => { + expect(pasted('holdsMixedPasted').map((p) => [p.same, p.kids, p.sourceKids])).toEqual( + Array(2).fill([true, 'Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}', 'Simulink.Signal[1 2]{dexdata.DerivedSignal,Simulink.Signal}']), + ); + expect(pasted('holdsBasePasted').map((p) => [p.same, p.kids, p.sourceKids])).toEqual( + Array(2).fill([true, 'Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}', 'Simulink.Signal[1 2]{Simulink.Signal,dexdata.DerivedSignal}']), + ); + }); + + it("a config set's Components, as a Simulink.ConfigComponent array, an SSC.SimscapeCC among them", () => { + for (const p of pasted('csSimscapePasted')) { + expect([p.same, p.kids === p.sourceKids, p.kids]).toEqual([true, true, expect.stringMatching(/^Simulink\.ConfigComponent\[10 1\]\{Simulink\.SolverCC,.*,SSC\.SimscapeCC\}$/)]); + } + }); + + it("the AUTOSAR target's dynamic properties, which the text form holds among the ordinary ones, as the dynamic properties they were", () => { + for (const c of PASTES.filter((c) => list(c.dynamic).length > 0)) { + expect(list(c.matlab.dynamic).map((d) => [d.name, d.save === d.original, d.dynamicInSave])).toEqual(list(c.dynamic).map((d) => [d.name, true, true])); + } + expect(PASTES.filter((c) => list(c.dynamic).length > 0).length).toBe(2); + }); + }); +}); diff --git a/test/parity/oldsaves/make_old_saves_r2027a.m b/test/parity/oldsaves/make_old_saves_r2027a.m new file mode 100644 index 00000000..07715d2d --- /dev/null +++ b/test/parity/oldsaves/make_old_saves_r2027a.m @@ -0,0 +1,138 @@ +function make_old_saves_r2027a(outDir) + % Copyright 2026 The MathWorks, Inc. + % Authors old_saves_r2027a_binary.sldd and old_saves_r2027a_text.sldd: one dictionary, in + % each format, holding the shapes no other binary fixture holds that the read-back of core's + % older saves asks about (probe_old_saves_readback.m). Run (one line: mw splits -batch on + % newlines): + % + % mw -using Bmain matlab -nodesktop -batch "cd(''); make_old_saves_r2027a" + % + % `outDir` defaults to this folder. Verify by the WROTE lines and the trailing + % MAKE_OLD_SAVES_R2027A_DONE; a FAIL line means a shape below is not in the file. The + % committed files were written by R2027a (27.1.0.3409468) on 2026-10-18; a re-run gives new + % UUIDs and timestamps, and the same values. The old saves beside them were made from the + % committed binary file (test/parity/matlab/old_saves.mjs), so a re-run here asks for them, + % and for MATLAB's read-back, again. + % + % THE ENTRIES + % Configurations + % csAutosar a default Simulink.ConfigSet switched to autosar.tlc: its target component + % is a Simulink.STFCustomTargetCC, and the options autosar.tlc adds to it are + % written as dynamic properties (Source="dynamic" PropertyType="...") in a + % binary dictionary, and under _dynamic_properties in a text one + % csSimscape a default Simulink.ConfigSet with an SSC.SimscapeCC attached, a component + % no text dictionary lists under Simulink.ConfigComponent, where Simscape is + % installed + % Other Data + % holdsMixed an NdHolder whose Kids is [dexdata.DerivedSignal, Simulink.Signal]: a + % Simulink.Signal array whose first element is a user's subclass of it (a + % dictionary refuses a Simulink.Signal array as an entry's value, and Design + % Data a value of a user's class; Other Data takes one, whose property may hold + % the array) + % holdsBase the same with the base class first, [Simulink.Signal, dexdata.DerivedSignal] + % Design Data + % sE00 struct([]), a 0x0 struct with no fields + % sE02 a 0x2 struct with no fields + % sE20 a 2x0 struct with no fields + % sE002 a 0x0x2 struct with no fields + % sE10ab a 1x0 struct with fields a and b + % sHolds a 1x1 struct whose field e is a 0x0 struct with field a, and whose field + % none is a 0x0 struct with no fields + here = fileparts(mfilename('fullpath')); + if nargin < 1 || isempty(outDir) + outDir = here; + end + addpath(fullfile(here, '..', '..', 'fixtures', 'mcos')); % NdHolder, +dexdata + Simulink.data.dictionary.closeAll('-discard'); + configs = {}; + try + cs = Simulink.ConfigSet; + cs.Name = 'csAutosar'; + switchTarget(cs, 'autosar.tlc', []); + target = cs.getComponent('Code Generation').getComponent('Target'); + fprintf('CONFIG csAutosar target %s components %s\n', class(target), classesOf(cs.Components)); + configs(end + 1, :) = {'csAutosar', cs}; + catch err + fprintf('FAIL csAutosar: %s\n', err.message); + end + try + cs = Simulink.ConfigSet; + cs.Name = 'csSimscape'; + attachComponent(cs, SSC.SimscapeCC); + fprintf('CONFIG csSimscape components %s\n', classesOf(cs.Components)); + configs(end + 1, :) = {'csSimscape', cs}; + catch err + fprintf('FAIL csSimscape: %s\n', err.message); + end + other = {}; + try + mixed = NdHolder; + mixed.Tag = 'mixed'; + mixed.Kids = [dexdata.DerivedSignal, Simulink.Signal]; + base = NdHolder; + base.Tag = 'base'; + base.Kids = [Simulink.Signal, dexdata.DerivedSignal]; + fprintf('MIXED %s %s; BASE %s %s\n', class(mixed.Kids), classesOf(mixed.Kids), class(base.Kids), classesOf(base.Kids)); + other = {'holdsMixed', mixed; 'holdsBase', base}; + catch err + fprintf('FAIL mixed signals: %s\n', err.message); + end + design = {}; + design(end + 1, :) = {'sE00', struct([])}; + design(end + 1, :) = {'sE02', repmat(struct(), 0, 2)}; + design(end + 1, :) = {'sE20', repmat(struct(), 2, 0)}; + design(end + 1, :) = {'sE002', repmat(struct(), [0 0 2])}; + design(end + 1, :) = {'sE10ab', struct('a', cell(1, 0), 'b', cell(1, 0))}; + design(end + 1, :) = {'sHolds', struct('x', 1, 'e', struct('a', {}), 'none', struct([]))}; + formats = {'uncompressed-text', 'text'; 'compressed-binary', 'binary'}; + for i = 1:size(formats, 1) + fn = fullfile(outDir, ['old_saves_r2027a_' formats{i, 2} '.sldd']); + if exist(fn, 'file'), delete(fn); end + dd = Simulink.data.dictionary.create(fn); + dd.FileFormat = formats{i, 1}; + ds = dd.getSection('Design Data'); + for k = 1:size(design, 1) + try + ds.addEntry(design{k, 1}, design{k, 2}); + catch err + fprintf('FAIL %s %s: %s\n', formats{i, 2}, design{k, 1}, err.message); + end + end + os = dd.getSection('Other Data'); + for k = 1:size(other, 1) + try + os.addEntry(other{k, 1}, other{k, 2}); + catch err + fprintf('FAIL %s %s: %s\n', formats{i, 2}, other{k, 1}, err.message); + end + end + cfg = dd.getSection('Configurations'); + for k = 1:size(configs, 1) + try + cfg.addEntry(configs{k, 1}, configs{k, 2}); + catch err + fprintf('FAIL %s %s: %s\n', formats{i, 2}, configs{k, 1}, err.message); + end + end + clear ds os cfg + dd.saveChanges(); + dd.close(); + fprintf('WROTE %s\n', fn); + end + % What MATLAB reads of each entry, back from the binary file. + dd = Simulink.data.dictionary.open(fullfile(outDir, 'old_saves_r2027a_binary.sldd')); + for s = {'Design Data', 'Other Data', 'Configurations'} + entries = find(getSection(dd, s{1})); %#ok + for k = 1:numel(entries) + v = getValue(entries(k)); + fprintf('READ %s %s %s %s\n', s{1}, entries(k).Name, class(v), mat2str(size(v))); + end + end + close(dd); + Simulink.data.dictionary.closeAll('-discard'); + fprintf('MAKE_OLD_SAVES_R2027A_DONE\n'); +end + +function s = classesOf(objects) + s = strjoin(arrayfun(@(k) {class(objects(k))}, 1:numel(objects)), ','); +end diff --git a/test/parity/oldsaves/old_saves_r2027a_binary.sldd b/test/parity/oldsaves/old_saves_r2027a_binary.sldd new file mode 100644 index 00000000..dbf925e9 Binary files /dev/null and b/test/parity/oldsaves/old_saves_r2027a_binary.sldd differ diff --git a/test/parity/oldsaves/old_saves_r2027a_text.sldd b/test/parity/oldsaves/old_saves_r2027a_text.sldd new file mode 100644 index 00000000..edaff791 --- /dev/null +++ b/test/parity/oldsaves/old_saves_r2027a_text.sldd @@ -0,0 +1,769 @@ +{ + "__MW_TEXT_COREPROPERTIES__": { + "release": "R2027a" + }, + "__MW_TEXT_PARTS__": { + "__MW_TEXT_PART__/data/chunk0": { + "__MW_TEXT_content": { + "entries": [ + { + "name": "sE00", + "metadata": { + "uuid": "4e7876e5-399b-4a22-be29-31007668dc7d", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.502587", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_type": "struct", + "_value": "[]" + } + }, + { + "name": "sE002", + "metadata": { + "uuid": "7fd87c02-0923-4d6d-9214-1bdc8dfc2951", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.505428", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_type": "struct", + "_value": "[]" + } + }, + { + "name": "sE02", + "metadata": { + "uuid": "b52c5ea8-593b-4372-a7b9-83d3ab45b86f", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.505069", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_type": "struct", + "_value": "[]" + } + }, + { + "name": "sE10ab", + "metadata": { + "uuid": "9a6707de-258e-4094-9d11-19a2a7238113", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.505616", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_type": "struct", + "_value": "[]" + } + }, + { + "name": "sE20", + "metadata": { + "uuid": "90827797-fc7e-4bf5-826f-5a81d5efbf82", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.505228", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_type": "struct", + "_value": "[]" + } + }, + { + "name": "sHolds", + "metadata": { + "uuid": "8733824d-6e2e-4e4b-915f-f3155ac74b6c", + "namespace": "dacaf35e-55a5-454d-a7c1-93db038a210e", + "lastmod": "20261019T030832.505772", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_array_type": "Struct", + "_dimensions": [ + 1, + 1 + ], + "_elements": [ + { + "e": { + "_type": "struct", + "_value": "[]" + }, + "none": { + "_type": "struct", + "_value": "[]" + }, + "x": 1 + } + ], + "_fields": [ + "x", + "e", + "none" + ], + "_mw_element_type": "MATLABArray" + } + }, + { + "name": "csAutosar", + "metadata": { + "uuid": "185eb70b-104e-43a5-a0f3-3aa84b46ca2e", + "namespace": "a3b2532e-8e6e-47f5-94fb-b15daf666a84", + "lastmod": "20261019T030832.510190", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_array_class": "Simulink.ConfigSet", + "_dimensions": [ + 1, + 1 + ], + "_elements": [ + { + "_id": "1", + "_properties": { + "Components": { + "_array_class": "Simulink.ConfigComponent", + "_dimensions": [ + 9, + 1 + ], + "_elements": [ + { + "_id": "2", + "_object_class": "Simulink.SolverCC", + "_properties": { + "SolverName": "VariableStepAuto", + "Version": "27.0.0" + } + }, + { + "_id": "3", + "_object_class": "Simulink.DataIOCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "4", + "_object_class": "Simulink.OptimizationCC", + "_properties": { + "DefaultParameterBehavior": "Inlined", + "DifferentSizesBufferReuse": "on", + "EfficientTunableParamExpr": "on", + "EnhancedBackFolding": "on", + "OptimizationCustomize": "off", + "OptimizeBlockOrder": "speed", + "PassReuseOutputArgsAs": "Individual arguments", + "RemoveInternalMemoryZeroInit": "on", + "RemoveRootLevelIOMemoryZeroInit": "on", + "Version": "27.0.0" + } + }, + { + "_id": "5", + "_object_class": "Simulink.DebuggingCC", + "_properties": { + "ParameterTunabilityLossMsg": "error", + "Version": "27.0.0" + } + }, + { + "_id": "6", + "_object_class": "Simulink.HardwareCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "7", + "_object_class": "Simulink.ModelReferenceCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "8", + "_object_class": "Simulink.SFSimCC", + "_properties": { + "CustomCodeFunctionArrayLayout": [], + "MATLABDynamicMemAlloc": "off", + "Version": "27.0.0" + } + }, + { + "_id": "9", + "_object_class": "Simulink.RTWCC", + "_properties": { + "BuildConfiguration": "Faster Builds", + "Components": { + "_array_class": "Simulink.ConfigComponent", + "_dimensions": [ + 2, + 1 + ], + "_elements": [ + { + "_id": "10", + "_object_class": "Simulink.CodeAppCC", + "_properties": { + "ForceParamTrailComments": "on", + "IgnoreCustomStorageClasses": "off", + "InsertBlockDesc": "on", + "OperatorAnnotations": "on", + "SFDataObjDesc": "on", + "ShowEliminatedStatement": "on", + "SimulinkDataObjDesc": "on", + "Version": "27.0.0" + } + }, + { + "_dynamic_properties": { + "AutosarCompilerAbstraction": "off", + "AutosarMatrixIOAsArray": "off", + "AutosarMaxShortNameLength": 128, + "AutosarSchemaVersion": "R23-11", + "DisableAUTOSARRoutinesHostLibrary": "off" + }, + "_id": "11", + "_object_class": "Simulink.STFCustomTargetCC", + "_properties": { + "AutosarCompliant": "on", + "CPPClassGenCompliant": "off", + "CombineOutputUpdateFcns": "on", + "CombineSignalStateStructs": "on", + "CompOptLevelCompliant": "on", + "Components": { + "_id": "12", + "_object_class": "Simulink.ERTTargetCC", + "_properties": { + "AutosarCompliant": "on", + "BooleanFalseId": "FALSE", + "BooleanTrueId": "TRUE", + "CPPClassGenCompliant": "off", + "CombineOutputUpdateFcns": "on", + "CombineSignalStateStructs": "on", + "CompOptLevelCompliant": "on", + "ConcurrentExecutionCompliant": "off", + "ConvertIfToSwitch": "on", + "ERTCustomFileBanners": "on", + "GRTInterface": "off", + "GenerateAllocFcn": "off", + "GenerateSampleERTMain": "off", + "IncludeMdlTerminateFcn": "off", + "LUTObjectStructOrderEvenSpacing": "Size,Table,Breakpoints", + "LookupTableObjectStructAxisOrder": "2,1,3,4,...", + "MATLABClassNameForMDSCustomization": "Simulink.SoftwareTarget.ERTCustomization", + "MatFileLogging": "off", + "MaxIdInt16": "", + "MaxIdInt32": "", + "MaxIdInt64": "", + "MaxIdInt8": "", + "MaxIdUint16": "", + "MaxIdUint32": "", + "MaxIdUint64": "", + "MaxIdUint8": "", + "MinIdInt16": "", + "MinIdInt32": "", + "MinIdInt64": "", + "MinIdInt8": "", + "ModelReferenceCompliant": "on", + "ModelStepFunctionPrototypeControlCompliant": "off", + "MultiwordLength": 256, + "ParMdlRefBuildCompliant": "on", + "RemoveResetFunc": "on", + "SupportComplex": "off", + "SupportContinuousTime": "off", + "SupportNonFinite": "off", + "SupportNonInlinedSFcns": "off", + "SuppressErrorStatus": "on", + "SuppressUnreachableDefaultCases": "on", + "UseToolchainInfoCompliant": "on", + "Version": "27.0.0" + } + }, + "ConcurrentExecutionCompliant": "off", + "ERTCustomFileBanners": "on", + "GRTInterface": "off", + "GenerateAllocFcn": "off", + "GenerateSampleERTMain": "off", + "IncludeMdlTerminateFcn": "off", + "LUTObjectStructOrderEvenSpacing": "Size,Table,Breakpoints", + "LookupTableObjectStructAxisOrder": "2,1,3,4,...", + "MATLABClassNameForMDSCustomization": "Simulink.SoftwareTarget.ERTCustomization", + "MatFileLogging": "off", + "ModelReferenceCompliant": "on", + "ModelStepFunctionPrototypeControlCompliant": "off", + "MultiwordLength": 256, + "ParMdlRefBuildCompliant": "on", + "RemoveResetFunc": "on", + "SupportComplex": "off", + "SupportContinuousTime": "off", + "SupportNonFinite": "off", + "SupportNonInlinedSFcns": "off", + "SuppressErrorStatus": "on", + "SystemTargetFile": "autosar.tlc", + "UseToolchainInfoCompliant": "on", + "Version": "27.0.0" + } + } + ] + }, + "DataTypeReplacement": "CDataTypesFixedWidth", + "Description": "AUTOSAR", + "GenCodeOnly": "on", + "SystemTargetFile": "autosar.tlc", + "TemplateMakefile": "ert_default_tmf", + "Toolchain": "Automatically locate an installed toolchain", + "Version": "27.0.0" + } + }, + { + "_id": "13", + "_object_class": "SlCovCC.ConfigComp", + "_properties": { + "CovEnable": "off", + "CovIncludeTopModel": "on", + "CovModelRefEnable": "off", + "CovScope": "EntireSystem", + "Description": "Simulink Coverage Configuration Component", + "Version": "27.0.0" + } + } + ] + }, + "ConfigPrmDlgPosition": [ + 0, + 0, + 0, + 0 + ], + "CurrentDlgPage": "", + "ExtraOptions": "", + "Name": "csAutosar", + "Version": "27.0.0" + } + } + ], + "_mw_element_type": "MATLABArray" + } + }, + { + "name": "csSimscape", + "metadata": { + "uuid": "9ea55741-afc2-4183-b145-87155f8f40dd", + "namespace": "a3b2532e-8e6e-47f5-94fb-b15daf666a84", + "lastmod": "20261019T030832.520987", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_array_class": "Simulink.ConfigSet", + "_dimensions": [ + 1, + 1 + ], + "_elements": [ + { + "_id": "1", + "_properties": { + "Components": { + "_array_class": "Simulink.ConfigComponent", + "_dimensions": [ + 10, + 1 + ], + "_elements": [ + { + "_id": "2", + "_object_class": "Simulink.SolverCC", + "_properties": { + "SolverName": "VariableStepAuto", + "Version": "27.0.0" + } + }, + { + "_id": "3", + "_object_class": "Simulink.DataIOCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "4", + "_object_class": "Simulink.OptimizationCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "5", + "_object_class": "Simulink.DebuggingCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "6", + "_object_class": "Simulink.HardwareCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "7", + "_object_class": "Simulink.ModelReferenceCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "8", + "_object_class": "Simulink.SFSimCC", + "_properties": { + "CustomCodeFunctionArrayLayout": [], + "Version": "27.0.0" + } + }, + { + "_id": "9", + "_object_class": "Simulink.RTWCC", + "_properties": { + "BuildConfiguration": "Faster Builds", + "Components": { + "_array_class": "Simulink.ConfigComponent", + "_dimensions": [ + 2, + 1 + ], + "_elements": [ + { + "_id": "10", + "_object_class": "Simulink.CodeAppCC", + "_properties": { + "Version": "27.0.0" + } + }, + { + "_id": "11", + "_object_class": "Simulink.GRTTargetCC", + "_properties": { + "CPPClassGenCompliant": "on", + "CombineOutputUpdateFcns": "on", + "CompOptLevelCompliant": "on", + "ConcurrentExecutionCompliant": "on", + "ERTCustomFileBanners": "off", + "GRTInterface": "off", + "GenerateAllocFcn": "off", + "GenerateSampleERTMain": "off", + "MATLABClassNameForMDSCustomization": "Simulink.SoftwareTarget.GRTCustomization", + "MatFileLogging": "on", + "ModelReferenceCompliant": "on", + "ModelStepFunctionPrototypeControlCompliant": "off", + "MultiwordLength": 2048, + "ParMdlRefBuildCompliant": "on", + "SupportContinuousTime": "on", + "SupportNonFinite": "on", + "SupportNonInlinedSFcns": "on", + "UseToolchainInfoCompliant": "on", + "Version": "27.0.0" + } + } + ] + }, + "Description": "Generic Real-Time Target", + "SystemTargetFile": "grt.tlc", + "Toolchain": "Automatically locate an installed toolchain", + "Version": "27.0.0" + } + }, + { + "_id": "12", + "_object_class": "SlCovCC.ConfigComp", + "_properties": { + "CovEnable": "off", + "CovIncludeTopModel": "on", + "CovModelRefEnable": "off", + "CovScope": "EntireSystem", + "Description": "Simulink Coverage Configuration Component", + "Version": "27.0.0" + } + }, + { + "_id": "13", + "_object_class": "SSC.SimscapeCC", + "_properties": { + "Components": { + "_id": "14", + "_object_class": "simmechanics.ConfigurationSet", + "_properties": { + "Components": { + "_array_class": "Simulink.CustomCC", + "_dimensions": [ + 2, + 1 + ], + "_elements": [ + { + "_id": "15", + "_object_class": "simmechanics.DiagnosticsConfigSet", + "_properties": { + "SimMechanicsConflictingReferenceFrames": "warning", + "SimMechanicsCrossSectionNullEdge": "warning", + "SimMechanicsInvalidVisualProperty": "warning", + "SimMechanicsJointTargetOverSpecification": "error", + "SimMechanicsRedundantBlock": "warning", + "SimMechanicsRigidlyBoundBlock": "error", + "SimMechanicsUnconnectedFramePorts": "warning", + "SimMechanicsUnconnectedGeometryPorts": "warning", + "SimMechanicsUnsatisfiedHighPriorityTargets": "warning", + "Version": "27.0.0" + } + }, + { + "_id": "16", + "_object_class": "simmechanics.ExplorerConfigSet", + "_properties": { + "InternalSimMechanicsExplorerSettings": "", + "InternalSimMechanicsPlaybackDataRate": "30", + "InternalSimMechanicsSavePlaybackData": "none", + "InternalSimMechanicsSavePlaybackDataVariable": "smplaybacklog", + "SimMechanicsOpenEditorOnUpdate": "on", + "Version": "27.0.0" + } + } + ] + }, + "Version": "27.0.0" + } + }, + "EditingMode": "Full", + "ExplicitSolverDiagnosticOptions": "warning", + "GlobalZcOffDiagnosticOptions": "warning", + "SelectedTab": "", + "SimscapeAutoSelectReusableComponents": "on", + "SimscapeCacheCompileArtifacts": "on", + "SimscapeCompileArtifactsDiskCache": "off", + "SimscapeCompileComponentReuse": "off", + "SimscapeLogDataHistory": 10000, + "SimscapeLogDecimation": 1, + "SimscapeLogLimitData": "on", + "SimscapeLogName": "simlog", + "SimscapeLogOpenViewer": "off", + "SimscapeLogSimulationStatistics": "off", + "SimscapeLogType": "all", + "SimscapeMultithreadedCompilation": "on", + "SimscapeNominalValues": "[{\"value\":\"1\",\"unit\":\"A\"},{\"value\":\"1\",\"unit\":\"bar\"},{\"value\":\"1\",\"unit\":\"cm^2\"},{\"value\":\"1\",\"unit\":\"cm^3/s\"},{\"value\":\"1\",\"unit\":\"kJ/kg\"},{\"value\":\"1\",\"unit\":\"kW\"},{\"value\":\"1\",\"unit\":\"l\"},{\"value\":\"1\",\"unit\":\"N\"},{\"value\":\"1\",\"unit\":\"N*m\"},{\"value\":\"1\",\"unit\":\"V\"},{\"value\":\"0.001\",\"unit\":\"Wb\"}]", + "SimscapeNormalizeSystem": "off", + "SimscapeOperatingPoint": "", + "SimscapeUseEnergyAccounting": "off", + "SimscapeUseOperatingPoints": "off", + "Version": "27.0.0" + } + } + ] + }, + "ConfigPrmDlgPosition": [ + 0, + 0, + 0, + 0 + ], + "CurrentDlgPage": "", + "ExtraOptions": "", + "Name": "csSimscape", + "Version": "27.0.0" + } + } + ], + "_mw_element_type": "MATLABArray" + } + }, + { + "name": "holdsBase", + "metadata": { + "uuid": "bf293156-5cd5-4341-ad52-ec7964744b09", + "namespace": "42516768-0ace-4981-8ac7-0a9b32cba471", + "lastmod": "20261019T030832.509844", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_array_class": "NdHolder", + "_dimensions": [ + 1, + 1 + ], + "_elements": [ + { + "_id": "1", + "_properties": { + "Kids": { + "_array_class": "Simulink.Signal", + "_dimensions": [ + 1, + 2 + ], + "_elements": [ + { + "_id": "2", + "_properties": { + "CoderInfo": { + "_id": "4", + "_object_class": "Simulink.CoderInfo", + "_properties": { + "CSCPackageName": "Simulink", + "CustomAttributes": { + "_id": "5", + "_object_class": "SimulinkCSC.AttribClass_Simulink_Default", + "_properties": {} + }, + "CustomStorageClass": "Default", + "ParameterOrSignal": "Signal", + "StorageClass": "Auto" + } + }, + "LoggingInfo": { + "_id": "3", + "_object_class": "Simulink.LoggingInfo", + "_properties": {} + } + } + }, + { + "_id": "6", + "_object_class": "dexdata.DerivedSignal", + "_properties": { + "CoderInfo": { + "_id": "8", + "_object_class": "Simulink.CoderInfo", + "_properties": { + "CSCPackageName": "Simulink", + "CustomAttributes": { + "_id": "9", + "_object_class": "SimulinkCSC.AttribClass_Simulink_Default", + "_properties": {} + }, + "CustomStorageClass": "Default", + "ParameterOrSignal": "Signal", + "StorageClass": "Auto" + } + }, + "LoggingInfo": { + "_id": "7", + "_object_class": "Simulink.LoggingInfo", + "_properties": {} + } + } + } + ] + }, + "Tag": "base" + } + } + ], + "_mw_element_type": "MATLABArray" + } + }, + { + "name": "holdsMixed", + "metadata": { + "uuid": "969ed16c-ddf7-4f08-a565-78ba997a999c", + "namespace": "42516768-0ace-4981-8ac7-0a9b32cba471", + "lastmod": "20261019T030832.509126", + "modifiedby": "weiwang", + "isderived": "0" + }, + "value": { + "_array_class": "NdHolder", + "_dimensions": [ + 1, + 1 + ], + "_elements": [ + { + "_id": "1", + "_properties": { + "Kids": { + "_array_class": "Simulink.Signal", + "_dimensions": [ + 1, + 2 + ], + "_elements": [ + { + "_id": "2", + "_object_class": "dexdata.DerivedSignal", + "_properties": { + "CoderInfo": { + "_id": "4", + "_object_class": "Simulink.CoderInfo", + "_properties": { + "CSCPackageName": "Simulink", + "CustomAttributes": { + "_id": "5", + "_object_class": "SimulinkCSC.AttribClass_Simulink_Default", + "_properties": {} + }, + "CustomStorageClass": "Default", + "ParameterOrSignal": "Signal", + "StorageClass": "Auto" + } + }, + "LoggingInfo": { + "_id": "3", + "_object_class": "Simulink.LoggingInfo", + "_properties": {} + } + } + }, + { + "_id": "6", + "_properties": { + "CoderInfo": { + "_id": "8", + "_object_class": "Simulink.CoderInfo", + "_properties": { + "CSCPackageName": "Simulink", + "CustomAttributes": { + "_id": "9", + "_object_class": "SimulinkCSC.AttribClass_Simulink_Default", + "_properties": {} + }, + "CustomStorageClass": "Default", + "ParameterOrSignal": "Signal", + "StorageClass": "Auto" + } + }, + "LoggingInfo": { + "_id": "7", + "_object_class": "Simulink.LoggingInfo", + "_properties": {} + } + } + } + ] + }, + "Tag": "mixed" + } + } + ], + "_mw_element_type": "MATLABArray" + } + } + ], + "Dictionary References": [], + "AllowAccessBWS": false + } + } + } +} \ No newline at end of file diff --git a/test/parity/oldsaves/pre69664b0_cases.sldd b/test/parity/oldsaves/pre69664b0_cases.sldd new file mode 100644 index 00000000..d92ecba5 Binary files /dev/null and b/test/parity/oldsaves/pre69664b0_cases.sldd differ diff --git a/test/parity/oldsaves/pre69664b0_dispcat_bin.sldd b/test/parity/oldsaves/pre69664b0_dispcat_bin.sldd new file mode 100644 index 00000000..8c71448e Binary files /dev/null and b/test/parity/oldsaves/pre69664b0_dispcat_bin.sldd differ diff --git a/test/parity/oldsaves/pre69664b0_old_saves_r2027a_binary.sldd b/test/parity/oldsaves/pre69664b0_old_saves_r2027a_binary.sldd new file mode 100644 index 00000000..c1f1e731 Binary files /dev/null and b/test/parity/oldsaves/pre69664b0_old_saves_r2027a_binary.sldd differ diff --git a/test/parity/oldsaves/pre90caf06_cases.sldd b/test/parity/oldsaves/pre90caf06_cases.sldd new file mode 100644 index 00000000..356617fc Binary files /dev/null and b/test/parity/oldsaves/pre90caf06_cases.sldd differ diff --git a/test/parity/oldsaves/pre90caf06_dispcat_bin.sldd b/test/parity/oldsaves/pre90caf06_dispcat_bin.sldd new file mode 100644 index 00000000..edb0d743 Binary files /dev/null and b/test/parity/oldsaves/pre90caf06_dispcat_bin.sldd differ diff --git a/test/parity/oldsaves/pre90caf06_old_saves_r2027a_binary.sldd b/test/parity/oldsaves/pre90caf06_old_saves_r2027a_binary.sldd new file mode 100644 index 00000000..5e534f04 Binary files /dev/null and b/test/parity/oldsaves/pre90caf06_old_saves_r2027a_binary.sldd differ diff --git a/test/parity/oldsaves/pre93_cases.sldd b/test/parity/oldsaves/pre93_cases.sldd new file mode 100644 index 00000000..356617fc Binary files /dev/null and b/test/parity/oldsaves/pre93_cases.sldd differ diff --git a/test/parity/oldsaves/pre93_dispcat_bin.sldd b/test/parity/oldsaves/pre93_dispcat_bin.sldd new file mode 100644 index 00000000..957d61c1 Binary files /dev/null and b/test/parity/oldsaves/pre93_dispcat_bin.sldd differ diff --git a/test/parity/oldsaves/pre93_old_saves_r2027a_binary.sldd b/test/parity/oldsaves/pre93_old_saves_r2027a_binary.sldd new file mode 100644 index 00000000..deb7b4c9 Binary files /dev/null and b/test/parity/oldsaves/pre93_old_saves_r2027a_binary.sldd differ