Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 188 additions & 0 deletions test/parity/m9MergedSaves.truth.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright 2026 The MathWorks, Inc.
//
// What MATLAB read back of S2's merged saves of a binary dictionary (M9 of the 2026-10-13
// save-fidelity plan, the last of its MATLAB read-backs), held to what core writes now.
//
// matlab/m9_merged_saves.mjs makes the saves through core's API: in every compressed-binary .sldd
// 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 it holds is renamed, has its Description set, and where its class
// has one cheap to edit, a nested part edited (a bus element's Description, a struct field), each
// edit made as a host makes it (session.editProperty) and saved by session.serializeSource into
// its own file. matlab/probe_m9_merged_reopen.m had
// MATLAB R2027a open each save and a copy of its original, and wrote to
// matlab/m9_merged_saves.truth.json, for every save, whether every entry nobody edited reads as in
// the original (class, isequaln and wbcompare.m's fullsig), whether the edited entry reads as the
// original with the same edit made in MATLAB, and whether the save opens and reads with no error
// or warning.
//
// What MATLAB read: every save of a fixture MATLAB can open reads as its original does, the edited
// entry as MATLAB makes the same edit, every other entry as it was, and with no error or warning
// but those the original itself gives. Three fixtures core wrote by hand, MATLAB cannot open at all.
// And MATLAB's own rename does one thing core's does not: it writes the new name into the value's
// Name property too, where it has one (a Simulink.VariantBank, the fixtures' MyGadget).
//
// This test makes the same saves from the package's source and asks that each is still the save
// MATLAB was asked about (every part's SHA-256, the edited entry's LastMod masked): a change to
// what core writes for any of them fails here until MATLAB is asked again (re-run the two, as the
// probe's header says). A fixture the truth names that is not in the tree is skipped, and said to
// be; a binary fixture the truth does not name fails, since MATLAB was never asked about it.
import { describe, it, expect } from 'vitest';
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 { inLayout, layoutOf } from '../../src/datamodel/parser/ChunkLayout.js';
import { binaryFixtures, m9MergedSaves, setWriters, withOlderSaves } from './matlab/m9_merged_saves.mjs';
import '../../src/datamodel/node/data/NodeClassMap.js';

type Matlab = {
asked?: boolean;
opened?: boolean;
warnings?: string[] | string;
asExpected?: boolean;
asOriginal?: boolean;
edited?: { class: string; same: boolean; sameAsMatlabEdit: boolean; absentOld: boolean; bySignature: boolean; why: string; whyMatlab: string };
unedited?: { checked: number; same: number; bySignature: number; differ: string[] | string };
error?: string;
};
type Case = {
fixture: string;
variant: string;
original: string;
file: string;
section: string;
entry: string;
kind: string;
path: unknown;
edit: string;
prop: string;
value: string;
refused: string;
save?: { chunk: string; parts: string };
merged?: boolean;
untouched?: number[];
otherPartsKept?: boolean;
matlab: Matlab;
};
type Original = { file: string; opened: boolean; warnings: string[] | string; entries: number; notSelfEqual: string[] | string };

const at = (rel: string) => fileURLToPath(new URL(rel, import.meta.url));
const TEST_DIR = at('..');
const TRUTH: { version: string; release: string; originals: Original[] | Original; cases: Case[] } = JSON.parse(
readFileSync(at('./matlab/m9_merged_saves.truth.json'), 'utf8'),
);
const ORIGINALS = ([] as Original[]).concat(TRUTH.originals);
/** MATLAB writes a one-element array as the element, and an empty one as []: every list as a list. */
const list = <T>(x: T[] | T | undefined): T[] => (x === undefined ? [] : ([] as T[]).concat(x));
/** What a row asked MATLAB about, in the form both halves write it. */
const input = (c: Case) => ({
fixture: c.fixture,
variant: c.variant,
original: c.original,
file: c.file,
section: c.section,
entry: c.entry,
kind: c.kind,
edit: c.edit,
prop: c.prop,
value: c.value,
path: c.path === '' || (Array.isArray(c.path) && c.path.length === 0) ? '' : c.path,
refused: c.refused,
save: c.save,
merged: c.merged,
untouched: c.untouched,
otherPartsKept: c.otherPartsKept,
});
const FIXTURES = binaryFixtures(TEST_DIR);
const PRESENT = new Set(FIXTURES.map((f: { rel: string }) => f.rel));
const NAMED = [...new Set(TRUTH.cases.map((c) => c.fixture))];
const asked = TRUTH.cases.filter((c) => c.refused === '');

setWriters({ regenerateEntryXml, layoutOf, inLayout });
const MADE: Case[] = m9MergedSaves({ createSession, ingest }, withOlderSaves(FIXTURES)).cases;

describe("MATLAB's read-back of S2's merged binary saves (M9)", () => {
it("the truth is MATLAB R2027a's, and every save was read", () => {
expect(TRUTH.release).toBe('2027a');
expect(asked.length).toBeGreaterThan(200);
expect(asked.filter((c) => c.matlab.asked === false || c.matlab.opened === undefined)).toEqual([]);
});

it('every binary fixture in the tree is one MATLAB was asked about', () => {
expect([...PRESENT].filter((rel) => !NAMED.includes(rel))).toEqual([]);
});

for (const fixture of NAMED) {
// Skipped, and so reported, where the fixture is not in the tree.
it.skipIf(!existsSync(at('../' + fixture)))(`core still makes each save of ${fixture} that MATLAB was asked about`, () => {
const made = MADE.filter((c) => c.fixture === fixture).map(input);
expect(made).toEqual(TRUTH.cases.filter((c) => c.fixture === fixture).map(input));
});
}

it('the one edit core refuses is not asked: an encoded value it can show but not write', () => {
expect(TRUTH.cases.filter((c) => c.refused !== '').map((c) => [c.fixture, c.entry, c.edit])).toEqual([
['fixtures/sparse/hexobj_binary.sldd', 'pTall', 'description'],
]);
});

it('MATLAB cannot open the three fixtures core wrote by hand (make-fixtures.mjs), so no save of theirs is held to them', () => {
expect(ORIGINALS.filter((o) => !o.opened).map((o) => o.file)).toEqual(['orig_arch_binary.sldd', 'orig_compressed.sldd', 'orig_object_array_binary.sldd']);
const unheld = asked.filter((c) => !ORIGINALS.find((o) => o.file === c.original)!.opened);
expect(unheld.length).toBe(10);
for (const c of unheld) {
expect(c.matlab.error, c.file).toMatch(/^MATLAB cannot open the original: Failed to open file/);
}
});

it('every other save reads in MATLAB as the original does: the edited entry as MATLAB edits it, every other entry as it was', () => {
const held = asked.filter((c) => ORIGINALS.find((o) => o.file === c.original)!.opened);
expect(held.length).toBe(asked.length - 10);
const wrong = held
.filter((c) => !(c.matlab.error === '' && c.matlab.asOriginal && c.matlab.edited!.same && c.matlab.edited!.absentOld && list(c.matlab.unedited!.differ).length === 0))
.map((c) => `${c.file} ${c.entry} ${c.edit}: ${c.matlab.error || c.matlab.edited?.why || list(c.matlab.unedited?.differ).join('; ')}`);
expect(wrong).toEqual([]);
for (const c of held) {
expect(c.matlab.unedited!.same, c.file).toBe(c.matlab.unedited!.checked);
}
// And the merge was at work in them: the edited entry's object is not what writing it whole gives.
expect(held.filter((c) => c.merged).length).toBeGreaterThanOrEqual(40);
});

it('with no error or warning, where the original has none; where it has, with no more than it', () => {
// params.sldd names a referenced dictionary, common.sldd, that is not in the tree; the classes
// of two of object_props_binary.sldd's values are not either.
expect(ORIGINALS.filter((o) => o.opened && list(o.warnings).length > 0).map((o) => [o.file, list(o.warnings).length])).toEqual([
['orig_object_props_binary.sldd', 2],
['orig_parity_binary_params.sldd', 2],
]);
const warned = new Set(['orig_object_props_binary.sldd', 'orig_parity_binary_params.sldd']);
for (const c of asked.filter((c) => ORIGINALS.find((o) => o.file === c.original)!.opened)) {
expect([c.file, c.matlab.asExpected], c.file).toEqual([c.file, !warned.has(c.original)]);
}
});

it("MATLAB's own rename also sets a Name property of the value, of a VariantBank and of MyGadget; core's leaves the value as it was", () => {
// M9 holds a rename to the original's value under the old name, which every save reads as.
// MATLAB's Entry.Name = … writes the new name into the value's own Name as well.
const differ = asked.filter((c) => c.matlab.edited?.same !== undefined && !c.matlab.edited.sameAsMatlabEdit);
expect(differ.map((c) => [c.file, c.entry, c.edit, c.kind])).toEqual([
['m9_classes_r2027a_binary_9.sldd', 'vbD', 'rename', 'Simulink.VariantBank'],
['m9_rt_bin_1.sldd', 'gadget', 'rename', 'MyGadget'],
['m9_rt_out_rt_bin_1.sldd', 'gadget', 'rename', 'MyGadget'],
['m9_variantbank_r2027a_binary_2.sldd', 'vbD', 'rename', 'Simulink.VariantBank'],
['m9_variantbank_r2027a_binary_older_1.sldd', 'vb1', 'rename', 'Simulink.VariantBank'],
]);
for (const c of differ) {
const [read, wanted] = c.matlab.edited!.whyMatlab.split(' | wanted ');
expect([read.includes(`'${c.value}'`), wanted.includes(`'${c.value}'`)], c.file).toEqual([false, true]);
}
});

it('an anonymous function handle is equal to no other, so the one entry holding one is held to its class and fullsig alone', () => {
expect(ORIGINALS.filter((o) => list(o.notSelfEqual).length > 0).map((o) => [o.file, list(o.notSelfEqual)])).toEqual([
['orig_sparse_hexobj_binary.sldd', ['design|anonAlias']],
]);
});
});
1 change: 1 addition & 0 deletions test/parity/matlab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ directory, because `Simulink.data.dictionary.open` rejects a relative one with
| `probe_writeback_bin.mjs` + `.m` | **the acceptance gate for the BINARY dictionary**: does MATLAB read back the XML chunk our writer emits? (defects 27-30) | `$PROBE_OUT` | — |
| `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_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` |
Expand Down
Loading
Loading