pasted
')); + return entry.slice(0, entry.indexOf('')).match(/'); + return xml.slice(xml.indexOf('
'));
+ };
+ expect(valueXml(entryOf(open(notAList(kind, value)).root, kind.entry))).toBe(valueXml(entryOf(emptied(kind).root, kind.entry)));
+ });
+});
diff --git a/test/parameterShape.test.ts b/test/parameterShape.test.ts
new file mode 100644
index 00000000..d2570a24
--- /dev/null
+++ b/test/parameterShape.test.ts
@@ -0,0 +1,332 @@
+// Copyright 2026 The MathWorks, Inc.
+//
+// A Simulink.Parameter's Dimensions and Complexity after an edit of its Value (B-25). MATLAB keeps
+// them as the Value's: it refuses a numeric Dimensions other than its size
+// (UDDData_CannotSetCustomDimensions) and any Complexity set (UDDData_CannotSetComplexity), and after
+// every edit of the Value they are its size and its complexity (measured, R2027a:
+// test/parity/matlab/probe_parameter_shape.m, truth parameter_shape.truth.json), except that
+// - a symbolic Dimensions ('[1,N]') is kept;
+// - an empty Value ([]) keeps both as they were: [1+2i 3+4i 5+6i] typed over with [] is still
+// [1 3] and complex;
+// - a struct Value keeps the Complexity it was given: [] and then struct('field', 0) typed over
+// a complex row leaves 'complex';
+// - text is a string scalar: 'abc' and '' are [1 1] and real.
+// Core kept the Dimensions and Complexity it read: pRow cut from three elements to two still
+// showed and saved [1 3], and [1 2 3 4] typed over it saved [1 3] and complex. The ruling A7
+// (2026-10-15) had fixed only the removal that collapses a Value to a scalar.
+//
+// Each probe row is made here by the edit core's tree makes for it (parameter_shape.steps.json),
+// through the session as a host makes it, on the same entry of the same MATLAB-written file.
+import { describe, it, expect } from 'vitest';
+import { readFileSync } from 'node:fs';
+import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
+import { fileURLToPath } from 'node:url';
+import { createSession, ingest } from '../src/index.js';
+import '../src/datamodel/node/data/NodeClassMap.js';
+import { applyStep, fileBytes as planFileBytes, parameterShapeSaves, PLAN } from './parity/matlab/parameter_shape.mjs';
+
+const here = (rel: string): string => fileURLToPath(new URL(rel, import.meta.url));
+const truth = JSON.parse(readFileSync(here('./parity/matlab/parameter_shape.truth.json'), 'utf8'));
+const plan = PLAN as { files: Record pRow end) {
+ throw new Error(`complex_binary.sldd: no ${key} in pRow`);
+ }
+ const from = xml.lastIndexOf('\n', tag);
+ const close = xml.indexOf('
${spec.entry}
`); + const end = xml.indexOf('', at); + const tag = xml.indexOf(spec.replace, at); + if (at < 0 || tag < 0 || tag > end) { + throw new Error(`${name}: no ${spec.replace} in ${spec.entry}`); + } + parts['data/chunk0.xml'] = strToU8(xml.slice(0, tag) + spec.with + xml.slice(tag + spec.replace.length)); + return zipSync(parts); +} + +/** + * Make one step of a row on the entry `name` as a host makes it, through the session, which + * records its undo: ['cut', k] removes the element row k, ['add'] is Add on the Value row, + * ['element', k, text] sets the element row k's Value, ['typed', text] the Parameter's. Throws + * where core refuses it. + */ +export function applyStep(session, root, name, step) { + const entry = entryOf(root, name); + const value = entry._valueNode; + let done; + if (step[0] === 'cut') { + done = session.deleteNodeById(value.children[step[1]].id) === true; + } else if (step[0] === 'add') { + done = session.addChildTo(value.id) !== null; + } else if (step[0] === 'element') { + const el = value.children[step[1]]; + session.setActive(root, el); + done = session.editProperty(el.id, 'Value', step[2]) === true; + } else if (step[0] === 'typed') { + session.setActive(root, entry); + done = session.editProperty(entry.id, 'Value', step[1]) === true; + } + if (!done) { + throw new Error(`${name}: ${JSON.stringify(step)} was refused`); + } +} + +/** `core` is { createSession, ingest }; `read(rel)` the bytes of a file under the package root. */ +export function parameterShapeSaves(core, read, ids = Object.keys(PLAN.rows)) { + const truth = JSON.parse(readFileSync(here('./parameter_shape.truth.json'), 'utf8')); + const files = {}; + const cases = []; + for (const id of ids) { + const made = PLAN.rows[id]; + const entry = truth.rows.find((r) => r.id === id).entry; + const session = core.createSession(); + const root = core.ingest(session, ab(fileBytes(made.file, read)), { filename: made.file }); + for (const step of made.steps) { + applyStep(session, root, entry, step); + } + const bytes = session.serializeSource(root.name).bytes; + const props = entryOf(root, entry).serializeValue()._elements[0]._properties; + files[`core_${id}.sldd`] = bytes; + cases.push({ id, entry, file: `core_${id}.sldd`, saved: [props.Dimensions, props.Complexity] }); + } + return { files, cases }; +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) { + const outdir = process.argv[2] || '/tmp/paramshape'; + const core = await import(here('../../../dist/index.js').href); + const read = (rel) => new Uint8Array(readFileSync(here('../../../' + rel))); + const { files, cases } = parameterShapeSaves(core, read); + mkdirSync(outdir, { recursive: true }); + for (const [name, bytes] of Object.entries(files)) { + writeFileSync(`${outdir}/${name}`, bytes); + } + for (const c of cases) { + console.log(c.file, c.entry, JSON.stringify(c.saved)); + } +} diff --git a/test/parity/matlab/parameter_shape.steps.json b/test/parity/matlab/parameter_shape.steps.json new file mode 100644 index 00000000..ba09a17c --- /dev/null +++ b/test/parity/matlab/parameter_shape.steps.json @@ -0,0 +1,373 @@ +{ + "about": "Core's edit for each row of probe_parameter_shape.m that core's tree can make, on the same entry of the same file: cut k removes the element row k (0-based), add is Add on the Value row, element k sets the element row k's Value, typed sets the Parameter's Value. leftOut names the rows core cannot make, and why. readsDiffer names the rows whose save MATLAB reads with another Dimensions or Complexity than its own edit leaves, and why. Read by ../../parameterShape.test.ts and parameter_shape.mjs.", + "files": { + "complex_binary.sldd": "test/fixtures/mcos/complex_binary.sldd", + "symbolic_dims.sldd": { + "from": "test/fixtures/mcos/complex_binary.sldd", + "entry": "pRow", + "replace": "1.0 3.0
", + "with": "[1,N]
", + "why": "the fixture with pRow's Dimensions spelled as MATLAB spells the symbolic '[1,N]' (the probe's MAKE, symbolicDimensionsTag in the truth). Made here and not kept: MATLAB's own save of it respells every entry and names the user who saved it, and a binary fixture in the tree is held to the M9 read-back (m9MergedSaves.truth.test.ts)." + } + }, + "rows": { + "cut_row_to_2": { + "file": "complex_binary.sldd", + "steps": [ + [ + "cut", + 2 + ] + ] + }, + "cut_row_to_1": { + "file": "complex_binary.sldd", + "steps": [ + [ + "cut", + 2 + ], + [ + "cut", + 1 + ] + ] + }, + "cut_col_to_2": { + "file": "complex_binary.sldd", + "steps": [ + [ + "cut", + 2 + ] + ] + }, + "cut_int16_to_2": { + "file": "complex_binary.sldd", + "steps": [ + [ + "cut", + 2 + ] + ] + }, + "add_row": { + "file": "complex_binary.sldd", + "steps": [ + [ + "add" + ] + ] + }, + "add_col": { + "file": "complex_binary.sldd", + "steps": [ + [ + "add" + ] + ] + }, + "add_int16": { + "file": "complex_binary.sldd", + "steps": [ + [ + "add" + ] + ] + }, + "add_then_cut": { + "file": "complex_binary.sldd", + "steps": [ + [ + "add" + ], + [ + "cut", + 3 + ] + ] + }, + "element_one_real": { + "file": "complex_binary.sldd", + "steps": [ + [ + "element", + 0, + "7" + ] + ] + }, + "elements_all_real": { + "file": "complex_binary.sldd", + "steps": [ + [ + "element", + 0, + "7" + ], + [ + "element", + 1, + "8" + ], + [ + "element", + 2, + "9" + ] + ] + }, + "typed_real_row": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2 3 4]" + ] + ] + }, + "typed_real_col": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1; 2; 3]" + ] + ] + }, + "typed_matrix": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2; 3 4]" + ] + ] + }, + "typed_scalar": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "5" + ] + ] + }, + "typed_complex": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "3+4i" + ] + ] + }, + "typed_empty": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[]" + ] + ] + }, + "typed_empty_char": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "''" + ] + ] + }, + "typed_empty_string": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "\"\"" + ] + ] + }, + "typed_empty_on_real": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2]" + ], + [ + "typed", + "[]" + ] + ] + }, + "typed_empty_on_struct": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[]" + ] + ] + }, + "typed_empty_then_row": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[]" + ], + [ + "typed", + "[1 2 3 4]" + ] + ] + }, + "typed_char": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "'abc'" + ] + ] + }, + "typed_string": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "\"abc\"" + ] + ] + }, + "typed_logical_row": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[true false]" + ] + ] + }, + "typed_logical": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "true" + ] + ] + }, + "typed_over_scalar": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2 3 4]" + ] + ] + }, + "typed_over_struct": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2 3 4]" + ] + ] + }, + "typed_then_add": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2 3]" + ], + [ + "add" + ] + ] + }, + "typed_then_cut": { + "file": "complex_binary.sldd", + "steps": [ + [ + "typed", + "[1 2 3]" + ], + [ + "cut", + 2 + ] + ] + }, + "struct_add_field": { + "file": "complex_binary.sldd", + "steps": [ + [ + "add" + ] + ] + }, + "symbolic_set": { + "file": "symbolic_dims.sldd", + "steps": [] + }, + "symbolic_cut": { + "file": "symbolic_dims.sldd", + "steps": [ + [ + "cut", + 2 + ] + ] + }, + "symbolic_add": { + "file": "symbolic_dims.sldd", + "steps": [ + [ + "add" + ] + ] + }, + "symbolic_typed": { + "file": "symbolic_dims.sldd", + "steps": [ + [ + "typed", + "[1 2 3 4]" + ] + ] + }, + "symbolic_typed_scalar": { + "file": "symbolic_dims.sldd", + "steps": [ + [ + "typed", + "5" + ] + ] + }, + "symbolic_typed_empty": { + "file": "symbolic_dims.sldd", + "steps": [ + [ + "typed", + "[]" + ] + ] + } + }, + "leftOut": { + "typed_zero_imag": "MATLAB evaluates 1+0i to the real 1; core holds the typed literal as the complex 1+0i, and writes it so, with Complexity complex to agree with it. The Value's class is not this row's question.", + "typed_empty_then_struct": "core has no way to make a struct Value of another one: MatlabValueParser reads no struct(...), and an empty Value has no row to Add on.", + "typed_struct_over_complex": "as typed_empty_then_struct.", + "dimensions_set_other": "MATLAB refuses it (UDDData_CannotSetCustomDimensions); core shows Dimensions as a label, with no editor.", + "complexity_set_real": "MATLAB refuses it (UDDData_CannotSetComplexity); core shows Complexity as a label, with no editor." + }, + "readsDiffer": { + "elements_all_real": "not the shape: core's binary writer spells a complex array whose elements are all real now as[7.0, 8.0, 9.0]
, with no Dimension (as on main 38c20de), which MATLAB reads as a 1x1 complex Value, and gives that Value's [1 1] and complex." + } +} diff --git a/test/parity/matlab/parameter_shape.truth.json b/test/parity/matlab/parameter_shape.truth.json new file mode 100644 index 00000000..6de47fd7 --- /dev/null +++ b/test/parity/matlab/parameter_shape.truth.json @@ -0,0 +1,1806 @@ +{ + "version": "27.1.0.3409468 (R2027a) Prerelease", + "release": "2027a", + "date": "2026-10-19", + "symbolicDimensionsTag": "[1,N]
", + "rows": [ + { + "id": "cut_row_to_2", + "entry": "pRow", + "edit": "p.Value(3) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "id": "cut_row_to_1", + "entry": "pRow", + "edit": "p.Value(3) = []; p.Value(2) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "cut_col_to_2", + "entry": "pCol", + "edit": "p.Value(3) = [];", + "before": { + "dimensions": [ + 3, + 1 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 2, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 2, + 1 + ], + "isreal": false + } + }, + { + "id": "cut_int16_to_2", + "entry": "pInt16Row", + "edit": "p.Value(3) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "complex", + "valueClass": "int16", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "id": "add_row", + "entry": "pRow", + "edit": "p.Value(end+1) = 0;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "id": "add_col", + "entry": "pCol", + "edit": "p.Value(end+1) = 0;", + "before": { + "dimensions": [ + 3, + 1 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 4, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 4, + 1 + ], + "isreal": false + } + }, + { + "id": "add_int16", + "entry": "pInt16Row", + "edit": "p.Value(end+1) = 0;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "complex", + "valueClass": "int16", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "id": "add_then_cut", + "entry": "pRow", + "edit": "p.Value(end+1) = 0; p.Value(end) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "id": "element_one_real", + "entry": "pRow", + "edit": "p.Value(1) = 7;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "id": "elements_all_real", + "entry": "pRow", + "edit": "p.Value(1) = 7; p.Value(2) = 8; p.Value(3) = 9;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": true + } + }, + { + "id": "typed_real_row", + "entry": "pRow", + "edit": "p.Value = [1 2 3 4];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "typed_real_col", + "entry": "pRow", + "edit": "p.Value = [1; 2; 3];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 3, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 3, + 1 + ], + "isreal": true + } + }, + { + "id": "typed_matrix", + "entry": "pRow", + "edit": "p.Value = [1 2; 3 4];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 2, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 2, + 2 + ], + "isreal": true + } + }, + { + "id": "typed_scalar", + "entry": "pRow", + "edit": "p.Value = 5;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "id": "typed_complex", + "entry": "pRow", + "edit": "p.Value = 3+4i;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_zero_imag", + "entry": "pRow", + "edit": "p.Value = 1+0i;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "id": "typed_empty", + "entry": "pRow", + "edit": "p.Value = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "id": "typed_empty_char", + "entry": "pRow", + "edit": "p.Value = '';", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_empty_string", + "entry": "pRow", + "edit": "p.Value = \"\";", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_empty_on_real", + "entry": "pRow", + "edit": "p.Value = [1 2]; p.Value = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "id": "typed_empty_on_struct", + "entry": "pStruct", + "edit": "p.Value = [];", + "before": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "id": "typed_empty_then_row", + "entry": "pRow", + "edit": "p.Value = []; p.Value = [1 2 3 4];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "typed_empty_then_struct", + "entry": "pRow", + "edit": "p.Value = []; p.Value = struct('field', 0);", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "struct", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_struct_over_complex", + "entry": "pRow", + "edit": "p.Value = struct('field', 0);", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "struct", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_char", + "entry": "pRow", + "edit": "p.Value = 'abc';", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_string", + "entry": "pRow", + "edit": "p.Value = \"abc\";", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "typed_logical_row", + "entry": "pRow", + "edit": "p.Value = [true false];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "logical", + "valueSize": [ + 1, + 2 + ], + "isreal": true + } + }, + { + "id": "typed_logical", + "entry": "pRow", + "edit": "p.Value = true;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "logical", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "id": "typed_over_scalar", + "entry": "pScalar", + "edit": "p.Value = [1 2 3 4];", + "before": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "typed_over_struct", + "entry": "pStruct", + "edit": "p.Value = [1 2 3 4];", + "before": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "typed_then_add", + "entry": "pRow", + "edit": "p.Value = [1 2 3]; p.Value(end+1) = 0;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "typed_then_cut", + "entry": "pRow", + "edit": "p.Value = [1 2 3]; p.Value(3) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": true + } + }, + { + "id": "struct_add_field", + "entry": "pStruct", + "edit": "p.Value.field = 0;", + "before": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "struct", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "id": "symbolic_set", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]';", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "id": "symbolic_cut", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]'; p.Value(3) = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "id": "symbolic_add", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]'; p.Value(end+1) = 0;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "id": "symbolic_typed", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]'; p.Value = [1 2 3 4];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "id": "symbolic_typed_scalar", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]'; p.Value = 5;", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "id": "symbolic_typed_empty", + "entry": "pRow", + "edit": "p.Dimensions = '[1,N]'; p.Value = [];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": true, + "identifier": "", + "message": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "id": "dimensions_set_other", + "entry": "pRow", + "edit": "p.Dimensions = [1 5];", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": false, + "identifier": "Simulink:Data:UDDData_CannotSetCustomDimensions", + "message": "To set 'Dimensions' for data object, 'Value' must be empty.", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "id": "complexity_set_real", + "entry": "pRow", + "edit": "p.Complexity = 'real';", + "before": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex" + }, + "ok": false, + "identifier": "Simulink:Data:UDDData_CannotSetComplexity", + "message": "'Complexity' must match complexity of numeric 'Value'.", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + } + ], + "reads": [ + { + "file": "core_add_col.sldd", + "id": "add_col", + "entry": "pCol", + "error": "", + "after": { + "dimensions": [ + 4, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 4, + 1 + ], + "isreal": false + } + }, + { + "file": "core_add_int16.sldd", + "id": "add_int16", + "entry": "pInt16Row", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "complex", + "valueClass": "int16", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "file": "core_add_row.sldd", + "id": "add_row", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "file": "core_add_then_cut.sldd", + "id": "add_then_cut", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "file": "core_cut_col_to_2.sldd", + "id": "cut_col_to_2", + "entry": "pCol", + "error": "", + "after": { + "dimensions": [ + 2, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 2, + 1 + ], + "isreal": false + } + }, + { + "file": "core_cut_int16_to_2.sldd", + "id": "cut_int16_to_2", + "entry": "pInt16Row", + "error": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "complex", + "valueClass": "int16", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "file": "core_cut_row_to_1.sldd", + "id": "cut_row_to_1", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_cut_row_to_2.sldd", + "id": "cut_row_to_2", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "file": "core_element_one_real.sldd", + "id": "element_one_real", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "file": "core_elements_all_real.sldd", + "id": "elements_all_real", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_struct_add_field.sldd", + "id": "struct_add_field", + "entry": "pStruct", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "struct", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_symbolic_add.sldd", + "id": "symbolic_add", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": false + } + }, + { + "file": "core_symbolic_cut.sldd", + "id": "symbolic_cut", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": false + } + }, + { + "file": "core_symbolic_set.sldd", + "id": "symbolic_set", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 3 + ], + "isreal": false + } + }, + { + "file": "core_symbolic_typed.sldd", + "id": "symbolic_typed", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_symbolic_typed_empty.sldd", + "id": "symbolic_typed_empty", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "file": "core_symbolic_typed_scalar.sldd", + "id": "symbolic_typed_scalar", + "entry": "pRow", + "error": "", + "after": { + "dimensions": "[1,N]", + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "file": "core_typed_char.sldd", + "id": "typed_char", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_typed_complex.sldd", + "id": "typed_complex", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_typed_empty.sldd", + "id": "typed_empty", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 3 + ], + "complexity": "complex", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "file": "core_typed_empty_char.sldd", + "id": "typed_empty_char", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_typed_empty_on_real.sldd", + "id": "typed_empty_on_real", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "file": "core_typed_empty_on_struct.sldd", + "id": "typed_empty_on_struct", + "entry": "pStruct", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 0, + 0 + ], + "isreal": true + } + }, + { + "file": "core_typed_empty_string.sldd", + "id": "typed_empty_string", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_typed_empty_then_row.sldd", + "id": "typed_empty_then_row", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_typed_logical.sldd", + "id": "typed_logical", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "file": "core_typed_logical_row.sldd", + "id": "typed_logical_row", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": true + } + }, + { + "file": "core_typed_matrix.sldd", + "id": "typed_matrix", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 2, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 2, + 2 + ], + "isreal": true + } + }, + { + "file": "core_typed_over_scalar.sldd", + "id": "typed_over_scalar", + "entry": "pScalar", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_typed_over_struct.sldd", + "id": "typed_over_struct", + "entry": "pStruct", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_typed_real_col.sldd", + "id": "typed_real_col", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 3, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 3, + 1 + ], + "isreal": true + } + }, + { + "file": "core_typed_real_row.sldd", + "id": "typed_real_row", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_typed_scalar.sldd", + "id": "typed_scalar", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 1 + ], + "isreal": true + } + }, + { + "file": "core_typed_string.sldd", + "id": "typed_string", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 1 + ], + "complexity": "real", + "valueClass": "string", + "valueSize": [ + 1, + 1 + ], + "isreal": false + } + }, + { + "file": "core_typed_then_add.sldd", + "id": "typed_then_add", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 4 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 4 + ], + "isreal": true + } + }, + { + "file": "core_typed_then_cut.sldd", + "id": "typed_then_cut", + "entry": "pRow", + "error": "", + "after": { + "dimensions": [ + 1, + 2 + ], + "complexity": "real", + "valueClass": "double", + "valueSize": [ + 1, + 2 + ], + "isreal": true + } + } + ] +} diff --git a/test/parity/matlab/probe_bus_one_element_text.m b/test/parity/matlab/probe_bus_one_element_text.m new file mode 100644 index 00000000..66984a2f --- /dev/null +++ b/test/parity/matlab/probe_bus_one_element_text.m @@ -0,0 +1,122 @@ +% How does MATLAB R2027a write, in a TEXT dictionary, a bus that holds one element, after an +% element is added to it and after one is then removed again; and what does it read back of core's +% saves of the same edits? (B-24, group H, measured 2026-10-19.) +% +% MATLAB writes a one-element Elements_internal as the element itself, `{_id, _object_class, +% _properties}` (../artifacts/text/params.sldd's MyConnBus), and one of more elements as the array, +% `{_array_class, _dimensions, _elements}`. Core rebuilt every bus's Elements_internal in the array +% form, so a save that changed nothing differed from such a file there. +% +% MAKE (BUS_ONE_MAKE=1): a text dictionary holding a Simulink.Bus, a Simulink.ConnectionBus and a +% Simulink.ServiceBus of one element each, saved (bus_one_element_text.sldd); a copy of it with an +% element added to each (self_added.sldd); a copy of that with the added element removed again +% (self_added_removed.sldd), and one with the first element removed instead +% (self_added_removed_first.sldd). Every file is made here, under $BUS_ONE_OUT. +% READ (always): what MATLAB reads of each of those, and of each dictionary +% bus_one_element_text.mjs saved through core from bus_one_element_text.sldd (core_*.sldd), bus by +% bus: its class, its elements' class and names, and any warning, or the error. +% +% Run: env BUS_ONE_OUT=/tmp/busone BUS_ONE_MAKE=1 mw -using Bmain matlab -nodesktop \ +% -batch "run('$PWD/test/parity/matlab/probe_bus_one_element_text.m')" +% then: cp /tmp/busone/bus_one_element_text.sldd /tmp/busone/self_*.sldd test/fixtures/bus_one_element/ +% node test/parity/matlab/bus_one_element_text.mjs /tmp/busone (with dist built) +% env BUS_ONE_OUT=/tmp/busone mw -using Bmain matlab -nodesktop -batch "run(...)" (READ only) +% cp /tmp/busone/bus_one_element_text.truth.json test/parity/matlab/ +% Prints BUS_ONE_ELEMENT_TEXT OK on its last line. Graded by ../../busOneElementText.test.ts. +% Output: bus_one_element_text.truth.json, {version, release, date, rows}: each row a file, an +% entry, for a core save the `fragment` it wrote there (its cases.json; '' for MATLAB's own files), +% and `reads`: {class, elementClass, names, warning} or {error}. + +outdir = getenv('BUS_ONE_OUT'); +if isempty(outdir), outdir = fullfile(tempdir, 'busone'); end +if ~exist(outdir, 'dir'), mkdir(outdir); end +Simulink.data.dictionary.closeAll('-discard'); +fprintf('VERSION %s\n', version); +ENTRIES = {'OneBus', 'OneConnBus', 'OneServiceBus'}; + +if strcmp(getenv('BUS_ONE_MAKE'), '1') + made = fullfile(outdir, 'bus_one_element_text.sldd'); + if exist(made, 'file'), delete(made); end + dd = Simulink.data.dictionary.create(made); dd.FileFormat = 'uncompressed-text'; + sec = getSection(dd, 'Design Data'); + b = Simulink.Bus; e = Simulink.BusElement; e.Name = 'x'; b.Elements = e; + addEntry(sec, 'OneBus', b); + c = Simulink.ConnectionBus; ce = Simulink.ConnectionElement; ce.Name = 'c1'; c.Elements = ce; + addEntry(sec, 'OneConnBus', c); + s = Simulink.ServiceBus; fe = Simulink.FunctionElement; fe.Prototype = 'y = svc(u,v)'; s.Elements = fe; + addEntry(sec, 'OneServiceBus', s); + saveChanges(dd); close(dd); + Simulink.data.dictionary.closeAll('-discard'); + % Each element added is named as core names the one it adds (BaseBusNode / ServiceBusNode.addChildNode). + added = {'a', 'a', 'f1'}; + editCopy(made, fullfile(outdir, 'self_added.sldd'), ENTRIES, @(v, k) appendElement(v, added{k})); + editCopy(fullfile(outdir, 'self_added.sldd'), fullfile(outdir, 'self_added_removed.sldd'), ENTRIES, @(v, k) dropElement(v, numel(v.Elements))); + editCopy(fullfile(outdir, 'self_added.sldd'), fullfile(outdir, 'self_added_removed_first.sldd'), ENTRIES, @(v, k) dropElement(v, 1)); +end + +files = [{'bus_one_element_text.sldd'}, listed(outdir, 'self_*.sldd'), listed(outdir, 'core_*.sldd')]; +% The core half's rows, each save's Elements_internal as written, carried into the truth. +cases = struct('file', {}, 'entry', {}, 'fragment', {}); +if exist(fullfile(outdir, 'cases.json'), 'file'), cases = jsondecode(fileread(fullfile(outdir, 'cases.json'))); end +rows = {}; +for f = 1:numel(files) + for k = 1:numel(ENTRIES) + reads = readBus(fullfile(outdir, files{f}), ENTRIES{k}); + at = find(strcmp({cases.file}, files{f}) & strcmp({cases.entry}, ENTRIES{k})); + fragment = ''; + if ~isempty(at), fragment = cases(at(1)).fragment; end + rows{end+1} = struct('file', files{f}, 'entry', ENTRIES{k}, 'fragment', fragment, 'reads', reads); %#okpRow
'); + stop = strfind(xml(at(1):end), ''); + symbolicTag = regexp(xml(at(1):at(1) + stop(1)), ']*>[^<]*
', 'match', 'once'); + fprintf('MADE %s: %s\n', made, symbolicTag); +end + +% {id, entry, MATLAB statements on p}. +cases = { ... + 'cut_row_to_2', 'pRow', 'p.Value(3) = [];'; ... + 'cut_row_to_1', 'pRow', 'p.Value(3) = []; p.Value(2) = [];'; ... + 'cut_col_to_2', 'pCol', 'p.Value(3) = [];'; ... + 'cut_int16_to_2', 'pInt16Row', 'p.Value(3) = [];'; ... + 'add_row', 'pRow', 'p.Value(end+1) = 0;'; ... + 'add_col', 'pCol', 'p.Value(end+1) = 0;'; ... + 'add_int16', 'pInt16Row', 'p.Value(end+1) = 0;'; ... + 'add_then_cut', 'pRow', 'p.Value(end+1) = 0; p.Value(end) = [];'; ... + 'element_one_real', 'pRow', 'p.Value(1) = 7;'; ... + 'elements_all_real', 'pRow', 'p.Value(1) = 7; p.Value(2) = 8; p.Value(3) = 9;'; ... + 'typed_real_row', 'pRow', 'p.Value = [1 2 3 4];'; ... + 'typed_real_col', 'pRow', 'p.Value = [1; 2; 3];'; ... + 'typed_matrix', 'pRow', 'p.Value = [1 2; 3 4];'; ... + 'typed_scalar', 'pRow', 'p.Value = 5;'; ... + 'typed_complex', 'pRow', 'p.Value = 3+4i;'; ... + 'typed_zero_imag', 'pRow', 'p.Value = 1+0i;'; ... + 'typed_empty', 'pRow', 'p.Value = [];'; ... + 'typed_empty_char', 'pRow', 'p.Value = '''';'; ... + 'typed_empty_string', 'pRow', 'p.Value = "";'; ... + 'typed_empty_on_real', 'pRow', 'p.Value = [1 2]; p.Value = [];'; ... + 'typed_empty_on_struct','pStruct', 'p.Value = [];'; ... + 'typed_empty_then_row','pRow', 'p.Value = []; p.Value = [1 2 3 4];'; ... + 'typed_empty_then_struct','pRow', 'p.Value = []; p.Value = struct(''field'', 0);'; ... + 'typed_struct_over_complex','pRow', 'p.Value = struct(''field'', 0);'; ... + 'typed_char', 'pRow', 'p.Value = ''abc'';'; ... + 'typed_string', 'pRow', 'p.Value = "abc";'; ... + 'typed_logical_row', 'pRow', 'p.Value = [true false];'; ... + 'typed_logical', 'pRow', 'p.Value = true;'; ... + 'typed_over_scalar', 'pScalar', 'p.Value = [1 2 3 4];'; ... + 'typed_over_struct', 'pStruct', 'p.Value = [1 2 3 4];'; ... + 'typed_then_add', 'pRow', 'p.Value = [1 2 3]; p.Value(end+1) = 0;'; ... + 'typed_then_cut', 'pRow', 'p.Value = [1 2 3]; p.Value(3) = [];'; ... + 'struct_add_field', 'pStruct', 'p.Value.field = 0;'; ... + 'symbolic_set', 'pRow', 'p.Dimensions = ''[1,N]'';'; ... + 'symbolic_cut', 'pRow', 'p.Dimensions = ''[1,N]''; p.Value(3) = [];'; ... + 'symbolic_add', 'pRow', 'p.Dimensions = ''[1,N]''; p.Value(end+1) = 0;'; ... + 'symbolic_typed', 'pRow', 'p.Dimensions = ''[1,N]''; p.Value = [1 2 3 4];'; ... + 'symbolic_typed_scalar','pRow', 'p.Dimensions = ''[1,N]''; p.Value = 5;'; ... + 'symbolic_typed_empty','pRow', 'p.Dimensions = ''[1,N]''; p.Value = [];'; ... + 'dimensions_set_other','pRow', 'p.Dimensions = [1 5];'; ... + 'complexity_set_real', 'pRow', 'p.Complexity = ''real'';'}; + +rows = {}; +for k = 1:size(cases, 1) + r = struct('id', cases{k,1}, 'entry', cases{k,2}, 'edit', cases{k,3}, ... + 'before', [], 'ok', false, 'identifier', '', 'message', '', 'after', []); + Simulink.data.dictionary.closeAll('-discard'); + work = fullfile(outdir, 'work.sldd'); + if exist(work, 'file'), delete(work); end + copyfile(fixture, work); + dd = Simulink.data.dictionary.open(work); + p = getValue(getEntry(getSection(dd, 'Design Data'), cases{k,2})); %#ok]*>/, + prop: /
]*>/, + pcell: /
]*>\s* ]*>\s* ]*>/,
+};
+const S = (elements, fields) => ({
+ _array_type: 'Struct',
+ _dimensions: [1, elements.length],
+ _elements: elements,
+ ...(fields ? { _fields: fields } : {}),
+ _mw_element_type: 'MATLABArray',
+});
+// Each case, its forms. `fields` is the struct's field list as core reads it.
+const CASES = [
+ { id: 'A2', fields: ['a'], held: S([{ a: 1 }, {}]), filled: S([{ a: 1 }, { a: [] }]) },
+ { id: 'N2', fields: ['a'], held: S([{ a: 1 }, null]), filled: S([{ a: 1 }, { a: [] }]) },
+ { id: 'A1', fields: ['a'], held: S([{}], ['a']), filled: S([{ a: [] }], ['a']) },
+ { id: 'N1', fields: ['a'], held: S([null], ['a']), filled: S([{ a: [] }], ['a']) },
+ { id: 'F2', fields: ['a', 'b'], held: S([{ a: 1, b: 2 }, { b: 3 }]), filled: S([{ a: 1, b: 2 }, { a: [], b: 3 }]), appended: S([{ a: 1, b: 2 }, { b: 3, a: [] }]) },
+ {
+ id: 'L2',
+ fields: ['a', 'b'],
+ held: S([{ b: 2 }, { a: 1, b: 3 }], ['a', 'b']),
+ filled: S([{ a: [], b: 2 }, { a: 1, b: 3 }], ['a', 'b']),
+ appended: S([{ b: 2, a: [] }, { a: 1, b: 3 }], ['a', 'b']),
+ },
+ { id: 'X2', fields: ['a'], held: S([{ a: 1 }, { b: 2 }]), filled: S([{ a: 1 }, { a: [], b: 2 }]), appended: S([{ a: 1 }, { b: 2, a: [] }]) },
+ // Every field held, in another order than the struct's `_fields`, as MATLAB's own text
+ // dictionaries hold their structs (the keys sorted, `_fields` in the struct's order).
+ { id: 'R1', fields: ['b', 'a'], held: S([{ a: 1, b: 2 }], ['b', 'a']), filled: S([{ b: 2, a: 1 }], ['b', 'a']) },
+ { id: 'R2', fields: ['b', 'a'], held: S([{ a: 1, b: 2 }, { a: 3, b: 4 }], ['b', 'a']), filled: S([{ b: 2, a: 1 }, { b: 4, a: 3 }], ['b', 'a']) },
+ // The controls: every element holds every field, as MATLAB writes a struct; C2 holds a field
+ // as [], which a Parameter's Value cannot (PrmInvalidStruct, parameter_value_truth.json).
+ { id: 'C2', fields: ['a'], held: S([{ a: 1 }, { a: [] }]) },
+ { id: 'C1', fields: ['a'], held: S([{ a: 1 }]) },
+ { id: 'C3', fields: ['a'], held: S([{ a: 1 }, { a: 2 }]) },
+];
+const FORMS = ['held', 'filled', 'appended', 'text'];
+const LETTER = { held: 'H', filled: 'F', appended: 'P', text: 'T' };
+const entriesOf = (text) => JSON.parse(text).__MW_TEXT_PARTS__['__MW_TEXT_PART__/data/chunk0'].__MW_TEXT_content.entries;
+// Where the struct is in an entry's value as a text dictionary holds it.
+const IN_TEXT = {
+ param: (v) => v._elements[0]._properties.Value,
+ prop: (v) => v._elements[0]._properties.Scalar,
+ pcell: (v) => v._elements[0]._properties.Scalar._elements[0],
+ pfield: (v) => v._elements[0]._properties.Scalar._elements[0].f,
+};
+
+/** The value of an entry that holds `v` at `place`; the Parameter and the Widget are MATLAB's own, copied out of the file. */
+function valueAt(bin, place, v) {
+ const copied = (section, name) => JSON.parse(JSON.stringify(bin.getSection(section).children.find((e) => e.name === name).serialize().value));
+ if (place === 'param') {
+ const p = copied('design', 'p_D022');
+ p._elements[0]._properties.Value = v;
+ p._elements[0]._properties.Dimensions = v._dimensions;
+ return p;
+ }
+ const w = copied('other', 'w_D022');
+ w._elements[0]._properties.Scalar =
+ place === 'prop'
+ ? v
+ : place === 'pcell'
+ ? { _array_type: 'Cell', _dimensions: [1, 1], _elements: [v], _mw_element_type: 'MATLABArray' }
+ : S([{ f: v }], ['f']);
+ return w;
+}
+
+/** Where the element of `xml` that opens at `from` ends its opening tag, and where its closing tag starts and ends. */
+function extentAt(xml, from) {
+ const tag = /<(\/?)[^>]*?(\/?)>/g;
+ tag.lastIndex = from;
+ let depth = 0;
+ let open = -1;
+ for (let m = tag.exec(xml); m; m = tag.exec(xml)) {
+ depth += m[1] ? -1 : m[2] ? 0 : 1;
+ if (open < 0) {
+ open = tag.lastIndex;
+ }
+ if (depth === 0) {
+ return { open, close: m.index, end: tag.lastIndex };
+ }
+ }
+ return null;
+}
+
+/** The element of `xml` that opens at `from`, through its close, with no whitespace between tags. */
+function elementAt(xml, from) {
+ const at = extentAt(xml, from);
+ return at ? xml.slice(from, at.end).replace(/>\s+<') : '?';
+}
+
+/** The ` ' + (Number.isInteger(x) ? x + '.0' : x) + ' 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 1.0 2.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 2.0 1.0 3.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 1.0 2.0 3.0 4.0 1.0 2.0 3.0 4.0 1.0 2.0 3.0 4.0 1.0 2.0 3.0 4.0 2.0 1.0 4.0 3.0 2.0 1.0 4.0 3.0 2.0 1.0 4.0 3.0 2.0 1.0 4.0 3.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 1.0 2.0 1.0 2.0 1.0 2.0 1.0 2.0 ]*>/,
+ prop: / ]*>/,
+ pcell: / ]*>\s* ]*>\s* ]*>/,
+};
+/** The element of `xml` at `place` in entry `name`, through its close, with no whitespace between tags. */
+function spellingIn(xml: string, name: string, place: Place): string {
+ const from = xml.indexOf('>' + name + '<');
+ const m = OPENS[place].exec(xml.slice(from));
+ if (from < 0 || !m) {
+ return '?';
+ }
+ const start = from + m.index + m[0].lastIndexOf('<');
+ const tag = /<(\/?)[^>]*?(\/?)>/g;
+ tag.lastIndex = start;
+ let depth = 0;
+ for (let t = tag.exec(xml); t; t = tag.exec(xml)) {
+ depth += t[1] ? -1 : t[2] ? 0 : 1;
+ if (depth === 0) {
+ return xml.slice(start, tag.lastIndex).replace(/>\s+<');
+ }
+ }
+ return '?';
+}
+
+/** The value of an entry holding `v` at `place`; the Parameter and the Widget are MATLAB's own, copied out of the file. */
+function valueAt(bin: any, place: Place, v: ReturnType `, a field no struct of MATLAB's can have, or
+ // failed the save; it is written as no field, as before B-17 (which wrote only the fields an
+ // element holds), and the struct as one whose element holds `a`, which MATLAB reads as such (C1).
+ it.each(PLACES)('at %s, a member of `_fields` that is not a name is written as no field, and the save does not fail', (place) => {
+ for (const [id, fields] of [
+ ['NS', ['a', 5]],
+ ['NL', [null, 'a']],
+ ] as [string, unknown[]][]) {
+ const saved = pastedAndSaved(id, place, S([{ a: 1 }], fields as string[]));
+ expect(saved.spelling, id).toBe(row('C1', 'held', place).spelling);
+ expect(matlabRead(place, saved.spelling), id).toEqual(row('C1', 'text', place).reads);
+ expect(saved.reopened.some((r) => r.endsWith('/a = 1')), id).toBe(true);
+ }
+ });
+});