diff --git a/CHANGELOG.md b/CHANGELOG.md index b0eb240..5eec44b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -126,7 +126,7 @@ The filter popup a column header opens now shows which comparison it is about to ## [1.36.0] — 2026-10-02 -A file too large to show whole now lists every entry it has and loads a row's contents when that row is opened, where before it hung with no table and no message. Reported against an 8 MB `allMidi.mat` — a cell array of MIDI songs — which "keeps spinning" and never opens: it flattens to 2,016,325 nodes over 7 levels, a row was built for every one of them, and the resulting `setRows` payload was 704 MiB of JSON. V8 cannot produce a string that long (the limit is 536,870,888), so `JSON.stringify` threw inside VS Code's own `postMessage`, which is `async` and serializes the message itself — the throw therefore arrived as a rejected promise that the `try`/`catch` around the post could not see, nothing posted `setRows`, nothing posted an error, and the table's spinner ended on nothing else. Three layers, because there were three faults. The post is now awaited and a rejection becomes the error banner, so an undeliverable payload can never again be silent. A hard cap of 100,000 rows sits in front of the read-only row payload — not a tuning knob but what keeps the payload serializable at all, sized for the worst row shape rather than the measured 366 B, and taken as a PREFIX because every builder emits a parent before its children and a prefix is the only cut in which every surviving row's parent is also present; a sample would leave orphans the webview cannot place. The editable `.sldd` views get the guard but not the cap, since a structural edit computed against a truncated row set is worse than an honest banner. And then the cap alone, which is where this version's real work is: a prefix of a 2-million-row pre-order traversal is the first cell's descendants and nothing else, so ten of the eleven top-level variables were not on screen and no gesture could reach them — the file opened and was still unusable. The rule now is breadth over depth, for every data source and every entry type: stop expanding large entries, but list all entries, because a name present matters more than a value shown. The payload is planned by whole LEVELS of the tree, level by level while the next one fits, and the first level that does not is omitted entirely rather than half-delivered — which is what makes a deferred row mean exactly "has children, none of them here" and makes one fetch the whole answer for that row. The reported file now delivers 238 rows, levels 1 through 4 complete, and paints them in 7 ms. One plan spans every section rather than one plan per section, and that is the half the rule actually turns on: a per-section plan would also deliver whole levels and also defer honestly while spending the budget on the depth of section 1 and leaving section 9's entries unnamed, so a dictionary's References heading would read as empty on a file whose Design Data is deep. Opening a deferred row fetches it: the twisty is driven by the deferred mark as well as by delivered children — through one predicate read by the click, the Space key and `aria-expanded` alike, so a tree a mouse can open is one a keyboard can open — the click asks the host, the host answers from the same planner that built the payload, and the rows are merged under their parent instead of being routed through a fresh payload, which would re-derive the columns and close an open grid view. Pairing the two halves on one choice of planner is deliberate: a fetch answered by a different rule than the payload was built with is a row that opens onto the wrong thing, with no error anywhere. The answer carries a count of children that got no row at all, for the one loss a fetch can still inflict — a node with more direct children than a whole delivery holds, since a cell's children are built uncapped — and the table says that number in the banner strip, because the file's own banner was composed before the user opened anything and cannot. Entry-scoped repaints, which is how an edit writes back, take the same budget: a 1000x1000 double is 1,000,001 rows from a single name, enough to be the whole problem by itself. That banner is also now in the strip rather than in the document's normal flow ahead of a full-bleed `position:absolute` table, which had been painting over it — an error message that was in the DOM and on screen nowhere, leaving the panel reading "No data". Qualified against the reported file in a real browser over the shipped bundle: all eleven variables named, the banner legible in both themes, descending to a frontier row costing no round trip until the frontier itself, and opening a 1x19252 struct array four levels down posting one request, answering in 90 ms and landing its rows under the row that was clicked. Unchanged by any of this: parsing that file still costs about 2.3 GiB of extension-host heap, so a file several times larger exhausts the host before a single row is built — that is the MAT reader in `data-explorer-core`, and no amount of planning on this side recovers it. +A file too large to show whole now lists every entry it has and loads a row's contents when that row is opened, where before it hung with no table and no message. Reported against an 8 MB MAT-file — a cell array of MIDI songs — which "keeps spinning" and never opens: it flattens to 2,016,325 nodes over 7 levels, a row was built for every one of them, and the resulting `setRows` payload was 704 MiB of JSON. V8 cannot produce a string that long (the limit is 536,870,888), so `JSON.stringify` threw inside VS Code's own `postMessage`, which is `async` and serializes the message itself — the throw therefore arrived as a rejected promise that the `try`/`catch` around the post could not see, nothing posted `setRows`, nothing posted an error, and the table's spinner ended on nothing else. Three layers, because there were three faults. The post is now awaited and a rejection becomes the error banner, so an undeliverable payload can never again be silent. A hard cap of 100,000 rows sits in front of the read-only row payload — not a tuning knob but what keeps the payload serializable at all, sized for the worst row shape rather than the measured 366 B, and taken as a PREFIX because every builder emits a parent before its children and a prefix is the only cut in which every surviving row's parent is also present; a sample would leave orphans the webview cannot place. The editable `.sldd` views get the guard but not the cap, since a structural edit computed against a truncated row set is worse than an honest banner. And then the cap alone, which is where this version's real work is: a prefix of a 2-million-row pre-order traversal is the first cell's descendants and nothing else, so ten of the eleven top-level variables were not on screen and no gesture could reach them — the file opened and was still unusable. The rule now is breadth over depth, for every data source and every entry type: stop expanding large entries, but list all entries, because a name present matters more than a value shown. The payload is planned by whole LEVELS of the tree, level by level while the next one fits, and the first level that does not is omitted entirely rather than half-delivered — which is what makes a deferred row mean exactly "has children, none of them here" and makes one fetch the whole answer for that row. The reported file now delivers 238 rows, levels 1 through 4 complete, and paints them in 7 ms. One plan spans every section rather than one plan per section, and that is the half the rule actually turns on: a per-section plan would also deliver whole levels and also defer honestly while spending the budget on the depth of section 1 and leaving section 9's entries unnamed, so a dictionary's References heading would read as empty on a file whose Design Data is deep. Opening a deferred row fetches it: the twisty is driven by the deferred mark as well as by delivered children — through one predicate read by the click, the Space key and `aria-expanded` alike, so a tree a mouse can open is one a keyboard can open — the click asks the host, the host answers from the same planner that built the payload, and the rows are merged under their parent instead of being routed through a fresh payload, which would re-derive the columns and close an open grid view. Pairing the two halves on one choice of planner is deliberate: a fetch answered by a different rule than the payload was built with is a row that opens onto the wrong thing, with no error anywhere. The answer carries a count of children that got no row at all, for the one loss a fetch can still inflict — a node with more direct children than a whole delivery holds, since a cell's children are built uncapped — and the table says that number in the banner strip, because the file's own banner was composed before the user opened anything and cannot. Entry-scoped repaints, which is how an edit writes back, take the same budget: a 1000x1000 double is 1,000,001 rows from a single name, enough to be the whole problem by itself. That banner is also now in the strip rather than in the document's normal flow ahead of a full-bleed `position:absolute` table, which had been painting over it — an error message that was in the DOM and on screen nowhere, leaving the panel reading "No data". Qualified against the reported file in a real browser over the shipped bundle: all eleven variables named, the banner legible in both themes, descending to a frontier row costing no round trip until the frontier itself, and opening a 1x19252 struct array four levels down posting one request, answering in 90 ms and landing its rows under the row that was clicked. Unchanged by any of this: parsing that file still costs about 2.3 GiB of extension-host heap, so a file several times larger exhausts the host before a single row is built — that is the MAT reader in `data-explorer-core`, and no amount of planning on this side recovers it. ## [1.35.0] — 2026-10-02 diff --git a/src/host/nameExtract.ts b/src/host/nameExtract.ts index a3b68c4..95aaa13 100644 --- a/src/host/nameExtract.ts +++ b/src/host/nameExtract.ts @@ -96,11 +96,11 @@ export function namesFromMat(names: readonly string[], sourceUri: string): NameR // graph concern, not a name one). // // One record per BLOCK, keyed by core's `blockKey` — the SID — and never by the name. -// A name is unique only inside its own system: f14.slx holds four blocks named `Gain` -// in four subsystems, which deduped by name gave ONE search hit that could reveal only -// whichever row came first, and the Constant whose label the file leaves blank had no -// hit at all (an empty name was dropped). Both are now searchable, told apart by their -// path, and revealed by the key the row publishes as `_blockKey`. +// A name is unique only inside its own system: a shipped Simulink demo model has four +// `Gain` blocks in four subsystems, which deduped by name gave ONE search hit that could +// reveal only whichever row came first, and the Constant whose label the file leaves +// blank had no hit at all (an empty name was dropped). Both are now searchable, told +// apart by their path, and revealed by the key the row publishes as `_blockKey`. // // A block with neither a name nor a SID has no key, and is still dropped: nothing could // be searched for, and nothing could be selected if it were found. diff --git a/src/host/usageCells.ts b/src/host/usageCells.ts index 8d60128..b1f9217 100644 --- a/src/host/usageCells.ts +++ b/src/host/usageCells.ts @@ -47,8 +47,8 @@ export interface RawSource { // and a target that navigates back to it. // // The model is part of the payload and not an embellishment: two models can hold -// blocks of the same name — `DragCalc(MainVehicle)` and `DragCalc(SubChassis)` both -// use SharedTypes' DragCoeff — so a bare block name is not a usable answer for a +// blocks of the same name — `DragCalc(Sedan)` and `DragCalc(Truck)` both +// use a shared dictionary's DragCoeff — so a bare block name is not a usable answer for a // variable that models share, which is every variable a dictionary exists to hold. // // `modelUri` rides along for the webview to GROUP by, and is not the same fact as @@ -220,8 +220,8 @@ function toParamLink(origin: ParamOrigin): ParamLink { * from core's SESSION — the models whose editor tab happened to be resolved in this * window — in a shape that names the block and not the model. So whether a dictionary * entry read `AFRConst, AFRCheck` or - * `AFR(EngineCtrl), AFRMonitor(EngineCtrl), MixTarget(EngineCtrl), AFRConst(FuelInjector), - * AFRCheck(FuelInjector)` depended on which models the user had opened — and merely + * `AFR(EngineLoop), AFRMonitor(EngineLoop), MixTarget(EngineLoop), AFRConst(FuelPath), + * AFRCheck(FuelPath)` depended on which models the user had opened — and merely * CLICKING a Usage link registered the model it pointed at, degrading the very cell it * was clicked from for as long as that model stayed registered. Two engines answering one * column is the defect; this makes the workspace graph the one that does. @@ -254,8 +254,8 @@ export function annotateVariableRows( /** * Drop the `(model)` qualifier from a usage inside the file the row itself belongs to. * - * The qualifier is there because a variable can be SHARED: `DragCalc(MainVehicle)` and - * `DragCalc(SubChassis)` are two different blocks and the model is the only thing that + * The qualifier is there because a variable can be SHARED: `DragCalc(Sedan)` and + * `DragCalc(Truck)` are two different blocks and the model is the only thing that * separates them. A MODEL-WORKSPACE variable is shared with nobody — every block that can * read it is in the model whose workspace holds it — so in a model view the qualifier * repeated the name of the open file on every link and disambiguated nothing. @@ -291,11 +291,12 @@ function withoutOwnModel(link: BlockLink, sourceUri: string): BlockLink { * `ModelBlockNode.toRow` publishes for exactly this purpose — and NEVER by the Name * label. A label is unique only inside one system, so two `Gain` blocks in different * subsystems would take each other's parameters; and a block Simulink recorded with no - * name at all (f14.slx's SID 65, whose `Name` attribute is a lone line break) has the - * synthetic label ``, which matches no block in core's index and left the cell - * empty. Unlike a variable row, a block row IS emptied when the graph has nothing for - * it: the row came from this same model's parse, so the graph has seen the block, and - * "no parameters resolved" is an answer about it rather than a gap in what was read. + * name at all (SID 65 of a shipped Simulink demo model, whose `Name` attribute is a + * lone line break) has the synthetic label ``, which matches no block in core's + * index and left the cell empty. Unlike a variable row, a block row IS emptied when the + * graph has nothing for it: the row came from this same model's parse, so the graph has + * seen the block, and "no parameters resolved" is an answer about it rather than a gap in + * what was read. */ export function annotateModelViewRows( modelUri: string, diff --git a/src/webview/components/dex-tree-table.ts b/src/webview/components/dex-tree-table.ts index ae7aa0f..6812888 100644 --- a/src/webview/components/dex-tree-table.ts +++ b/src/webview/components/dex-tree-table.ts @@ -265,9 +265,9 @@ interface ParamLinkCell { } // A Usage cell's block links, one group per model: `AFR, AFRMonitor, -// MixTarget(EngineCtrl); AFRConst, AFRCheck(FuelInjector)`. A dictionary variable is +// MixTarget(EngineLoop); AFRConst, AFRCheck(FuelPath)`. A dictionary variable is // used by as many blocks as a model has, so naming the model once per block spent most -// of a narrow column repeating `(EngineCtrl)` and buried the block names the user is +// of a narrow column repeating `(EngineLoop)` and buried the block names the user is // actually reading. // // Grouped on `modelUri`, NOT on the displayed `modelName`: the label is a stripped diff --git a/src/webview/table-main.ts b/src/webview/table-main.ts index afae10f..d7a0fce 100644 --- a/src/webview/table-main.ts +++ b/src/webview/table-main.ts @@ -213,7 +213,7 @@ function applyPendingNameSelection(): void { // written before SIDs existed) is a NAME and matches the Name label; a block // target is core's block KEY — the SID — which is not printed anywhere, so it is // matched against the `_blockKey` the row publishes. Without that second pass a - // `blocks:65@f14.slx` click would open the model and select nothing, since the + // `blocks:65@.slx` click would open the model and select nothing, since the // row it means reads ``. Name first, so every pre-SID target keeps its // existing answer when a same-spelled key also exists. // diff --git a/test/banners.test.ts b/test/banners.test.ts index e79521f..4bbe44a 100644 --- a/test/banners.test.ts +++ b/test/banners.test.ts @@ -124,7 +124,7 @@ describe('renderBanners', () => { // OUTSIDE the strip — normal flow, ahead of a table pinned at inset:0 — so the // table painted over it, measured in a real browser as a cleared spinner above // the words "No data". - renderError(table, 'Failed to show allMidi.mat: Invalid string length', now); + renderError(table, 'Failed to show songs.mat: Invalid string length', now); expect(el('dex-error').style.display).toBe('block'); expect(el('dex-error').textContent).toContain('Invalid string length'); expect(table.style.top).not.toBe(''); @@ -151,7 +151,7 @@ describe('renderBanners', () => { // the offset from what is SHOWING rather than from its own argument. Before // that, an `updateEntryRows` with nothing to say would have dropped the table // back to the top of the panel and slid it under the error still above it. - renderError(table, 'Failed to show allMidi.mat: Invalid string length', now); + renderError(table, 'Failed to show songs.mat: Invalid string length', now); renderBanners(table, {}, now); expect(el('dex-error').style.display).toBe('block'); expect(table.style.top).not.toBe(''); diff --git a/test/blockSidIdentity.test.ts b/test/blockSidIdentity.test.ts index 112e232..729af2a 100644 --- a/test/blockSidIdentity.test.ts +++ b/test/blockSidIdentity.test.ts @@ -4,14 +4,14 @@ // A block is identified by its SID, and its NAME is only a label — over real bytes, from // the parse to the text in the cell. // -// Found in ~/delite/f/f14.slx, in two shapes at once. A block name is unique only inside -// its own system, and that model has four blocks named `Gain` in four different -// subsystems; keyed by name they collapsed into ONE row whose Usage read +// Found in a shipped Simulink demo model, in two shapes at once. A block name is unique +// only inside its own system, and that model has four blocks named `Gain` in four +// different subsystems; keyed by name they collapsed into ONE row whose Usage read // `Gain=Mq, Gain=Zw, Gain=Kf, Gain=Zw` — four blocks' parameters in one cell — and all -// four shared the row id `f14.slx/blocks/Gain`, so the Property Inspector, a selection, +// four shared the row id `.slx/blocks/Gain`, so the Property Inspector, a selection, // and a Usage link could not tell them apart. And the model holds a Constant whose `Name` // attribute Simulink wrote as a lone line break (`Name=" "` SID 65): it normalizes to -// the empty string, so the row showed a BLANK Name, took the id `f14.slx/blocks/`, and its +// the empty string, so the row showed a BLANK Name, took the id `.slx/blocks/`, and its // Usage link — whose text is the block name — was an empty anchor with nothing to click. // // Core v1.8.0 splits the two facts (`blockKey` = the SID, `blockLabel` = the name or @@ -90,7 +90,7 @@ describe('the workspace graph over same-named and nameless blocks', () => { }); it('gives the nameless block a printable label and a followable link', () => { - // The f14 block the user asked about, over the same `Uo` it reads there. The cell text + // That model's block the user asked about, over the same `Uo` it reads there. The cell text // was empty — an anchor with no characters in it cannot be clicked — and the target // named no block at all. expect(graph().blocksUsing(DICT, 'Uo')).toEqual([ diff --git a/test/filterEntries.test.ts b/test/filterEntries.test.ts index a2d2140..4a2c046 100644 --- a/test/filterEntries.test.ts +++ b/test/filterEntries.test.ts @@ -69,15 +69,15 @@ describe('filterEntries', () => { }); // With one record per BLOCK rather than per name, a model answers `Gain` with a hit for -// every `Gain` it holds — f14.slx has four. The label cannot separate them, so the path -// is searchable too: "the Gain in Controller" has to be expressible, and a SUBSYSTEM's -// own name is otherwise absent from this index entirely (a subsystem has no parameter -// reference of its own to be recorded by). +// every `Gain` it holds — a shipped Simulink demo model has four. The label cannot +// separate them, so the path is searchable too: "the Gain in Controller" has to be +// expressible, and a SUBSYSTEM's own name is otherwise absent from this index entirely +// (a subsystem has no parameter reference of its own to be recorded by). describe('filterEntries over block records, which repeat their labels', () => { const block = (name: string, blockPath: string): NameRecord => ({ name, - sourceUri: 'file:///w/f14.slx', - sourceLabel: 'f14.slx', + sourceUri: 'file:///w/aircraft.slx', + sourceLabel: 'aircraft.slx', kind: 'block', selectName: blockPath, blockPath, diff --git a/test/fixtures/make-fixtures.mjs b/test/fixtures/make-fixtures.mjs index e2f910d..7cc2666 100644 --- a/test/fixtures/make-fixtures.mjs +++ b/test/fixtures/make-fixtures.mjs @@ -430,18 +430,18 @@ writeFileSync( // --- sid_blocks.slx: a block's identity is its SID, not its name --------------- // -// The fourth model of the usage set, shaped after ~/delite/f/f14.slx, which is where -// both halves of this were found: +// The fourth model of the usage set, shaped after a shipped Simulink demo model, which +// is where both halves of this were found: // // two blocks named `Gain`, in DIFFERENT systems (SIDs 15 and 24). A block name is -// unique only within its own system, and f14 has four `Gain` blocks; keyed by name +// unique only within its own system, and that model has four `Gain` blocks; keyed by name // they collapsed into ONE row reading `Gain=Mq, Gain=Zw, Gain=Kf, Gain=Zw` and all -// four shared the id `f14.slx/blocks/Gain`. Here they read different variables +// four shared the id `.slx/blocks/Gain`. Here they read different variables // (`Kp` and `Ki`) precisely so a merge is visible: each row must show its own. // // a block with NO name (SID 65). Simulink wrote its `Name` attribute as a lone line // break, which normalizes to the empty string, so it displayed as a blank Name, an -// id of `f14.slx/blocks/`, and an EMPTY Usage link the user could not click. It +// id of `.slx/blocks/`, and an EMPTY Usage link the user could not click. It // reads `Uo` — the same parameter, and the same block, the user asked about. // // The nameless block is written as ` ` rather than `Name=""`: that is the byte @@ -580,7 +580,7 @@ writeFileSync( // interface's definition), so renaming a function has to patch that member too. // // The same shape as core's functionElementRename.test.ts, which is the corpus's one binary -// dictionary with a service interface (example/dd1.sldd), trimmed: `ServiceInterface` holds +// dictionary with a service interface, trimmed: `ServiceInterface` holds // `f: y = f(u)` and `g: [a,b] = g(g)`. It carries two traps, because the host patches text: // a DATA interface that is also named `f`, and `g`'s first argument, which is named `g`. A // rename of either function that took either of those would corrupt something else. diff --git a/test/nameExtract.test.ts b/test/nameExtract.test.ts index 53689ee..bc5e41d 100644 --- a/test/nameExtract.test.ts +++ b/test/nameExtract.test.ts @@ -101,14 +101,14 @@ describe('namesFromSlx', () => { }); // A block name is unique inside its own system and nowhere else, so the name is the - // wrong thing to key a block record on — and it was: f14.slx's four blocks named - // `Gain` deduped to a single hit that could reveal only whichever one came first, - // and its nameless Constant was dropped for having no name to search for. Both are - // core's rules (blockKey / blockLabel / joinBlockPath), applied here. + // wrong thing to key a block record on — and it was: a shipped Simulink demo model's + // four blocks named `Gain` deduped to a single hit that could reveal only whichever one + // came first, and its nameless Constant was dropped for having no name to search for. + // Both are core's rules (blockKey / blockLabel / joinBlockPath), applied here. describe('one record per BLOCK, not per name', () => { - const uri = 'file:///w/f14.slx'; + const uri = 'file:///w/aircraft.slx'; // Two `Gain` blocks in two subsystems and one whose label the file leaves blank — - // f14.slx's shape, which is where this came from. + // that demo model's shape, which is where this came from. const parsed = { blockParamUsages: [ { blockName: 'Gain', sid: '15', systemPath: '' }, diff --git a/test/rowCap.test.ts b/test/rowCap.test.ts index b2a87ef..10f3727 100644 --- a/test/rowCap.test.ts +++ b/test/rowCap.test.ts @@ -144,11 +144,11 @@ describe('postOrReport', () => { .fn() .mockRejectedValueOnce(new RangeError('Invalid string length')) .mockResolvedValue(true); - await expect(postOrReport({ postMessage }, { type: 'setRows' }, 'allMidi.mat')).resolves.toBe(false); + await expect(postOrReport({ postMessage }, { type: 'setRows' }, 'songs.mat')).resolves.toBe(false); expect(postMessage).toHaveBeenCalledTimes(2); expect(postMessage.mock.calls[1][0]).toEqual({ type: 'error', - message: 'Failed to show allMidi.mat: Invalid string length', + message: 'Failed to show songs.mat: Invalid string length', }); }); diff --git a/test/treeTableCells.test.ts b/test/treeTableCells.test.ts index 4dede87..0bffc8a 100644 --- a/test/treeTableCells.test.ts +++ b/test/treeTableCells.test.ts @@ -454,13 +454,13 @@ describe('links navigate rather than following an href', () => { // four `Controller/Inner/Gain`s would fill a column sized for names. describe('a blockLink carries where its block is, as a tooltip', () => { it('titles each link with its own path, and adds nothing to the text', async () => { - const M = 'file:///w/f14.slx'; + const M = 'file:///w/aircraft.slx'; const table = await mount([ makeRow('u', 'u', { UsedBy: { blockLinks: [ - { blockName: 'Gain', blockPath: 'Controller/Gain', modelName: 'f14', modelUri: M, linkTarget: 'b:15' }, - { blockName: 'Gain', blockPath: 'Sensors/Gain', modelName: 'f14', modelUri: M, linkTarget: 'b:24' }, + { blockName: 'Gain', blockPath: 'Controller/Gain', modelName: 'aircraft', modelUri: M, linkTarget: 'b:15' }, + { blockName: 'Gain', blockPath: 'Sensors/Gain', modelName: 'aircraft', modelUri: M, linkTarget: 'b:24' }, ], } as any, }), @@ -472,8 +472,8 @@ describe('links navigate rather than following an href', () => { ]); // The visible cell is unchanged by the paths — that is the point of a tooltip — so // sorting and copying read what they always did. - expect(td.textContent!.trim()).toBe('Gain, Gain(f14)'); - expect((table as any)._getCellText(table.rows[0], 'UsedBy')).toBe('Gain, Gain(f14)'); + expect(td.textContent!.trim()).toBe('Gain, Gain(aircraft)'); + expect((table as any)._getCellText(table.rows[0], 'UsedBy')).toBe('Gain, Gain(aircraft)'); table.remove(); }); @@ -482,7 +482,7 @@ describe('links navigate rather than following an href', () => { // path. An empty `title=""` is a tooltip that flashes nothing on hover. const table = await mount([ makeRow('u', 'u', { - UsedBy: { blockLinks: [{ blockName: 'Gain', modelName: 'f14', modelUri: 'file:///w/f14.slx', linkTarget: 'b:1' }] } as any, + UsedBy: { blockLinks: [{ blockName: 'Gain', modelName: 'aircraft', modelUri: 'file:///w/aircraft.slx', linkTarget: 'b:1' }] } as any, }), ]); const a = cell(table, 'u', 'UsedBy').querySelector('a.value-link')!; @@ -493,7 +493,7 @@ describe('links navigate rather than following an href', () => { }); // A dictionary variable is used by every block that reads it, so the one-qualifier- - // per-block form spent a narrow column repeating `(EngineCtrl)` and pushed the block + // per-block form spent a narrow column repeating `(EngineLoop)` and pushed the block // names — the part being read — out of sight. describe('blockLinks aggregate by model', () => { const linksOf = (td: HTMLElement): string[] => @@ -502,25 +502,25 @@ describe('links navigate rather than following an href', () => { [...td.querySelectorAll('.param-source')].map((s) => s.textContent!); it('names each model once, after its last block', async () => { - const E = 'file:///w/EngineCtrl.slx'; - const F = 'file:///w/FuelInjector.slx'; + const E = 'file:///w/EngineLoop.slx'; + const F = 'file:///w/FuelPath.slx'; const table = await mount([ makeRow('u', 'u', { UsedBy: { blockLinks: [ - { blockName: 'AFR', modelName: 'EngineCtrl', modelUri: E, linkTarget: 'b:1' }, - { blockName: 'AFRMonitor', modelName: 'EngineCtrl', modelUri: E, linkTarget: 'b:2' }, - { blockName: 'MixTarget', modelName: 'EngineCtrl', modelUri: E, linkTarget: 'b:3' }, - { blockName: 'AFRConst', modelName: 'FuelInjector', modelUri: F, linkTarget: 'b:4' }, - { blockName: 'AFRCheck', modelName: 'FuelInjector', modelUri: F, linkTarget: 'b:5' }, + { blockName: 'AFR', modelName: 'EngineLoop', modelUri: E, linkTarget: 'b:1' }, + { blockName: 'AFRMonitor', modelName: 'EngineLoop', modelUri: E, linkTarget: 'b:2' }, + { blockName: 'MixTarget', modelName: 'EngineLoop', modelUri: E, linkTarget: 'b:3' }, + { blockName: 'AFRConst', modelName: 'FuelPath', modelUri: F, linkTarget: 'b:4' }, + { blockName: 'AFRCheck', modelName: 'FuelPath', modelUri: F, linkTarget: 'b:5' }, ], } as any, }), ]); const td = cell(table, 'u', 'UsedBy'); - expect(sourcesOf(td)).toEqual(['(EngineCtrl)', '(FuelInjector)']); + expect(sourcesOf(td)).toEqual(['(EngineLoop)', '(FuelPath)']); expect(linksOf(td)).toEqual(['AFR', 'AFRMonitor', 'MixTarget', 'AFRConst', 'AFRCheck']); - expect(td.textContent!.trim()).toBe('AFR, AFRMonitor, MixTarget(EngineCtrl); AFRConst, AFRCheck(FuelInjector)'); + expect(td.textContent!.trim()).toBe('AFR, AFRMonitor, MixTarget(EngineLoop); AFRConst, AFRCheck(FuelPath)'); // Every block keeps its own link — grouping is presentational and must not cost // the user a navigable target. expect(linksOf(td).length).toBe(5); @@ -587,20 +587,20 @@ describe('links navigate rather than following an href', () => { }); it('gives sorting and copying the same grouped text the cell shows', async () => { - const E = 'file:///w/EngineCtrl.slx'; - const F = 'file:///w/FuelInjector.slx'; + const E = 'file:///w/EngineLoop.slx'; + const F = 'file:///w/FuelPath.slx'; const table = await mount([ makeRow('u', 'u', { UsedBy: { blockLinks: [ - { blockName: 'AFR', modelName: 'EngineCtrl', modelUri: E, linkTarget: 'b:1' }, - { blockName: 'AFRConst', modelName: 'FuelInjector', modelUri: F, linkTarget: 'b:2' }, - { blockName: 'AFRMonitor', modelName: 'EngineCtrl', modelUri: E, linkTarget: 'b:3' }, + { blockName: 'AFR', modelName: 'EngineLoop', modelUri: E, linkTarget: 'b:1' }, + { blockName: 'AFRConst', modelName: 'FuelPath', modelUri: F, linkTarget: 'b:2' }, + { blockName: 'AFRMonitor', modelName: 'EngineLoop', modelUri: E, linkTarget: 'b:3' }, ], } as any, }), ]); - const grouped = 'AFR, AFRMonitor(EngineCtrl); AFRConst(FuelInjector)'; + const grouped = 'AFR, AFRMonitor(EngineLoop); AFRConst(FuelPath)'; expect((table as any)._getCellText(table.rows[0], 'UsedBy')).toBe(grouped); expect(cell(table, 'u', 'UsedBy').textContent!.trim()).toBe(grouped); table.remove(); diff --git a/test/usageCells.test.ts b/test/usageCells.test.ts index 20a3a95..aa324f6 100644 --- a/test/usageCells.test.ts +++ b/test/usageCells.test.ts @@ -224,9 +224,9 @@ describe('paramLinks — the cell a block row shows', () => { // own row shows `g1=g1_param`. // // Which is exactly MATLAB's answer for the model this came from. `Simulink.findVars` on -// it reports `g1` in the mask workspace `mWhereUsedRefVars/MulAdd` used by -// `mWhereUsedRefVars/MulAdd/Gain`, and `g1_param` in the model workspace used by -// `mWhereUsedRefVars/MulAdd` — the inner Gain is NOT a user of `g1_param`. The two-hop +// it reports `g1` in the mask workspace `/MulAdd` used by +// `/MulAdd/Gain`, and `g1_param` in the model workspace used by +// `/MulAdd` — the inner Gain is NOT a user of `g1_param`. The two-hop // chain is the fact; the tests below are that both hops land in a cell. describe('paramLinks — a mask parameter, the origin that is not a file', () => { // A masked subsystem: `MulAdd` (SID 158) declares `g1 = g1_param` and holds an inner @@ -374,8 +374,8 @@ describe('annotateVariableRows names the model in every usage', () => { }); it('keeps same-named blocks in different models apart', () => { - // The case a bare block name cannot express: SharedTypes' DragCoeff is used by a - // `DragCalc` in MainVehicle AND a `DragCalc` in SubChassis. Without the model the + // The case a bare block name cannot express: a shared dictionary's DragCoeff is used by a + // `DragCalc` in Sedan AND a `DragCalc` in Truck. Without the model the // cell reads `DragCalc, DragCalc` and neither link is identifiable. const rows = [row('DragCoeff')]; annotateVariableRows( @@ -383,14 +383,14 @@ describe('annotateVariableRows names the model in every usage', () => { rows, graphSaying({ [`${SRC}\nDragCoeff`]: [ - link('DragCalc', 'MainVehicle', 'file:///w/MainVehicle.slx'), - link('DragCalc', 'SubChassis', 'file:///w/SubChassis.slx'), + link('DragCalc', 'Sedan', 'file:///w/Sedan.slx'), + link('DragCalc', 'Truck', 'file:///w/Truck.slx'), ], }), ); expect(rows[0].UsedBy.blockLinks.map((b: any) => `${b.blockName}(${b.modelName})`)).toEqual([ - 'DragCalc(MainVehicle)', - 'DragCalc(SubChassis)', + 'DragCalc(Sedan)', + 'DragCalc(Truck)', ]); }); @@ -426,41 +426,41 @@ describe('annotateVariableRows names the model in every usage', () => { it('OVERWRITES a session-supplied cell that names no model and knows fewer blocks', () => { // The regression. `node.toRow()` had already answered from the session, which held - // FuelInjector.slx but not EngineCtrl.slx, so AFRTarget read `AFRConst, AFRCheck`: + // FuelPath.slx but not EngineLoop.slx, so AFRTarget read `AFRConst, AFRCheck`: // no model, and three of its five users missing. The workspace graph knows all five // whether or not either model is open, so it wins outright. const rows = [ row('AFRTarget', { links: [ - { text: 'AFRConst', linkTarget: 'AFRConst@file:///w/FuelInjector.slx' }, - { text: 'AFRCheck', linkTarget: 'AFRCheck@file:///w/FuelInjector.slx' }, + { text: 'AFRConst', linkTarget: 'AFRConst@file:///w/FuelPath.slx' }, + { text: 'AFRCheck', linkTarget: 'AFRCheck@file:///w/FuelPath.slx' }, ], }), ]; - const E = 'file:///w/EngineCtrl.slx'; - const F = 'file:///w/FuelInjector.slx'; + const E = 'file:///w/EngineLoop.slx'; + const F = 'file:///w/FuelPath.slx'; expect( annotateVariableRows( SRC, rows, graphSaying({ [`${SRC}\nAFRTarget`]: [ - link('AFR', 'EngineCtrl', E), - link('AFRMonitor', 'EngineCtrl', E), - link('MixTarget', 'EngineCtrl', E), - link('AFRConst', 'FuelInjector', F), - link('AFRCheck', 'FuelInjector', F), + link('AFR', 'EngineLoop', E), + link('AFRMonitor', 'EngineLoop', E), + link('MixTarget', 'EngineLoop', E), + link('AFRConst', 'FuelPath', F), + link('AFRCheck', 'FuelPath', F), ], }), ), ).toBe(true); expect('links' in rows[0].UsedBy).toBe(false); expect(rows[0].UsedBy.blockLinks.map((b: any) => `${b.blockName}(${b.modelName})`)).toEqual([ - 'AFR(EngineCtrl)', - 'AFRMonitor(EngineCtrl)', - 'MixTarget(EngineCtrl)', - 'AFRConst(FuelInjector)', - 'AFRCheck(FuelInjector)', + 'AFR(EngineLoop)', + 'AFRMonitor(EngineLoop)', + 'MixTarget(EngineLoop)', + 'AFRConst(FuelPath)', + 'AFRCheck(FuelPath)', ]); }); diff --git a/test/usageEndToEnd.test.ts b/test/usageEndToEnd.test.ts index 35085b1..c23cbe6 100644 --- a/test/usageEndToEnd.test.ts +++ b/test/usageEndToEnd.test.ts @@ -109,8 +109,8 @@ describe('the usage graph over the real fixture files', () => { // Each also carries where the block IS. Every one of these sits in its model's root // system, so the path is the label alone — the interesting case (two `Gain`s in two // subsystems) is pinned in usageCells.test.ts over bytes built for it, and in core - // against MATLAB-written f14.slx. What this pins is that the field survives the - // whole path from core's index to the cell. + // against a shipped Simulink demo model MATLAB wrote. What this pins is that the + // field survives the whole path from core's index to the cell. expect(graph().blocksUsing(DICT, 'Kp')).toEqual([ { blockName: 'Gain1', blockPath: 'Gain1', modelName: 'legacy_ctrl', modelUri: CTRL, linkTarget: `blocks:Gain1@${CTRL}` }, { blockName: 'PlantGain', blockPath: 'PlantGain', modelName: 'shared_gain', modelUri: GAIN, linkTarget: `blocks:1@${GAIN}` },