From 8cb996961708d9f9777947b74c6a1b48fff2fabc Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 15:59:57 +0100 Subject: [PATCH 01/47] Refactor for reuse Signed-off-by: Jens Reinecke --- src/desktop/extension.ts | 2 +- ...iewer-main.ts => component-viewer-base.ts} | 48 +++++++++++-------- .../component-viewer/component-viewer.ts | 28 +++++++++++ ....test.ts => component-viewer-base.test.ts} | 15 ++++-- 4 files changed, 67 insertions(+), 26 deletions(-) rename src/views/component-viewer/{component-viewer-main.ts => component-viewer-base.ts} (87%) create mode 100644 src/views/component-viewer/component-viewer.ts rename src/views/component-viewer/test/unit/{component-viewer-main.test.ts => component-viewer-base.test.ts} (98%) diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index ffd69ef6..c45b5e3c 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -24,7 +24,7 @@ import { CpuStates } from '../features/cpu-states/cpu-states'; import { CpuStatesCommands } from '../features/cpu-states/cpu-states-commands'; import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch'; import { GenericCommands } from '../features/generic-commands'; -import { ComponentViewer } from '../views/component-viewer/component-viewer-main'; +import { ComponentViewer } from '../views/component-viewer/component-viewer'; import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view'; const BUILTIN_TOOLS_PATHS = [ diff --git a/src/views/component-viewer/component-viewer-main.ts b/src/views/component-viewer/component-viewer-base.ts similarity index 87% rename from src/views/component-viewer/component-viewer-main.ts rename to src/views/component-viewer/component-viewer-base.ts index 1d0ed937..580e3777 100755 --- a/src/views/component-viewer/component-viewer-main.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -23,6 +23,7 @@ import { componentViewerLogger, logger } from '../../logger'; import type { ScvdGuiInterface } from './model/scvd-gui-interface'; import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; +import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; export type UpdateReason = 'sessionChanged' | 'refreshTimer' | 'stackTrace' | 'stackItemChanged' | 'unlockingInstance'; @@ -33,7 +34,7 @@ export interface ComponentViewerInstancesWrapper { dirtyWhileLocked: boolean; // Flag to indicate if an update was attempted while instance was locked, used to trigger an update when instance is unlocked } -export class ComponentViewer { +export class ComponentViewerBase { private _activeSession: GDBTargetDebugSession | undefined; private _instances: ComponentViewerInstancesWrapper[] = []; private _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider; @@ -46,7 +47,12 @@ export class ComponentViewer { private _refreshTimerEnabled: boolean = true; private static readonly pendingUpdateDelayMs = 150; - public constructor(context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider) { + public constructor( + context: vscode.ExtensionContext, + componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, + protected readonly _viewName: string, + protected readonly _viewId: string + ) { this._context = context; this._componentViewerTreeDataProvider = componentViewerTreeDataProvider; } @@ -65,29 +71,31 @@ export class ComponentViewer { } protected async registerTreeView(): Promise { - if (!await vscodeViewExists('componentViewer')) { + if (!await vscodeViewExists(this._viewId)) { return false; } - const treeView = vscode.window.createTreeView('cmsis-debugger.componentViewer', { + const fullViewId = `${VIEW_PREFIX}.${this._viewId}`; + const commandPrefix = `${EXTENSION_NAME}.${this._viewId}`; + const treeView = vscode.window.createTreeView(fullViewId, { treeDataProvider: this._componentViewerTreeDataProvider, showCollapseAll: true }); - componentViewerLogger.debug('Component Viewer: Created Component Viewer tree view: cmsis-debugger.componentViewer'); + componentViewerLogger.debug(`${this._viewName}: Created ${this._viewName} tree view with id: ${fullViewId}`); const onDidExpandElementDisposable = treeView.onDidExpandElement(event => this.handleOnDidToggleExpand(event, true)); const onDidCollapseElementDisposable = treeView.onDidCollapseElement(event => this.handleOnDidToggleExpand(event, false)); const lockInstanceCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.lockComponent', async (node) => { this.handleLockInstance(node); }); - const unlockInstanceCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.unlockComponent', async (node) => { + const unlockInstanceCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.unlockComponent`, async (node) => { this.handleLockInstance(node); }); - const enablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.enablePeriodicUpdate', async () => { + const enablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.enablePeriodicUpdate`, async () => { this._refreshTimerEnabled = true; - componentViewerLogger.info('Component Viewer: Auto refresh enabled'); + componentViewerLogger.info(`${this._viewName}: Auto refresh enabled`); }); - const disablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate', async () => { + const disablePeriodicUpdateCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.disablePeriodicUpdate`, async () => { this._refreshTimerEnabled = false; - componentViewerLogger.info('Component Viewer: Auto refresh disabled'); + componentViewerLogger.info(`${this._viewName}: Auto refresh disabled`); }); this._context.subscriptions.push( treeView, @@ -126,7 +134,7 @@ export class ComponentViewer { shouldTriggerUpdate = true; } instance.lockState = !instance.lockState; - componentViewerLogger.info(`Component Viewer: Instance lock state changed to ${instance.lockState}`); + componentViewerLogger.info(`${this._viewName}: Instance lock state changed to ${instance.lockState}`); // If instance is locked, set isLocked flag to true for root nodes const guiTree = instance.componentViewerInstance.getGuiTree(); if (!guiTree || guiTree.length === 0) { @@ -163,9 +171,9 @@ export class ComponentViewer { try { await instance.readModel(URI.file(scvdFilePath), this._activeSession, tracker); } catch (error) { - componentViewerLogger.error(`Component Viewer: Failed to read SCVD file at ${scvdFilePath} - ${(error as Error).message}`); + componentViewerLogger.error(`${this._viewName}: Failed to read SCVD file at ${scvdFilePath} - ${(error as Error).message}`); // Show error message in a pop up to the user, but continue loading other instances if there are multiple SCVD files - vscode.window.showErrorMessage(`Component Viewer: cannot read SCVD file at ${scvdFilePath}`); + vscode.window.showErrorMessage(`${this._viewName}: cannot read SCVD file at ${scvdFilePath}`); continue; } @@ -226,7 +234,7 @@ export class ComponentViewer { private async handleOnStackTrace(session: GDBTargetDebugSession): Promise { // Clear active session if it is NOT the one being stopped if (this._activeSession?.session.id !== session.session.id) { - throw new Error(`Component Viewer: Received stack trace event for session ${session.session.id} while active session is ${this._activeSession?.session.id}`); + throw new Error(`${this._viewName}: Received stack trace event for session ${session.session.id} while active session is ${this._activeSession?.session.id}`); } // Update component viewer instance(s) if active session is stopped this.schedulePendingUpdate('stackTrace'); @@ -236,7 +244,7 @@ export class ComponentViewer { // If the active session is not the one being updated, update it. // This can happen when a session is started and stack trace/item events are emitted before the session is set as active in the component viewer. if (this._activeSession?.session.id !== session.session.id) { - throw new Error(`Component Viewer: Received stack item changed event for session ${session.session.id} while active session is ${this._activeSession?.session.id}`); + throw new Error(`${this._viewName}: Received stack item changed event for session ${session.session.id} while active session is ${this._activeSession?.session.id}`); } this.schedulePendingUpdate('stackItemChanged'); } @@ -302,7 +310,7 @@ export class ComponentViewer { this._pendingUpdateTimer = setTimeout(() => { this._pendingUpdateTimer = undefined; void this.runUpdate(updateReason); - }, ComponentViewer.pendingUpdateDelayMs); + }, ComponentViewerBase.pendingUpdateDelayMs); } private async runUpdate(updateReason: UpdateReason): Promise { @@ -315,7 +323,7 @@ export class ComponentViewer { try { await this.updateInstances(updateReason); } catch (error) { - componentViewerLogger.error(`Component Viewer: Error during update - ${(error as Error).message}`); + componentViewerLogger.error(`${this._viewName}: Error during update - ${(error as Error).message}`); } } this._runningUpdate = false; @@ -345,11 +353,11 @@ export class ComponentViewer { this._componentViewerTreeDataProvider.clear(); return; } - componentViewerLogger.debug(`Component Viewer: Queuing update due to '${updateReason}'`); + componentViewerLogger.debug(`${this._viewName}: Queuing update due to '${updateReason}'`); this._instanceUpdateCounter = 0; if (!this.shouldUpdateInstances(this._activeSession)) { - componentViewerLogger.debug(`Component Viewer: Skipping update due to '${updateReason}' - conditions not met`); + componentViewerLogger.debug(`${this._viewName}: Skipping update due to '${updateReason}' - conditions not met`); return; } @@ -365,7 +373,7 @@ export class ComponentViewer { continue; } this._instanceUpdateCounter++; - componentViewerLogger.debug(`Updating Component Viewer Instance #${this._instanceUpdateCounter} due to '${updateReason}'`); + componentViewerLogger.debug(`${this._viewName}: Updating ${this._viewName} Instance #${this._instanceUpdateCounter} due to '${updateReason}'`); // Check instance's lock state, skip update if locked if (!instance.lockState) { diff --git a/src/views/component-viewer/component-viewer.ts b/src/views/component-viewer/component-viewer.ts new file mode 100644 index 00000000..638af8ab --- /dev/null +++ b/src/views/component-viewer/component-viewer.ts @@ -0,0 +1,28 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; +import { ComponentViewerBase } from './component-viewer-base'; +import { ComponentViewerTreeDataProvider } from './component-viewer-tree-view'; + +export class ComponentViewer extends ComponentViewerBase { + public constructor( + context: vscode.ExtensionContext, + componentViewerTreeDataProvider: ComponentViewerTreeDataProvider + ) { + super(context, componentViewerTreeDataProvider, 'Component Viewer', 'componentViewer'); + } +} diff --git a/src/views/component-viewer/test/unit/component-viewer-main.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts similarity index 98% rename from src/views/component-viewer/test/unit/component-viewer-main.test.ts rename to src/views/component-viewer/test/unit/component-viewer-base.test.ts index 61d9e6b8..449a42e4 100644 --- a/src/views/component-viewer/test/unit/component-viewer-main.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -24,7 +24,7 @@ import type { GDBTargetDebugTracker } from '../../../../debug-session'; import type { TargetState } from '../../../../debug-session/gdbtarget-debug-session'; import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; -import { ComponentViewer, ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-main'; +import { ComponentViewerBase, ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; @@ -76,13 +76,13 @@ function asMockedFunction( return fn as unknown as jest.MockedFunction<(...args: Args) => Return>; } -const getUpdateInstances = (controller: ComponentViewer) => +const getUpdateInstances = (controller: ComponentViewerBase) => (controller as unknown as { updateInstances: (reason: UpdateReason) => Promise }).updateInstances.bind(controller); -const getSchedulePendingUpdate = (controller: ComponentViewer) => +const getSchedulePendingUpdate = (controller: ComponentViewerBase) => (controller as unknown as { schedulePendingUpdate: (reason: UpdateReason) => void }).schedulePendingUpdate.bind(controller); -const getRunUpdate = (controller: ComponentViewer) => +const getRunUpdate = (controller: ComponentViewerBase) => (controller as unknown as { runUpdate: (reason: UpdateReason) => Promise }).runUpdate.bind(controller); // Local test mocks @@ -119,7 +119,12 @@ type TrackerCallbacks = { const createController = ( context: vscode.ExtensionContext = extensionContextFactory(), provider: ComponentViewerTreeDataProvider | ReturnType = treeProviderFactory() -): ComponentViewer => new ComponentViewer(context, provider as ComponentViewerTreeDataProvider); +): ComponentViewerBase => new ComponentViewerBase( + context, + provider as ComponentViewerTreeDataProvider, + 'Component Viewer', + 'componentViewer' +); describe('ComponentViewer', () => { beforeEach(() => { From 532cb9ff7ea215d905f5c8920bb5f2747dfcf693 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 16:48:44 +0100 Subject: [PATCH 02/47] Get SCVD files from derived class Signed-off-by: Jens Reinecke --- .../component-viewer/component-viewer-base.ts | 33 +++++++++++-------- .../component-viewer/component-viewer.ts | 16 +++++++++ ...-base.test.ts => component-viewer.test.ts} | 19 +++++------ 3 files changed, 44 insertions(+), 24 deletions(-) rename src/views/component-viewer/test/unit/{component-viewer-base.test.ts => component-viewer.test.ts} (98%) diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 580e3777..e90fe5fe 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -149,19 +149,24 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.refresh(); } - protected async readScvdFiles(tracker: GDBTargetDebugTracker,session?: GDBTargetDebugSession): Promise { + /** + * Get SCVF file paths for a given debug session. Derived class implements to get SCVD files as needed + * for specific component viewer flavor. + * + * @param _session GDB target session to get SCVD Files for + * @returns promise to an array of SCVD file paths, or empty array if no SCVD files found + */ + protected async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { + return []; + } + + protected async readScvdFiles(tracker: GDBTargetDebugTracker, session?: GDBTargetDebugSession): Promise { if (!session) { return; } - const cbuildRunReader = await session.getCbuildRun(); - const pname = await session.getPname(); - if (!cbuildRunReader) { - return; - } - // Get SCVD file paths from cbuild-run reader - const scvdFilesPaths: string [] = cbuildRunReader.getScvdFilePaths(undefined, pname); + const scvdFilesPaths = await this.getScvdFilePaths(session); if (scvdFilesPaths.length === 0) { - return undefined; + return; } parsePerf?.reset(); const cbuildRunInstances: ComponentViewerInstance[] = []; @@ -190,12 +195,12 @@ export class ComponentViewerBase { }))); } - private async loadCbuildRunInstances(session: GDBTargetDebugSession, tracker: GDBTargetDebugTracker) : Promise { + private async loadScvdFiles(session: GDBTargetDebugSession, tracker: GDBTargetDebugTracker) : Promise { this._loadingCounter++; - componentViewerLogger.debug(`Loading SCVD files from cbuild-run, attempt #${this._loadingCounter}`); - // Try to read SCVD files from cbuild-run file first + componentViewerLogger.debug(`Loading SCVD files, attempt #${this._loadingCounter}`); + // Try to read SCVD files await this.readScvdFiles(tracker, session); - // Are there any SCVD files found in cbuild-run? + // Are there any SCVD files found and loaded? if (this._instances.length === 0) { return undefined; } @@ -283,7 +288,7 @@ export class ComponentViewerBase { // Update debug session this._activeSession = session; // Load SCVD files from cbuild-run - await this.loadCbuildRunInstances(session, tracker); + await this.loadScvdFiles(session, tracker); } private async handleRefreshTimerEvent(session: GDBTargetDebugSession): Promise { diff --git a/src/views/component-viewer/component-viewer.ts b/src/views/component-viewer/component-viewer.ts index 638af8ab..9b6c46a2 100644 --- a/src/views/component-viewer/component-viewer.ts +++ b/src/views/component-viewer/component-viewer.ts @@ -17,6 +17,7 @@ import * as vscode from 'vscode'; import { ComponentViewerBase } from './component-viewer-base'; import { ComponentViewerTreeDataProvider } from './component-viewer-tree-view'; +import { GDBTargetDebugSession } from '../../debug-session'; export class ComponentViewer extends ComponentViewerBase { public constructor( @@ -25,4 +26,19 @@ export class ComponentViewer extends ComponentViewerBase { ) { super(context, componentViewerTreeDataProvider, 'Component Viewer', 'componentViewer'); } + + protected override async getScvdFilePaths(session: GDBTargetDebugSession): Promise { + const cbuildRunReader = await session.getCbuildRun(); + const pname = await session.getPname(); + if (!cbuildRunReader) { + return []; + } + // Get SCVD file paths from cbuild-run reader + const scvdFilesPaths: string [] = cbuildRunReader.getScvdFilePaths(undefined, pname); + if (scvdFilesPaths.length === 0) { + return []; + } + return scvdFilesPaths; + } + } diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer.test.ts similarity index 98% rename from src/views/component-viewer/test/unit/component-viewer-base.test.ts rename to src/views/component-viewer/test/unit/component-viewer.test.ts index 449a42e4..aa573d95 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer.test.ts @@ -24,7 +24,8 @@ import type { GDBTargetDebugTracker } from '../../../../debug-session'; import type { TargetState } from '../../../../debug-session/gdbtarget-debug-session'; import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; -import { ComponentViewerBase, ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; +import { ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; +import { ComponentViewer } from '../../component-viewer'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; @@ -76,13 +77,13 @@ function asMockedFunction( return fn as unknown as jest.MockedFunction<(...args: Args) => Return>; } -const getUpdateInstances = (controller: ComponentViewerBase) => +const getUpdateInstances = (controller: ComponentViewer) => (controller as unknown as { updateInstances: (reason: UpdateReason) => Promise }).updateInstances.bind(controller); -const getSchedulePendingUpdate = (controller: ComponentViewerBase) => +const getSchedulePendingUpdate = (controller: ComponentViewer) => (controller as unknown as { schedulePendingUpdate: (reason: UpdateReason) => void }).schedulePendingUpdate.bind(controller); -const getRunUpdate = (controller: ComponentViewerBase) => +const getRunUpdate = (controller: ComponentViewer) => (controller as unknown as { runUpdate: (reason: UpdateReason) => Promise }).runUpdate.bind(controller); // Local test mocks @@ -119,11 +120,9 @@ type TrackerCallbacks = { const createController = ( context: vscode.ExtensionContext = extensionContextFactory(), provider: ComponentViewerTreeDataProvider | ReturnType = treeProviderFactory() -): ComponentViewerBase => new ComponentViewerBase( +): ComponentViewer => new ComponentViewer( context, - provider as ComponentViewerTreeDataProvider, - 'Component Viewer', - 'componentViewer' + provider as ComponentViewerTreeDataProvider ); describe('ComponentViewer', () => { @@ -318,8 +317,8 @@ describe('ComponentViewer', () => { (controller as unknown as { readScvdFiles: typeof readScvdFiles }).readScvdFiles = readScvdFiles; const load = (controller as unknown as { - loadCbuildRunInstances: (s: Session, t: TrackerCallbacks) => Promise; - }).loadCbuildRunInstances.bind(controller); + loadScvdFiles: (s: Session, t: TrackerCallbacks) => Promise; + }).loadScvdFiles.bind(controller); const result = await load(session, tracker); expect(result).toBeUndefined(); From ffde1d41d9e4af8bc9ed12802391295b1a8718bd Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 18:18:44 +0100 Subject: [PATCH 03/47] Add Core Peripheral Viewer Signed-off-by: Jens Reinecke --- package.json | 53 +++++++++++++++++++ src/desktop/extension.ts | 9 ++++ .../core-peripheral-viewer.ts | 34 ++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/views/core-peripheral-viewer/core-peripheral-viewer.ts diff --git a/package.json b/package.json index 1c8bb2f1..ef0f0759 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,11 @@ "id": "cmsis-debugger.componentViewer", "name": "Component Viewer", "icon": "media/trace-and-live-light.svg" + }, + { + "id": "cmsis-debugger.corePeripheralViewer", + "name": "Core Peripheral Viewer", + "icon": "media/trace-and-live-light.svg" } ] }, @@ -165,6 +170,28 @@ "command": "vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate", "title": "Disable Periodic Update", "category": "Component Viewer" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", + "title": "Lock Component", + "icon": "$(unlock)", + "category": "Core Peripheral Viewer" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", + "title": "Unlock Component", + "icon": "$(lock)", + "category": "Core Peripheral Viewer" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.enablePeriodicUpdate", + "title": "Enable Periodic Update", + "category": "Core Peripheral Viewer" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.disablePeriodicUpdate", + "title": "Disable Periodic Update", + "category": "Core Peripheral Viewer" } ], "menus": { @@ -264,6 +291,22 @@ { "command": "vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate", "when": "true" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", + "when": "false" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.enablePeriodicUpdate", + "when": "true" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.disablePeriodicUpdate", + "when": "true" } ], "view/title": [ @@ -350,6 +393,16 @@ "command": "vscode-cmsis-debugger.componentViewer.unlockComponent", "when": "view == cmsis-debugger.componentViewer && viewItem == locked.parentInstance", "group": "inline@2" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", + "when": "view == cmsis-debugger.corePeripheralViewer && viewItem == parentInstance", + "group": "inline@1" + }, + { + "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", + "when": "view == cmsis-debugger.corePeripheralViewer && viewItem == locked.parentInstance", + "group": "inline@2" } ] }, diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index c45b5e3c..ebce193e 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -26,6 +26,7 @@ import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch'; import { GenericCommands } from '../features/generic-commands'; import { ComponentViewer } from '../views/component-viewer/component-viewer'; import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view'; +import { CorePeripheralViewer } from '../views/core-peripheral-viewer/core-peripheral-viewer'; const BUILTIN_TOOLS_PATHS = [ 'tools/pyocd/pyocd', @@ -34,6 +35,7 @@ const BUILTIN_TOOLS_PATHS = [ let liveWatchTreeDataProvider: LiveWatchTreeDataProvider; let componentViewerTreeDataProvider: ComponentViewerTreeDataProvider; +let corePeripheralViewerTreeDataProvider: ComponentViewerTreeDataProvider; const askForReload = async (): Promise => { const result = await vscode.window.showWarningMessage('Cannot activate all Arm CMSIS Debugger views. Please reload the window.', 'Reload Window'); @@ -53,7 +55,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise // Register the Tree View under the id from package.json liveWatchTreeDataProvider = new LiveWatchTreeDataProvider(context); componentViewerTreeDataProvider = new ComponentViewerTreeDataProvider(); + corePeripheralViewerTreeDataProvider = new ComponentViewerTreeDataProvider(); const componentViewer = new ComponentViewer(context, componentViewerTreeDataProvider); + const corePeripheralViewer = new CorePeripheralViewer(context, corePeripheralViewerTreeDataProvider); addToolsToPath(context, BUILTIN_TOOLS_PATHS); // Activate generic commands @@ -75,6 +79,11 @@ export const activate = async (context: vscode.ExtensionContext): Promise if (!await componentViewer.activate(gdbtargetDebugTracker)) { canCompleteActivation = false; } + // Core Peripheral Viewer + logger.debug('Activating Core Peripheral Viewer'); + if (!await corePeripheralViewer.activate(gdbtargetDebugTracker)) { + canCompleteActivation = false; + } if (!canCompleteActivation) { logger.debug('CMSIS Debugger activation incomplete'); diff --git a/src/views/core-peripheral-viewer/core-peripheral-viewer.ts b/src/views/core-peripheral-viewer/core-peripheral-viewer.ts new file mode 100644 index 00000000..08ec14b5 --- /dev/null +++ b/src/views/core-peripheral-viewer/core-peripheral-viewer.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as vscode from 'vscode'; +import { ComponentViewerBase } from '../component-viewer/component-viewer-base'; +import { ComponentViewerTreeDataProvider } from '../component-viewer/component-viewer-tree-view'; +import { GDBTargetDebugSession } from '../../debug-session'; + +export class CorePeripheralViewer extends ComponentViewerBase { + public constructor( + context: vscode.ExtensionContext, + componentViewerTreeDataProvider: ComponentViewerTreeDataProvider + ) { + super(context, componentViewerTreeDataProvider, 'Core Peripheral Viewer', 'corePeripheralViewer'); + } + + protected override async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { + return []; + } + +} From 89e9725a1b075a66ce223b2e63252b33b712a694 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 18:22:12 +0100 Subject: [PATCH 04/47] Add core peripheral scvd files Signed-off-by: Jens Reinecke --- .../Memory_Protection_Unit.scvd | 208 ++++++++++++++++++ .../Nested_Vectored_Interrupt_Controller.scvd | 185 ++++++++++++++++ .../System_Config_and_Control.scvd | 45 ++++ .../System_Tick_Timer.scvd | 50 +++++ 4 files changed, 488 insertions(+) create mode 100644 configs/core-peripheral-viewer/Memory_Protection_Unit.scvd create mode 100644 configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd create mode 100644 configs/core-peripheral-viewer/System_Config_and_Control.scvd create mode 100644 configs/core-peripheral-viewer/System_Tick_Timer.scvd diff --git a/configs/core-peripheral-viewer/Memory_Protection_Unit.scvd b/configs/core-peripheral-viewer/Memory_Protection_Unit.scvd new file mode 100644 index 00000000..30ef8338 --- /dev/null +++ b/configs/core-peripheral-viewer/Memory_Protection_Unit.scvd @@ -0,0 +1,208 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MPU_Region.Number=(MPU_RNR_value>>0)&0xFF; + MPU_Region.Enable=(MPU_Region.MPU_RLAR_value>>0)&0x1; + MPU_Region.StartAddress=(MPU_Region.MPU_RBAR_value&0xFFFFFFE0); + MPU_Region.EndAddress=(MPU_Region.MPU_RLAR_value|0x1F); + MPU_Region.ExecuteNever=(MPU_Region.MPU_RBAR_value>>0)&0x1; + MPU_Region.AccessPermissions=(MPU_Region.MPU_RBAR_value>>1)&0x3; + MPU_Region.Shareability=(MPU_Region.MPU_RBAR_value>>3)&0x3; + MPU_Region.AttributeIndex=(MPU_Region.MPU_RLAR_value>>1)&0x7; + MPU_Region.PrivilegedExecuteNever=(MPU_Region.MPU_RLAR_value>>4)&0x1; + + + + + MPU_Attributes.Attr_Outer[Count]=((((MPU_Attributes.MPU_MAIR_values[Count/4]>>(((Count)&3)*8))&0xFF)>>4)&0xF); + MPU_Attributes.Attr_Inner[Count]=((((MPU_Attributes.MPU_MAIR_values[Count/4]>>(((Count)&3)*8))&0xFF)>>0)&0xF); + MPU_Attributes.Attr_Device[Count]=MPU_Attributes.Attr_Inner[Count]; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd b/configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd new file mode 100644 index 00000000..665e45ae --- /dev/null +++ b/configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Vectors.Index[Count]=Count; + + + + Vectors.Displayed[Count]=1; + + + + Vectors.Enable[Count]=1; + Vectors.Pending[Count]=((ICSR_value>>31)&0x1); + Vectors.Active[Count]=((ICSR_value>>0)&0x1F)==2; + Vectors.Priority[Count]=-2; + + + + Vectors.Enable[Count]=1; + Vectors.Active[Count]=((ICSR_value>>0)&0x1F)==3; + Vectors.Pending[Count]=0; + Vectors.Priority[Count]=-1; + + + Vectors.Enable[Count]=((SHCSR_value>>16)&0x1); + Vectors.Active[Count]=((SHCSR_value>>0)&0x1); + Vectors.Pending[Count]=((SHCSR_value>>13)&0x1); + Vectors.Priority[Count]=((SHPR_values[0]>>0)&0xFF); + + + Vectors.Enable[Count]=((SHCSR_value>>17)&0x1); + Vectors.Active[Count]=((SHCSR_value>>1)&0x1); + Vectors.Pending[Count]=((SHCSR_value>>14)&0x1); + Vectors.Priority[Count]=((SHPR_values[0]>>8)&0xFF); + + + Vectors.Enable[Count]=((SHCSR_value>>18)&0x1); + Vectors.Active[Count]=((SHCSR_value>>3)&0x1); + Vectors.Pending[Count]=((SHCSR_value>>12)&0x1); + Vectors.Priority[Count]=((SHPR_values[0]>>16)&0xFF); + + + Vectors.Enable[Count]=((SHCSR_value>>19)&0x1); + Vectors.Active[Count]=((SHCSR_value>>4)&0x1); + Vectors.Pending[Count]=((SHCSR_value>>20)&0x1); + Vectors.Priority[Count]=((SHPR_values[0]>>24)&0xFF); + + + Vectors.Enable[Count]=1; + Vectors.Active[Count]=((SHCSR_value>>7)&0x1); + Vectors.Pending[Count]=((SHCSR_value>>15)&0x1); + Vectors.Priority[Count]=((SHPR_values[1]>>24)&0xFF); + + + Vectors.Enable[Count]=((DEMCR_value>>16)&0x1); + Vectors.Active[Count]=((SHCSR_value>>8)&0x1); + Vectors.Pending[Count]=((DEMCR_value>>17)&0x1); + Vectors.Priority[Count]=((SHPR_values[2]>>0)&0xFF); + + + Vectors.Enable[Count]=1; + Vectors.Active[Count]=((SHCSR_value>>10)&0x1); + Vectors.Pending[Count]=((ICSR_value>>28)&0x1); + Vectors.Priority[Count]=((SHPR_values[2]>>16)&0xFF); + + + Vectors.Enable[Count]=((SYST_CSR_value>>1)&0x1); + Vectors.Active[Count]=((SHCSR_value>>11)&0x1); + Vectors.Pending[Count]=((ICSR_value>>26)&0x1); + Vectors.Priority[Count]=((SHPR_values[2]>>24)&0xFF); + + + + + Vectors.Enable[Count]=((NVIC_ISER_values[((Count-16)/32)]>>(((Count-16)&0x1F)))&0x1); + Vectors.Active[Count]=((NVIC_IABR_values[((Count-16)/32)]>>(((Count-16)&0x1F)))&0x1); + Vectors.Pending[Count]=((NVIC_ISPR_values[((Count-16)/32)]>>(((Count-16)&0x1F)))&0x1); + Vectors.Priority[Count]=((NVIC_IPR_values[((Count-16)/4)]>>(((Count-16)&0x3)*8))&0xFF); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/core-peripheral-viewer/System_Config_and_Control.scvd b/configs/core-peripheral-viewer/System_Config_and_Control.scvd new file mode 100644 index 00000000..8163e603 --- /dev/null +++ b/configs/core-peripheral-viewer/System_Config_and_Control.scvd @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configs/core-peripheral-viewer/System_Tick_Timer.scvd b/configs/core-peripheral-viewer/System_Tick_Timer.scvd new file mode 100644 index 00000000..16f9e3bd --- /dev/null +++ b/configs/core-peripheral-viewer/System_Tick_Timer.scvd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 60ec5f22d2f8b4e5fe8a27bafc6f3c7d95050abf Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 18:33:10 +0100 Subject: [PATCH 05/47] Read core perpiheral scvd files Signed-off-by: Jens Reinecke --- .../core-peripheral-viewer.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/views/core-peripheral-viewer/core-peripheral-viewer.ts b/src/views/core-peripheral-viewer/core-peripheral-viewer.ts index 08ec14b5..86eff3dc 100644 --- a/src/views/core-peripheral-viewer/core-peripheral-viewer.ts +++ b/src/views/core-peripheral-viewer/core-peripheral-viewer.ts @@ -14,10 +14,16 @@ * limitations under the License. */ +import * as fs from 'fs'; +import * as path from 'path'; import * as vscode from 'vscode'; import { ComponentViewerBase } from '../component-viewer/component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../component-viewer/component-viewer-tree-view'; import { GDBTargetDebugSession } from '../../debug-session'; +import { promisify } from 'util'; + +// Relative to dist folder at runtime +const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripheral-viewer'); export class CorePeripheralViewer extends ComponentViewerBase { public constructor( @@ -28,7 +34,14 @@ export class CorePeripheralViewer extends ComponentViewerBase { } protected override async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - return []; + const filePaths = await promisify(fs.readdir)(CORE_PERIPHERAL_SCVD_BASE, { + encoding: 'buffer', + withFileTypes: true + }); + const scvdFilePaths = filePaths + .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) + .map((file) => path.join(CORE_PERIPHERAL_SCVD_BASE, file.name.toString())); + return scvdFilePaths; } } From 52666b6e8e3e02a75dd5e1aac4b0418cc28a3d48 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 20 Feb 2026 20:33:09 +0100 Subject: [PATCH 06/47] tests Signed-off-by: Jens Reinecke --- __mocks__/vscode.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index 1ecacd4c..86bd4f29 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -121,10 +121,13 @@ module.exports = { executeCommand: jest.fn(), // Default to all views in extension having been correctly loaded getCommands: jest.fn(() => Promise.resolve([ + // Real views in extension 'cmsis-debugger.liveWatch.open', 'cmsis-debugger.liveWatch.focus', 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus', + 'cmsis-debugger.corePeripheralViewer.open', + 'cmsis-debugger.corePeripheralViewer.focus', ])), registerCommand: jest.fn(), }, From 33e2553ba85c148c34f56a50f5f6779b9665b822 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 24 Feb 2026 19:42:09 +0100 Subject: [PATCH 07/47] Fixes after rebase to main Signed-off-by: Jens Reinecke --- src/views/component-viewer/component-viewer-base.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index e90fe5fe..1abfccb0 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -59,13 +59,13 @@ export class ComponentViewerBase { public async activate(tracker: GDBTargetDebugTracker): Promise { // Register Component Viewer tree view - logger.debug('Activating Component Viewer Tree View and commands'); + logger.debug(`Activating ${this._viewName} Tree View and commands`); if (!await this.registerTreeView()) { - logger.error('Component Viewer: Component Viewer cannot be registered, abort activation'); + logger.error(`${this._viewName}: ${this._viewName} cannot be registered, abort activation`); return false; } // Subscribe to debug tracker events to update active session - componentViewerLogger.debug('Subscribing to debug tracker events'); + componentViewerLogger.debug(`${this._viewName}: Subscribing to debug tracker events`); this.subscribetoDebugTrackerEvents(tracker); return true; } @@ -83,7 +83,7 @@ export class ComponentViewerBase { componentViewerLogger.debug(`${this._viewName}: Created ${this._viewName} tree view with id: ${fullViewId}`); const onDidExpandElementDisposable = treeView.onDidExpandElement(event => this.handleOnDidToggleExpand(event, true)); const onDidCollapseElementDisposable = treeView.onDidCollapseElement(event => this.handleOnDidToggleExpand(event, false)); - const lockInstanceCommandDisposable = vscode.commands.registerCommand('vscode-cmsis-debugger.componentViewer.lockComponent', async (node) => { + const lockInstanceCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.lockComponent`, async (node) => { this.handleLockInstance(node); }); const unlockInstanceCommandDisposable = vscode.commands.registerCommand(`${commandPrefix}.unlockComponent`, async (node) => { @@ -112,7 +112,7 @@ export class ComponentViewerBase { protected handleOnDidToggleExpand(expansionEvent: vscode.TreeViewExpansionEvent, expand: boolean): void { const expandStateString = expand ? 'expanded' : 'collapsed'; const elementName = expansionEvent.element.getGuiName() ?? 'unknown'; - componentViewerLogger.debug(`Component Viewer: Tree item ${expandStateString} - ${elementName}`); + componentViewerLogger.debug(`${this._viewName}: Tree item ${expandStateString} - ${elementName}`); this._componentViewerTreeDataProvider.setElementExpanded(expansionEvent.element, expand); } @@ -197,7 +197,7 @@ export class ComponentViewerBase { private async loadScvdFiles(session: GDBTargetDebugSession, tracker: GDBTargetDebugTracker) : Promise { this._loadingCounter++; - componentViewerLogger.debug(`Loading SCVD files, attempt #${this._loadingCounter}`); + componentViewerLogger.debug(`${this._viewName}: Loading SCVD files, attempt #${this._loadingCounter}`); // Try to read SCVD files await this.readScvdFiles(tracker, session); // Are there any SCVD files found and loaded? From d64d8ff36a97a5d0ec1d2b8fffff54ab625203ae Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 24 Feb 2026 19:45:07 +0100 Subject: [PATCH 08/47] "Core Peripheral Viewer" -> "Core Periphersals" Signed-off-by: Jens Reinecke --- .../Memory_Protection_Unit.scvd | 0 .../Nested_Vectored_Interrupt_Controller.scvd | 0 .../System_Config_and_Control.scvd | 0 .../System_Tick_Timer.scvd | 0 package.json | 36 +++++++++---------- src/desktop/extension.ts | 14 ++++---- .../core-peripherals.ts} | 6 ++-- 7 files changed, 28 insertions(+), 28 deletions(-) rename configs/{core-peripheral-viewer => core-peripherals}/Memory_Protection_Unit.scvd (100%) rename configs/{core-peripheral-viewer => core-peripherals}/Nested_Vectored_Interrupt_Controller.scvd (100%) rename configs/{core-peripheral-viewer => core-peripherals}/System_Config_and_Control.scvd (100%) rename configs/{core-peripheral-viewer => core-peripherals}/System_Tick_Timer.scvd (100%) rename src/views/{core-peripheral-viewer/core-peripheral-viewer.ts => core-peripherals/core-peripherals.ts} (92%) diff --git a/configs/core-peripheral-viewer/Memory_Protection_Unit.scvd b/configs/core-peripherals/Memory_Protection_Unit.scvd similarity index 100% rename from configs/core-peripheral-viewer/Memory_Protection_Unit.scvd rename to configs/core-peripherals/Memory_Protection_Unit.scvd diff --git a/configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd b/configs/core-peripherals/Nested_Vectored_Interrupt_Controller.scvd similarity index 100% rename from configs/core-peripheral-viewer/Nested_Vectored_Interrupt_Controller.scvd rename to configs/core-peripherals/Nested_Vectored_Interrupt_Controller.scvd diff --git a/configs/core-peripheral-viewer/System_Config_and_Control.scvd b/configs/core-peripherals/System_Config_and_Control.scvd similarity index 100% rename from configs/core-peripheral-viewer/System_Config_and_Control.scvd rename to configs/core-peripherals/System_Config_and_Control.scvd diff --git a/configs/core-peripheral-viewer/System_Tick_Timer.scvd b/configs/core-peripherals/System_Tick_Timer.scvd similarity index 100% rename from configs/core-peripheral-viewer/System_Tick_Timer.scvd rename to configs/core-peripherals/System_Tick_Timer.scvd diff --git a/package.json b/package.json index ef0f0759..6b1dfd68 100644 --- a/package.json +++ b/package.json @@ -67,8 +67,8 @@ "icon": "media/trace-and-live-light.svg" }, { - "id": "cmsis-debugger.corePeripheralViewer", - "name": "Core Peripheral Viewer", + "id": "cmsis-debugger.corePeripherals", + "name": "Core Peripherals", "icon": "media/trace-and-live-light.svg" } ] @@ -172,26 +172,26 @@ "category": "Component Viewer" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", + "command": "vscode-cmsis-debugger.corePeripherals.lockComponent", "title": "Lock Component", "icon": "$(unlock)", - "category": "Core Peripheral Viewer" + "category": "Core Peripherals" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", + "command": "vscode-cmsis-debugger.corePeripherals.unlockComponent", "title": "Unlock Component", "icon": "$(lock)", - "category": "Core Peripheral Viewer" + "category": "Core Peripherals" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.enablePeriodicUpdate", + "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", "title": "Enable Periodic Update", - "category": "Core Peripheral Viewer" + "category": "Core Peripherals" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.disablePeriodicUpdate", + "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", "title": "Disable Periodic Update", - "category": "Core Peripheral Viewer" + "category": "Core Peripherals" } ], "menus": { @@ -293,19 +293,19 @@ "when": "true" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", + "command": "vscode-cmsis-debugger.corePeripherals.lockComponent", "when": "false" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", + "command": "vscode-cmsis-debugger.corePeripherals.unlockComponent", "when": "false" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.enablePeriodicUpdate", + "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", "when": "true" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.disablePeriodicUpdate", + "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", "when": "true" } ], @@ -395,13 +395,13 @@ "group": "inline@2" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.lockComponent", - "when": "view == cmsis-debugger.corePeripheralViewer && viewItem == parentInstance", + "command": "vscode-cmsis-debugger.corePeripherals.lockComponent", + "when": "view == cmsis-debugger.corePeripherals && viewItem == parentInstance", "group": "inline@1" }, { - "command": "vscode-cmsis-debugger.corePeripheralViewer.unlockComponent", - "when": "view == cmsis-debugger.corePeripheralViewer && viewItem == locked.parentInstance", + "command": "vscode-cmsis-debugger.corePeripherals.unlockComponent", + "when": "view == cmsis-debugger.corePeripherals && viewItem == locked.parentInstance", "group": "inline@2" } ] diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index ebce193e..db46e607 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -26,7 +26,7 @@ import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch'; import { GenericCommands } from '../features/generic-commands'; import { ComponentViewer } from '../views/component-viewer/component-viewer'; import { ComponentViewerTreeDataProvider } from '../views/component-viewer/component-viewer-tree-view'; -import { CorePeripheralViewer } from '../views/core-peripheral-viewer/core-peripheral-viewer'; +import { CorePeripherals } from '../views/core-peripherals/core-peripherals'; const BUILTIN_TOOLS_PATHS = [ 'tools/pyocd/pyocd', @@ -35,7 +35,7 @@ const BUILTIN_TOOLS_PATHS = [ let liveWatchTreeDataProvider: LiveWatchTreeDataProvider; let componentViewerTreeDataProvider: ComponentViewerTreeDataProvider; -let corePeripheralViewerTreeDataProvider: ComponentViewerTreeDataProvider; +let corePeripheralsTreeDataProvider: ComponentViewerTreeDataProvider; const askForReload = async (): Promise => { const result = await vscode.window.showWarningMessage('Cannot activate all Arm CMSIS Debugger views. Please reload the window.', 'Reload Window'); @@ -55,9 +55,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise // Register the Tree View under the id from package.json liveWatchTreeDataProvider = new LiveWatchTreeDataProvider(context); componentViewerTreeDataProvider = new ComponentViewerTreeDataProvider(); - corePeripheralViewerTreeDataProvider = new ComponentViewerTreeDataProvider(); + corePeripheralsTreeDataProvider = new ComponentViewerTreeDataProvider(); const componentViewer = new ComponentViewer(context, componentViewerTreeDataProvider); - const corePeripheralViewer = new CorePeripheralViewer(context, corePeripheralViewerTreeDataProvider); + const corePeripherals = new CorePeripherals(context, corePeripheralsTreeDataProvider); addToolsToPath(context, BUILTIN_TOOLS_PATHS); // Activate generic commands @@ -79,9 +79,9 @@ export const activate = async (context: vscode.ExtensionContext): Promise if (!await componentViewer.activate(gdbtargetDebugTracker)) { canCompleteActivation = false; } - // Core Peripheral Viewer - logger.debug('Activating Core Peripheral Viewer'); - if (!await corePeripheralViewer.activate(gdbtargetDebugTracker)) { + // Core Peripherals + logger.debug('Activating Core Peripherals'); + if (!await corePeripherals.activate(gdbtargetDebugTracker)) { canCompleteActivation = false; } diff --git a/src/views/core-peripheral-viewer/core-peripheral-viewer.ts b/src/views/core-peripherals/core-peripherals.ts similarity index 92% rename from src/views/core-peripheral-viewer/core-peripheral-viewer.ts rename to src/views/core-peripherals/core-peripherals.ts index 86eff3dc..b32a3758 100644 --- a/src/views/core-peripheral-viewer/core-peripheral-viewer.ts +++ b/src/views/core-peripherals/core-peripherals.ts @@ -23,14 +23,14 @@ import { GDBTargetDebugSession } from '../../debug-session'; import { promisify } from 'util'; // Relative to dist folder at runtime -const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripheral-viewer'); +const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); -export class CorePeripheralViewer extends ComponentViewerBase { +export class CorePeripherals extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider ) { - super(context, componentViewerTreeDataProvider, 'Core Peripheral Viewer', 'corePeripheralViewer'); + super(context, componentViewerTreeDataProvider, 'Core Peripherals', 'corePeripherals'); } protected override async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { From cd3d18891cefbc2cc941ed5f6fe3da14b19e7912 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 17:21:04 +0100 Subject: [PATCH 09/47] Fix vscode mock Signed-off-by: Jens Reinecke --- __mocks__/vscode.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__mocks__/vscode.js b/__mocks__/vscode.js index 86bd4f29..0e7665f9 100644 --- a/__mocks__/vscode.js +++ b/__mocks__/vscode.js @@ -126,8 +126,8 @@ module.exports = { 'cmsis-debugger.liveWatch.focus', 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus', - 'cmsis-debugger.corePeripheralViewer.open', - 'cmsis-debugger.corePeripheralViewer.focus', + 'cmsis-debugger.corePeripherals.open', + 'cmsis-debugger.corePeripherals.focus', ])), registerCommand: jest.fn(), }, From fd0616c23b390a846acc92f33d0ca700dfbbe74f Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 17:48:47 +0100 Subject: [PATCH 10/47] ComponentViewer tests -> ComponentViewerBase tests Signed-off-by: Jens Reinecke --- ....test.ts => component-viewer-base.test.ts} | 84 ++++++++++++------- 1 file changed, 54 insertions(+), 30 deletions(-) rename src/views/component-viewer/test/unit/{component-viewer.test.ts => component-viewer-base.test.ts} (93%) diff --git a/src/views/component-viewer/test/unit/component-viewer.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts similarity index 93% rename from src/views/component-viewer/test/unit/component-viewer.test.ts rename to src/views/component-viewer/test/unit/component-viewer-base.test.ts index aa573d95..d7261938 100644 --- a/src/views/component-viewer/test/unit/component-viewer.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -16,16 +16,16 @@ // generated with AI /** - * Unit test for ComponentViewerController. + * Unit test for ComponentViewerBase class. */ import * as vscode from 'vscode'; import type { GDBTargetDebugTracker } from '../../../../debug-session'; -import type { TargetState } from '../../../../debug-session/gdbtarget-debug-session'; +import type { GDBTargetDebugSession, TargetState } from '../../../../debug-session/gdbtarget-debug-session'; import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; import { ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; -import { ComponentViewer } from '../../component-viewer'; +import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; @@ -77,15 +77,33 @@ function asMockedFunction( return fn as unknown as jest.MockedFunction<(...args: Args) => Return>; } -const getUpdateInstances = (controller: ComponentViewer) => +class TestClass extends ComponentViewerBase { + public constructor( + context: vscode.ExtensionContext, + componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, + ) { + super(context, componentViewerTreeDataProvider, 'Test Class', 'testClass'); + } + + protected override async getScvdFilePaths(session: GDBTargetDebugSession): Promise { + // Lightweight implementation based on session logic + const cbuildRunReader = await session.getCbuildRun(); + return cbuildRunReader?.getScvdFilePaths() ?? []; + } +}; + +const getUpdateInstances = (controller: TestClass) => (controller as unknown as { updateInstances: (reason: UpdateReason) => Promise }).updateInstances.bind(controller); -const getSchedulePendingUpdate = (controller: ComponentViewer) => +const getSchedulePendingUpdate = (controller: TestClass) => (controller as unknown as { schedulePendingUpdate: (reason: UpdateReason) => void }).schedulePendingUpdate.bind(controller); -const getRunUpdate = (controller: ComponentViewer) => +const getRunUpdate = (controller: TestClass) => (controller as unknown as { runUpdate: (reason: UpdateReason) => Promise }).runUpdate.bind(controller); +const getReadScvdFiles = (controller: TestClass) => + (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); + // Local test mocks type OnRefreshCallback = (session: Session) => void; @@ -120,14 +138,21 @@ type TrackerCallbacks = { const createController = ( context: vscode.ExtensionContext = extensionContextFactory(), provider: ComponentViewerTreeDataProvider | ReturnType = treeProviderFactory() -): ComponentViewer => new ComponentViewer( +): TestClass => new TestClass( context, provider as ComponentViewerTreeDataProvider ); -describe('ComponentViewer', () => { - beforeEach(() => { +describe('ComponentViewerBase', () => { + beforeEach(async () => { jest.clearAllMocks(); + // Extend registered commands for test class. + const defaultMockedCommands = await vscode.commands.getCommands(); + asMockedFunction(vscode.commands.getCommands).mockResolvedValue([ + ...defaultMockedCommands, + 'cmsis-debugger.testClass.open', + 'cmsis-debugger.testClass.focus', + ]); (vscode.debug as unknown as { activeDebugSession?: unknown }).activeDebugSession = undefined; (vscode.debug as unknown as { activeStackItem?: unknown }).activeStackItem = undefined; }); @@ -195,22 +220,22 @@ describe('ComponentViewer', () => { const activationResult = await controller.activate(tracker as unknown as GDBTargetDebugTracker); expect(activationResult).toBe(true); - expect(vscode.window.createTreeView).toHaveBeenCalledWith('cmsis-debugger.componentViewer', { + expect(vscode.window.createTreeView).toHaveBeenCalledWith('cmsis-debugger.testClass', { treeDataProvider: expect.any(Object), showCollapseAll: true }); - expect(vscode.commands.registerCommand).toHaveBeenCalledWith('vscode-cmsis-debugger.componentViewer.lockComponent', expect.any(Function)); - expect(vscode.commands.registerCommand).toHaveBeenCalledWith('vscode-cmsis-debugger.componentViewer.unlockComponent', expect.any(Function)); + expect(vscode.commands.registerCommand).toHaveBeenCalledWith('vscode-cmsis-debugger.testClass.lockComponent', expect.any(Function)); + expect(vscode.commands.registerCommand).toHaveBeenCalledWith('vscode-cmsis-debugger.testClass.unlockComponent', expect.any(Function)); // 1 tree view + 2 event listeners + 4 commands + 6 tracker disposables expect(context.subscriptions.length).toBe(13); }); - it('should fail to activate the component viewer tree data provider if view is not correctly loaded', async () => { + it('should fail to activate the test class tree data provider if view is not correctly loaded', async () => { const context = extensionContextFactory(); const tracker = makeTracker(); const controller = createController(context); - // Clear component viewer commands to simulate view not correctly loaded. + // Clear test class commands to simulate view not correctly loaded. // Ensure to override the mock only once to not permanently change the global mock implementation for other tests. (vscode.commands.getCommands as jest.Mock).mockResolvedValueOnce([ 'cmsis-debugger.liveWatch.open', @@ -224,8 +249,7 @@ describe('ComponentViewer', () => { const controller = createController(); const tracker = makeTracker(); - const readScvdFiles = (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); - + const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, undefined); const sessionNoReader: Session = { @@ -244,7 +268,7 @@ describe('ComponentViewer', () => { const controller = createController(); const tracker = makeTracker(); const session = makeSession('s1', []); - const readScvdFiles = (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); + const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); const instances = (controller as unknown as { _instances: unknown[] })._instances; @@ -258,7 +282,7 @@ describe('ComponentViewer', () => { const session = makeSession('s1', ['a.scvd', 'b.scvd']); (controller as unknown as { _activeSession?: Session })._activeSession = session; - const readScvdFiles = (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); + const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); const instances = (controller as unknown as { _instances: unknown[] })._instances; @@ -270,7 +294,7 @@ describe('ComponentViewer', () => { const controller = createController(); const tracker = makeTracker(); const session = makeSession('s1', ['a.scvd']); - const readScvdFiles = (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); + const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); @@ -296,14 +320,14 @@ describe('ComponentViewer', () => { const showErrorSpy = jest.spyOn(vscode.window, 'showErrorMessage').mockResolvedValue(undefined); const errorSpy = jest.spyOn(componentViewerLogger, 'error'); - const readScvdFiles = (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); + const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); expect(readModel).toHaveBeenCalled(); expect(errorSpy).toHaveBeenCalledWith( - 'Component Viewer: Failed to read SCVD file at a.scvd - boom' + 'Test Class: Failed to read SCVD file at a.scvd - boom' ); - expect(showErrorSpy).toHaveBeenCalledWith('Component Viewer: cannot read SCVD file at a.scvd'); + expect(showErrorSpy).toHaveBeenCalledWith('Test Class: cannot read SCVD file at a.scvd'); const instances = (controller as unknown as { _instances: unknown[] })._instances; expect(instances).toEqual([]); }); @@ -578,7 +602,7 @@ describe('ComponentViewer', () => { (controller as unknown as { _instances: unknown[] })._instances = [{ componentViewerInstance: inst, lockState: false, sessionId: 's1', dirtyWhileLocked: false }]; const registerCommandMock = asMockedFunction(vscode.commands.registerCommand); - const lockHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.componentViewer.lockComponent')?.[1] as + const lockHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.testClass.lockComponent')?.[1] as | ((node: ScvdGuiInterface) => Promise | void) | undefined; expect(lockHandler).toBeDefined(); @@ -643,10 +667,10 @@ describe('ComponentViewer', () => { await controller.activate(tracker as unknown as GDBTargetDebugTracker); const registerCommandMock = asMockedFunction(vscode.commands.registerCommand); - const enableHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.componentViewer.enablePeriodicUpdate')?.[1] as + const enableHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.testClass.enablePeriodicUpdate')?.[1] as | (() => Promise | void) | undefined; - const disableHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.componentViewer.disablePeriodicUpdate')?.[1] as + const disableHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.testClass.disablePeriodicUpdate')?.[1] as | (() => Promise | void) | undefined; @@ -655,11 +679,11 @@ describe('ComponentViewer', () => { await enableHandler?.(); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(true); - expect(componentViewerLogger.info).toHaveBeenCalledWith('Component Viewer: Auto refresh enabled'); + expect(componentViewerLogger.info).toHaveBeenCalledWith('Test Class: Auto refresh enabled'); await disableHandler?.(); expect((controller as unknown as { _refreshTimerEnabled: boolean })._refreshTimerEnabled).toBe(false); - expect(componentViewerLogger.info).toHaveBeenCalledWith('Component Viewer: Auto refresh disabled'); + expect(componentViewerLogger.info).toHaveBeenCalledWith('Test Class: Auto refresh disabled'); }); it('invokes unlock handler and skips lock when no matching instance exists', async () => { @@ -669,7 +693,7 @@ describe('ComponentViewer', () => { await controller.activate(tracker as unknown as GDBTargetDebugTracker); const registerCommandMock = asMockedFunction(vscode.commands.registerCommand); - const unlockHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.componentViewer.unlockComponent')?.[1] as + const unlockHandler = registerCommandMock.mock.calls.find(([command]) => command === 'vscode-cmsis-debugger.testClass.unlockComponent')?.[1] as | ((node: ScvdGuiInterface) => Promise | void) | undefined; @@ -840,7 +864,7 @@ describe('ComponentViewer', () => { }); it('silently skips periodic refresh if event comes for session not in front', async () => { - // Set up component viewer parts + // Set up test class parts const controller = createController(); const tracker = makeTracker(); await controller.activate(tracker as unknown as GDBTargetDebugTracker); @@ -885,7 +909,7 @@ describe('ComponentViewer', () => { onDidCollapseElement: jest.fn(callback => collapseCallback = callback), }); - // Set up component viewer parts + // Set up test class parts const controller = createController(); const tracker = makeTracker(); await controller.activate(tracker as unknown as GDBTargetDebugTracker); From a57c5dffd4e7eed2397c6cb319432d0b94aee3a9 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 18:39:16 +0100 Subject: [PATCH 11/47] tree data provider factory Signed-off-by: Jens Reinecke --- .../component-viewer-parts.factory.ts | 36 +++++++++++++++++++ .../test/unit/component-viewer-base.test.ts | 36 ++++++------------- 2 files changed, 47 insertions(+), 25 deletions(-) create mode 100644 src/views/component-viewer/__test__/component-viewer-parts.factory.ts diff --git a/src/views/component-viewer/__test__/component-viewer-parts.factory.ts b/src/views/component-viewer/__test__/component-viewer-parts.factory.ts new file mode 100644 index 00000000..da6addf7 --- /dev/null +++ b/src/views/component-viewer/__test__/component-viewer-parts.factory.ts @@ -0,0 +1,36 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +import * as vscode from 'vscode'; +import { ComponentViewerTreeDataProvider } from '../component-viewer-tree-view'; +import type { ScvdGuiInterface } from '../model/scvd-gui-interface'; + +export function treeDataProviderFactory(): jest.Mocked { + const eventEmitter = new vscode.EventEmitter(); + const provider: Partial> = { + onDidChangeTreeData: jest.fn().mockReturnValue(eventEmitter.event), + onWillStopSession: jest.fn(), + setElementExpanded: jest.fn(), + getTreeItem: jest.fn(), + resolveTreeItem: jest.fn(), + getChildren: jest.fn(), + setRoots: jest.fn(), + clear: jest.fn(), + refresh: jest.fn().mockImplementation(() => eventEmitter.fire()), + }; + return provider as unknown as jest.Mocked; +}; diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index d7261938..3c858e2d 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -24,24 +24,13 @@ import type { GDBTargetDebugTracker } from '../../../../debug-session'; import type { GDBTargetDebugSession, TargetState } from '../../../../debug-session/gdbtarget-debug-session'; import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; +import { treeDataProviderFactory } from '../../__test__/component-viewer-parts.factory'; import { ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; -const treeProviderFactory = jest.fn(() => ({ - setRoots: jest.fn(), - clear: jest.fn(), - refresh: jest.fn(), - onWillStopSession: jest.fn(), - setElementExpanded: jest.fn(), -})); - -jest.mock('../../component-viewer-tree-view', () => ({ - ComponentViewerTreeDataProvider: jest.fn(() => treeProviderFactory()), -})); - const instanceFactory = jest.fn(() => ({ readModel: jest.fn().mockResolvedValue(undefined), update: jest.fn().mockResolvedValue(undefined), @@ -69,8 +58,6 @@ jest.mock('../../../../logger', () => ({ }, })); -jest.mock('../../../../debug-session', () => ({})); - function asMockedFunction( fn: (...args: Args) => Return ): jest.MockedFunction<(...args: Args) => Return> { @@ -137,7 +124,7 @@ type TrackerCallbacks = { const createController = ( context: vscode.ExtensionContext = extensionContextFactory(), - provider: ComponentViewerTreeDataProvider | ReturnType = treeProviderFactory() + provider: ComponentViewerTreeDataProvider = treeDataProviderFactory() ): TestClass => new TestClass( context, provider as ComponentViewerTreeDataProvider @@ -470,7 +457,7 @@ describe('ComponentViewerBase', () => { it('updates instances when active session and instances are present', async () => { const context = extensionContextFactory(); - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(context, provider); const debugSpy = jest.spyOn(componentViewerLogger, 'debug'); @@ -507,7 +494,7 @@ describe('ComponentViewerBase', () => { }); it('skips gui tree updates when an instance returns no gui tree', async () => { - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(extensionContextFactory(), provider); const updateInstances = getUpdateInstances(controller); @@ -523,7 +510,7 @@ describe('ComponentViewerBase', () => { }); it('updates only instances for the active session', async () => { - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(extensionContextFactory(), provider); const sessionA = makeSession('s1', [], 'stopped'); @@ -556,7 +543,7 @@ describe('ComponentViewerBase', () => { }); it('skips updating a locked instance and marks root as locked', async () => { - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(extensionContextFactory(), provider); (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); @@ -619,7 +606,7 @@ describe('ComponentViewerBase', () => { it('schedules an update when unlocking a locked instance', () => { const controller = createController(extensionContextFactory()); - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); (controller as unknown as { _componentViewerTreeDataProvider: typeof provider })._componentViewerTreeDataProvider = provider; const root = makeGuiNode('root'); @@ -641,7 +628,7 @@ describe('ComponentViewerBase', () => { it('does not schedule an update when locking an unlocked instance', () => { const controller = createController(extensionContextFactory()); - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); (controller as unknown as { _componentViewerTreeDataProvider: typeof provider })._componentViewerTreeDataProvider = provider; const root = makeGuiNode('root'); @@ -703,7 +690,7 @@ describe('ComponentViewerBase', () => { }); it('skips lock operations when gui trees are missing', () => { - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(extensionContextFactory(), provider); const instMissingTree = instanceFactory(); @@ -716,7 +703,7 @@ describe('ComponentViewerBase', () => { }); it('returns early when gui tree disappears after toggling lock', () => { - const provider = treeProviderFactory(); + const provider = treeDataProviderFactory(); const controller = createController(extensionContextFactory(), provider); const root = makeGuiNode('root'); @@ -919,8 +906,7 @@ describe('ComponentViewerBase', () => { expect(collapseCallback!).toBeDefined(); // Setup spy on expected method calls when elements are expanded/collapsed - const provider = controller as unknown as { _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider }; - const setElementExpandedSpy = jest.spyOn(provider._componentViewerTreeDataProvider, 'setElementExpanded'); + const setElementExpandedSpy = jest.spyOn(controller['_componentViewerTreeDataProvider'], 'setElementExpanded'); // Simulate expanding an element const element = makeGuiNode('element'); From 9f3043f5c6c851a19de8fd936c829ec002675689 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 18:53:32 +0100 Subject: [PATCH 12/47] Reduce redundant test code Signed-off-by: Jens Reinecke --- .../test/unit/component-viewer-base.test.ts | 100 ++---------------- 1 file changed, 11 insertions(+), 89 deletions(-) diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 3c858e2d..73fdb12a 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -131,8 +131,17 @@ const createController = ( ); describe('ComponentViewerBase', () => { + let context: vscode.ExtensionContext; + let tracker: TrackerCallbacks; + let provider: ComponentViewerTreeDataProvider; + let controller: TestClass; + beforeEach(async () => { jest.clearAllMocks(); + context = extensionContextFactory(); + provider = treeDataProviderFactory(); + tracker = makeTracker(); + controller = createController(context, provider); // Extend registered commands for test class. const defaultMockedCommands = await vscode.commands.getCommands(); asMockedFunction(vscode.commands.getCommands).mockResolvedValue([ @@ -200,10 +209,6 @@ describe('ComponentViewerBase', () => { }); it('activates tree provider and registers tracker events', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); - const activationResult = await controller.activate(tracker as unknown as GDBTargetDebugTracker); expect(activationResult).toBe(true); @@ -218,10 +223,6 @@ describe('ComponentViewerBase', () => { }); it('should fail to activate the test class tree data provider if view is not correctly loaded', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); - // Clear test class commands to simulate view not correctly loaded. // Ensure to override the mock only once to not permanently change the global mock implementation for other tests. (vscode.commands.getCommands as jest.Mock).mockResolvedValueOnce([ @@ -233,9 +234,6 @@ describe('ComponentViewerBase', () => { }); it('skips reading scvd files when session or cbuild-run is missing', async () => { - const controller = createController(); - const tracker = makeTracker(); - const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, undefined); @@ -252,8 +250,6 @@ describe('ComponentViewerBase', () => { }); it('skips reading when no scvd files are listed', async () => { - const controller = createController(); - const tracker = makeTracker(); const session = makeSession('s1', []); const readScvdFiles = getReadScvdFiles(controller); @@ -263,9 +259,6 @@ describe('ComponentViewerBase', () => { }); it('reads scvd files when active session is set', async () => { - const context = extensionContextFactory(); - const controller = createController(context); - const tracker = makeTracker(); const session = makeSession('s1', ['a.scvd', 'b.scvd']); (controller as unknown as { _activeSession?: Session })._activeSession = session; @@ -278,8 +271,6 @@ describe('ComponentViewerBase', () => { }); it('skips reading scvd files when no active session is set', async () => { - const controller = createController(); - const tracker = makeTracker(); const session = makeSession('s1', ['a.scvd']); const readScvdFiles = getReadScvdFiles(controller); @@ -290,8 +281,6 @@ describe('ComponentViewerBase', () => { }); it('logs and shows error when scvd read fails', async () => { - const controller = createController(extensionContextFactory()); - const tracker = makeTracker(); const session = makeSession('s1', ['a.scvd']); (controller as unknown as { _activeSession?: Session })._activeSession = session; @@ -320,8 +309,6 @@ describe('ComponentViewerBase', () => { }); it('returns undefined when cbuild run contains no scvd instances', async () => { - const controller = createController(); - const tracker = makeTracker(); const session = makeSession('s1', []); const readScvdFiles = jest.fn().mockResolvedValue(undefined); @@ -338,14 +325,8 @@ describe('ComponentViewerBase', () => { }); it('handles tracker events and updates sessions', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); await controller.activate(tracker as unknown as GDBTargetDebugTracker); - // Bypass 'private' qualifier for test purposes. Do NOT do this in production code! - const provider = controller['_componentViewerTreeDataProvider']; - const session = makeSession('s1', ['a.scvd']); const otherSession = makeSession('s2', []); @@ -376,13 +357,8 @@ describe('ComponentViewerBase', () => { }); it('does not reset instances when the same session reconnects', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); await controller.activate(tracker as unknown as GDBTargetDebugTracker); - // Bypass 'private' qualifier for test purposes. Do NOT do this in production code! - const provider = controller['_componentViewerTreeDataProvider']; const session: Session = { session: { id: 's1' }, getCbuildRun: async () => undefined, @@ -399,7 +375,6 @@ describe('ComponentViewerBase', () => { }); it('clears all instances after all sessions stop', async () => { - const controller = createController(); const sessionA = makeSession('s1', [], 'stopped'); const sessionB = makeSession('s2', [], 'stopped'); @@ -418,7 +393,6 @@ describe('ComponentViewerBase', () => { }); it('updates instances on stack item change', async () => { - const controller = createController(); const sessionA = makeSession('s1', [], 'stopped'); (controller as unknown as { _activeSession?: Session })._activeSession = sessionA; @@ -435,7 +409,6 @@ describe('ComponentViewerBase', () => { }); it('does not update active session when stack item matches the active session', async () => { - const controller = createController(); const sessionA = makeSession('s1'); const updateSpy = jest.fn(); @@ -456,9 +429,6 @@ describe('ComponentViewerBase', () => { }); it('updates instances when active session and instances are present', async () => { - const context = extensionContextFactory(); - const provider = treeDataProviderFactory(); - const controller = createController(context, provider); const debugSpy = jest.spyOn(componentViewerLogger, 'debug'); const updateInstances = getUpdateInstances(controller); @@ -466,7 +436,7 @@ describe('ComponentViewerBase', () => { (controller as unknown as { _activeSession?: Session | undefined })._activeSession = undefined; await updateInstances('stackTrace'); expect(provider.clear).toHaveBeenCalledTimes(1); - provider.clear.mockClear(); + (provider.clear as jest.Mock).mockClear(); (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); (controller as unknown as { _instances: unknown[] })._instances = []; @@ -494,9 +464,6 @@ describe('ComponentViewerBase', () => { }); it('skips gui tree updates when an instance returns no gui tree', async () => { - const provider = treeDataProviderFactory(); - const controller = createController(extensionContextFactory(), provider); - const updateInstances = getUpdateInstances(controller); (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); const instance = instanceFactory(); @@ -510,9 +477,6 @@ describe('ComponentViewerBase', () => { }); it('updates only instances for the active session', async () => { - const provider = treeDataProviderFactory(); - const controller = createController(extensionContextFactory(), provider); - const sessionA = makeSession('s1', [], 'stopped'); (controller as unknown as { _activeSession?: Session | undefined })._activeSession = sessionA; @@ -543,9 +507,6 @@ describe('ComponentViewerBase', () => { }); it('skips updating a locked instance and marks root as locked', async () => { - const provider = treeDataProviderFactory(); - const controller = createController(extensionContextFactory(), provider); - (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); const rootUnlocked = makeGuiNode('u'); @@ -574,14 +535,7 @@ describe('ComponentViewerBase', () => { }); it('toggles lock state when lock command is invoked for a node in an instance tree', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); await controller.activate(tracker as unknown as GDBTargetDebugTracker); - - // Bypass 'private' qualifier for test purposes. Do NOT do this in production code! - const provider = controller['_componentViewerTreeDataProvider']; - const root = makeGuiNode('root', [makeGuiNode('child')]); const inst = instanceFactory(); inst.getGuiTree = jest.fn(() => [root]); @@ -605,8 +559,6 @@ describe('ComponentViewerBase', () => { }); it('schedules an update when unlocking a locked instance', () => { - const controller = createController(extensionContextFactory()); - const provider = treeDataProviderFactory(); (controller as unknown as { _componentViewerTreeDataProvider: typeof provider })._componentViewerTreeDataProvider = provider; const root = makeGuiNode('root'); @@ -627,8 +579,6 @@ describe('ComponentViewerBase', () => { }); it('does not schedule an update when locking an unlocked instance', () => { - const controller = createController(extensionContextFactory()); - const provider = treeDataProviderFactory(); (controller as unknown as { _componentViewerTreeDataProvider: typeof provider })._componentViewerTreeDataProvider = provider; const root = makeGuiNode('root'); @@ -648,9 +598,6 @@ describe('ComponentViewerBase', () => { }); it('toggles periodic updates via commands', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); await controller.activate(tracker as unknown as GDBTargetDebugTracker); const registerCommandMock = asMockedFunction(vscode.commands.registerCommand); @@ -674,9 +621,6 @@ describe('ComponentViewerBase', () => { }); it('invokes unlock handler and skips lock when no matching instance exists', async () => { - const context = extensionContextFactory(); - const tracker = makeTracker(); - const controller = createController(context); await controller.activate(tracker as unknown as GDBTargetDebugTracker); const registerCommandMock = asMockedFunction(vscode.commands.registerCommand); @@ -690,9 +634,6 @@ describe('ComponentViewerBase', () => { }); it('skips lock operations when gui trees are missing', () => { - const provider = treeDataProviderFactory(); - const controller = createController(extensionContextFactory(), provider); - const instMissingTree = instanceFactory(); instMissingTree.getGuiTree = jest.fn(() => undefined); (controller as unknown as { _instances: unknown[] })._instances = [{ componentViewerInstance: instMissingTree, lockState: false, sessionId: 's1', dirtyWhileLocked: false }]; @@ -703,9 +644,6 @@ describe('ComponentViewerBase', () => { }); it('returns early when gui tree disappears after toggling lock', () => { - const provider = treeDataProviderFactory(); - const controller = createController(extensionContextFactory(), provider); - const root = makeGuiNode('root'); const inst = instanceFactory(); inst.getGuiTree = jest.fn() @@ -721,7 +659,6 @@ describe('ComponentViewerBase', () => { it('runs a debounced update when scheduling multiple times', async () => { jest.useFakeTimers(); - const controller = createController(); const runUpdate = jest .spyOn(controller as unknown as { runUpdate: (reason: UpdateReason) => Promise }, 'runUpdate') .mockResolvedValue(undefined); @@ -737,7 +674,6 @@ describe('ComponentViewerBase', () => { }); it('does nothing when an update is already running', async () => { - const controller = createController(); (controller as unknown as { _runningUpdate: boolean })._runningUpdate = true; const updateInstances = jest.spyOn(controller as unknown as { updateInstances: (reason: UpdateReason) => Promise }, 'updateInstances'); const runUpdate = getRunUpdate(controller); @@ -748,7 +684,6 @@ describe('ComponentViewerBase', () => { }); it('runs update immediately when idle', async () => { - const controller = createController(); (controller as unknown as { _pendingUpdate: boolean })._pendingUpdate = true; (controller as unknown as { _runningUpdate: boolean })._runningUpdate = false; const updateInstances = jest @@ -761,7 +696,6 @@ describe('ComponentViewerBase', () => { }); it('swallows errors during a coalescing update', async () => { - const controller = createController(); (controller as unknown as { _pendingUpdate: boolean })._pendingUpdate = true; (controller as unknown as { _runningUpdate: boolean })._runningUpdate = false; const updateInstances = jest.spyOn(controller as unknown as { updateInstances: (reason: UpdateReason) => Promise }, 'updateInstances') @@ -775,7 +709,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when no instances', () => { - const controller = createController(); const session = makeSession('s1', [], 'stopped'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = []; @@ -785,7 +718,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when target state is unknown', () => { - const controller = createController(); const session = makeSession('s1', [], 'unknown'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ @@ -797,7 +729,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when running and refresh disabled', () => { - const controller = createController(); const session = makeSession('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; @@ -811,7 +742,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when running without access', () => { - const controller = createController(); const session = makeSession('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = false; @@ -825,7 +755,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns true when running with refresh and access', () => { - const controller = createController(); const session = makeSession('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; @@ -839,7 +768,6 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns true when stopped', () => { - const controller = createController(); const session = makeSession('s1', [], 'stopped'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ @@ -851,9 +779,6 @@ describe('ComponentViewerBase', () => { }); it('silently skips periodic refresh if event comes for session not in front', async () => { - // Set up test class parts - const controller = createController(); - const tracker = makeTracker(); await controller.activate(tracker as unknown as GDBTargetDebugTracker); // Set up two sessions @@ -896,9 +821,6 @@ describe('ComponentViewerBase', () => { onDidCollapseElement: jest.fn(callback => collapseCallback = callback), }); - // Set up test class parts - const controller = createController(); - const tracker = makeTracker(); await controller.activate(tracker as unknown as GDBTargetDebugTracker); // Ensure callbacks are set @@ -906,7 +828,7 @@ describe('ComponentViewerBase', () => { expect(collapseCallback!).toBeDefined(); // Setup spy on expected method calls when elements are expanded/collapsed - const setElementExpandedSpy = jest.spyOn(controller['_componentViewerTreeDataProvider'], 'setElementExpanded'); + const setElementExpandedSpy = jest.spyOn(provider, 'setElementExpanded'); // Simulate expanding an element const element = makeGuiNode('element'); From cc0ab2a5d6748254e4b69330237a6a3c516d83ef Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 19:13:03 +0100 Subject: [PATCH 13/47] Factory refactoring Signed-off-by: Jens Reinecke --- .../__test__/debug-session.factory.ts | 89 +++++++++++++ .../test/unit/component-viewer-base.test.ts | 121 ++++-------------- 2 files changed, 115 insertions(+), 95 deletions(-) create mode 100644 src/views/component-viewer/__test__/debug-session.factory.ts diff --git a/src/views/component-viewer/__test__/debug-session.factory.ts b/src/views/component-viewer/__test__/debug-session.factory.ts new file mode 100644 index 00000000..4cd7116a --- /dev/null +++ b/src/views/component-viewer/__test__/debug-session.factory.ts @@ -0,0 +1,89 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// generated with AI + +import { GDBTargetDebugTracker, TargetState } from '../../../debug-session'; + +export type OnRefreshCallback = (session: Session) => void; + +export type Session = { + session: { id: string }; + getCbuildRun: () => Promise<{ getScvdFilePaths: () => string[] } | undefined>; + getPname: () => Promise; + refreshTimer: { onRefresh: (cb: OnRefreshCallback) => void }; + targetState?: TargetState; + canAccessWhileRunning?: boolean; +}; + +export type TrackerCallbacks = { + onWillStopSession: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; + onConnected: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; + onDidChangeActiveDebugSession: (cb: (session: Session | undefined) => Promise) => { dispose: jest.Mock }; + onStackTrace: (cb: (session: { session: Session }) => Promise) => { dispose: jest.Mock }; + onDidChangeActiveStackItem: (cb: (session: { session: Session }) => Promise) => { dispose: jest.Mock }; + onWillStartSession: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; + callbacks: Partial<{ + willStop: (session: Session) => Promise; + connected: (session: Session) => Promise; + activeSession: (session: Session | undefined) => Promise; + stackTrace: (session: { session: Session }) => Promise; + activeStackItem: (session: { session: Session }) => Promise; + willStart: (session: Session) => Promise; + }>; +}; + +export const trackerFactory = (): TrackerCallbacks => { + const callbacks: TrackerCallbacks['callbacks'] = {}; + return { + callbacks, + onWillStopSession: (cb) => { + callbacks.willStop = cb; + return { dispose: jest.fn() }; + }, + onConnected: (cb) => { + callbacks.connected = cb; + return { dispose: jest.fn() }; + }, + onDidChangeActiveDebugSession: (cb) => { + callbacks.activeSession = cb; + return { dispose: jest.fn() }; + }, + onStackTrace: (cb) => { + callbacks.stackTrace = cb; + return { dispose: jest.fn() }; + }, + onDidChangeActiveStackItem: (cb) => { + callbacks.activeStackItem = cb; + return { dispose: jest.fn() }; + }, + onWillStartSession: (cb) => { + callbacks.willStart = cb; + return { dispose: jest.fn() }; + }, + }; +}; + +export const debugTrackerFactory = () => trackerFactory() as unknown as GDBTargetDebugTracker; + +export const debugSessionFactory = (id: string, paths: string[] = [], targetState: Session['targetState'] = 'unknown'): Session => ({ + session: { id }, + getCbuildRun: async () => ({ getScvdFilePaths: () => paths }), + getPname: async () => undefined, + refreshTimer: { + onRefresh: jest.fn(), + }, + targetState, +}); diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index 73fdb12a..dfea3047 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -21,7 +21,7 @@ import * as vscode from 'vscode'; import type { GDBTargetDebugTracker } from '../../../../debug-session'; -import type { GDBTargetDebugSession, TargetState } from '../../../../debug-session/gdbtarget-debug-session'; +import type { GDBTargetDebugSession } from '../../../../debug-session/gdbtarget-debug-session'; import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; import { treeDataProviderFactory } from '../../__test__/component-viewer-parts.factory'; @@ -29,6 +29,7 @@ import { ComponentViewerInstancesWrapper, UpdateReason } from '../../component-v import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; +import { debugSessionFactory, trackerFactory, OnRefreshCallback, Session, TrackerCallbacks } from '../../__test__/debug-session.factory'; const instanceFactory = jest.fn(() => ({ @@ -92,36 +93,8 @@ const getReadScvdFiles = (controller: TestClass) => (controller as unknown as { readScvdFiles: (t: TrackerCallbacks, s?: Session) => Promise }).readScvdFiles.bind(controller); // Local test mocks - -type OnRefreshCallback = (session: Session) => void; type ExpansionEventCallback = (event: vscode.TreeViewExpansionEvent) => void; -type Session = { - session: { id: string }; - getCbuildRun: () => Promise<{ getScvdFilePaths: () => string[] } | undefined>; - getPname: () => Promise; - refreshTimer: { onRefresh: (cb: OnRefreshCallback) => void }; - targetState?: TargetState; - canAccessWhileRunning?: boolean; -}; - -type TrackerCallbacks = { - onWillStopSession: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; - onConnected: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; - onDidChangeActiveDebugSession: (cb: (session: Session | undefined) => Promise) => { dispose: jest.Mock }; - onStackTrace: (cb: (session: { session: Session }) => Promise) => { dispose: jest.Mock }; - onDidChangeActiveStackItem: (cb: (session: { session: Session }) => Promise) => { dispose: jest.Mock }; - onWillStartSession: (cb: (session: Session) => Promise) => { dispose: jest.Mock }; - callbacks: Partial<{ - willStop: (session: Session) => Promise; - connected: (session: Session) => Promise; - activeSession: (session: Session | undefined) => Promise; - stackTrace: (session: { session: Session }) => Promise; - activeStackItem: (session: { session: Session }) => Promise; - willStart: (session: Session) => Promise; - }>; -}; - const createController = ( context: vscode.ExtensionContext = extensionContextFactory(), provider: ComponentViewerTreeDataProvider = treeDataProviderFactory() @@ -140,7 +113,7 @@ describe('ComponentViewerBase', () => { jest.clearAllMocks(); context = extensionContextFactory(); provider = treeDataProviderFactory(); - tracker = makeTracker(); + tracker = trackerFactory(); controller = createController(context, provider); // Extend registered commands for test class. const defaultMockedCommands = await vscode.commands.getCommands(); @@ -166,48 +139,6 @@ describe('ComponentViewerBase', () => { hasGuiChildren: () => children.length > 0, }); - - const makeTracker = (): TrackerCallbacks => { - const callbacks: TrackerCallbacks['callbacks'] = {}; - return { - callbacks, - onWillStopSession: (cb) => { - callbacks.willStop = cb; - return { dispose: jest.fn() }; - }, - onConnected: (cb) => { - callbacks.connected = cb; - return { dispose: jest.fn() }; - }, - onDidChangeActiveDebugSession: (cb) => { - callbacks.activeSession = cb; - return { dispose: jest.fn() }; - }, - onStackTrace: (cb) => { - callbacks.stackTrace = cb; - return { dispose: jest.fn() }; - }, - onDidChangeActiveStackItem: (cb) => { - callbacks.activeStackItem = cb; - return { dispose: jest.fn() }; - }, - onWillStartSession: (cb) => { - callbacks.willStart = cb; - return { dispose: jest.fn() }; - }, - }; - }; - - const makeSession = (id: string, paths: string[] = [], targetState: Session['targetState'] = 'unknown'): Session => ({ - session: { id }, - getCbuildRun: async () => ({ getScvdFilePaths: () => paths }), - getPname: async () => undefined, - refreshTimer: { - onRefresh: jest.fn(), - }, - targetState, - }); - it('activates tree provider and registers tracker events', async () => { const activationResult = await controller.activate(tracker as unknown as GDBTargetDebugTracker); @@ -250,7 +181,7 @@ describe('ComponentViewerBase', () => { }); it('skips reading when no scvd files are listed', async () => { - const session = makeSession('s1', []); + const session = debugSessionFactory('s1', []); const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); @@ -259,7 +190,7 @@ describe('ComponentViewerBase', () => { }); it('reads scvd files when active session is set', async () => { - const session = makeSession('s1', ['a.scvd', 'b.scvd']); + const session = debugSessionFactory('s1', ['a.scvd', 'b.scvd']); (controller as unknown as { _activeSession?: Session })._activeSession = session; const readScvdFiles = getReadScvdFiles(controller); @@ -271,7 +202,7 @@ describe('ComponentViewerBase', () => { }); it('skips reading scvd files when no active session is set', async () => { - const session = makeSession('s1', ['a.scvd']); + const session = debugSessionFactory('s1', ['a.scvd']); const readScvdFiles = getReadScvdFiles(controller); await readScvdFiles(tracker, session); @@ -281,7 +212,7 @@ describe('ComponentViewerBase', () => { }); it('logs and shows error when scvd read fails', async () => { - const session = makeSession('s1', ['a.scvd']); + const session = debugSessionFactory('s1', ['a.scvd']); (controller as unknown as { _activeSession?: Session })._activeSession = session; const readModelError = new Error('boom'); @@ -309,7 +240,7 @@ describe('ComponentViewerBase', () => { }); it('returns undefined when cbuild run contains no scvd instances', async () => { - const session = makeSession('s1', []); + const session = debugSessionFactory('s1', []); const readScvdFiles = jest.fn().mockResolvedValue(undefined); (controller as unknown as { readScvdFiles: typeof readScvdFiles }).readScvdFiles = readScvdFiles; @@ -327,8 +258,8 @@ describe('ComponentViewerBase', () => { it('handles tracker events and updates sessions', async () => { await controller.activate(tracker as unknown as GDBTargetDebugTracker); - const session = makeSession('s1', ['a.scvd']); - const otherSession = makeSession('s2', []); + const session = debugSessionFactory('s1', ['a.scvd']); + const otherSession = debugSessionFactory('s2', []); await tracker.callbacks.willStart?.(session); await tracker.callbacks.connected?.(session); @@ -375,8 +306,8 @@ describe('ComponentViewerBase', () => { }); it('clears all instances after all sessions stop', async () => { - const sessionA = makeSession('s1', [], 'stopped'); - const sessionB = makeSession('s2', [], 'stopped'); + const sessionA = debugSessionFactory('s1', [], 'stopped'); + const sessionB = debugSessionFactory('s2', [], 'stopped'); (controller as unknown as { _instances: unknown[] })._instances = [ { componentViewerInstance: instanceFactory(), lockState: false, sessionId: 's1', dirtyWhileLocked: false }, @@ -393,7 +324,7 @@ describe('ComponentViewerBase', () => { }); it('updates instances on stack item change', async () => { - const sessionA = makeSession('s1', [], 'stopped'); + const sessionA = debugSessionFactory('s1', [], 'stopped'); (controller as unknown as { _activeSession?: Session })._activeSession = sessionA; @@ -409,7 +340,7 @@ describe('ComponentViewerBase', () => { }); it('does not update active session when stack item matches the active session', async () => { - const sessionA = makeSession('s1'); + const sessionA = debugSessionFactory('s1'); const updateSpy = jest.fn(); (controller as unknown as { _activeSession?: Session })._activeSession = sessionA; @@ -438,7 +369,7 @@ describe('ComponentViewerBase', () => { expect(provider.clear).toHaveBeenCalledTimes(1); (provider.clear as jest.Mock).mockClear(); - (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); + (controller as unknown as { _activeSession?: Session | undefined })._activeSession = debugSessionFactory('s1', [], 'stopped'); (controller as unknown as { _instances: unknown[] })._instances = []; await updateInstances('stackTrace'); expect(provider.clear).not.toHaveBeenCalled(); @@ -465,7 +396,7 @@ describe('ComponentViewerBase', () => { it('skips gui tree updates when an instance returns no gui tree', async () => { const updateInstances = getUpdateInstances(controller); - (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); + (controller as unknown as { _activeSession?: Session | undefined })._activeSession = debugSessionFactory('s1', [], 'stopped'); const instance = instanceFactory(); instance.getGuiTree = jest.fn(() => undefined); (controller as unknown as { _instances: unknown[] })._instances = [ @@ -477,7 +408,7 @@ describe('ComponentViewerBase', () => { }); it('updates only instances for the active session', async () => { - const sessionA = makeSession('s1', [], 'stopped'); + const sessionA = debugSessionFactory('s1', [], 'stopped'); (controller as unknown as { _activeSession?: Session | undefined })._activeSession = sessionA; const rootA = { ...makeGuiNode('rootA'), clear: jest.fn() } as ScvdGuiInterface & { clear: jest.Mock }; @@ -507,7 +438,7 @@ describe('ComponentViewerBase', () => { }); it('skips updating a locked instance and marks root as locked', async () => { - (controller as unknown as { _activeSession?: Session | undefined })._activeSession = makeSession('s1', [], 'stopped'); + (controller as unknown as { _activeSession?: Session | undefined })._activeSession = debugSessionFactory('s1', [], 'stopped'); const rootUnlocked = makeGuiNode('u'); const rootLocked = makeGuiNode('l'); @@ -709,7 +640,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when no instances', () => { - const session = makeSession('s1', [], 'stopped'); + const session = debugSessionFactory('s1', [], 'stopped'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = []; @@ -718,7 +649,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when target state is unknown', () => { - const session = makeSession('s1', [], 'unknown'); + const session = debugSessionFactory('s1', [], 'unknown'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ { componentViewerInstance: instanceFactory() as unknown as ComponentViewerInstancesWrapper['componentViewerInstance'], lockState: false, sessionId: 's1', dirtyWhileLocked: false }, @@ -729,7 +660,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when running and refresh disabled', () => { - const session = makeSession('s1', [], 'running'); + const session = debugSessionFactory('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ @@ -742,7 +673,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns false when running without access', () => { - const session = makeSession('s1', [], 'running'); + const session = debugSessionFactory('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = false; (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ @@ -755,7 +686,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns true when running with refresh and access', () => { - const session = makeSession('s1', [], 'running'); + const session = debugSessionFactory('s1', [], 'running'); (session as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ @@ -768,7 +699,7 @@ describe('ComponentViewerBase', () => { }); it('shouldUpdateInstances returns true when stopped', () => { - const session = makeSession('s1', [], 'stopped'); + const session = debugSessionFactory('s1', [], 'stopped'); (controller as unknown as { _instances: ComponentViewerInstancesWrapper[] })._instances = [ { componentViewerInstance: instanceFactory() as unknown as ComponentViewerInstancesWrapper['componentViewerInstance'], lockState: false, sessionId: 's1', dirtyWhileLocked: false }, @@ -782,8 +713,8 @@ describe('ComponentViewerBase', () => { await controller.activate(tracker as unknown as GDBTargetDebugTracker); // Set up two sessions - const sessionFront = makeSession('s1', [], 'running'); - const sessionOther = makeSession('s2', [], 'running'); + const sessionFront = debugSessionFactory('s1', [], 'running'); + const sessionOther = debugSessionFactory('s2', [], 'running'); (sessionFront as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; (sessionOther as unknown as { canAccessWhileRunning: boolean }).canAccessWhileRunning = true; From f0d6e54b0923ad6f5ddfde28c9f013585b4456ce Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 19:13:15 +0100 Subject: [PATCH 14/47] Component Viewer tests Signed-off-by: Jens Reinecke --- .../test/unit/component-viewer.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/views/component-viewer/test/unit/component-viewer.test.ts diff --git a/src/views/component-viewer/test/unit/component-viewer.test.ts b/src/views/component-viewer/test/unit/component-viewer.test.ts new file mode 100644 index 00000000..bc91e4d4 --- /dev/null +++ b/src/views/component-viewer/test/unit/component-viewer.test.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { extensionContextFactory } from '../../../../__test__/vscode.factory'; +import { treeDataProviderFactory } from '../../__test__/component-viewer-parts.factory'; +import { debugTrackerFactory } from '../../__test__/debug-session.factory'; +import { ComponentViewer } from '../../component-viewer'; + +/** + * Unit test for ComponentViewer class. + */ + + +describe('ComponentViewer', () => { + let componentViewer: ComponentViewer; + + beforeEach(() => { + const context = extensionContextFactory(); + const componentViewerTreeDataProvider = treeDataProviderFactory(); + componentViewer = new ComponentViewer(context, componentViewerTreeDataProvider); + }); + + it('correctly activates', async () => { + const tracker = debugTrackerFactory(); + expect(componentViewer).toBeDefined(); + await componentViewer.activate(tracker); + }); + +}); From 290a15fff9fa6589e2cf72b6ae1d9f7a3aaeadd0 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 19:33:03 +0100 Subject: [PATCH 15/47] Fix missing clear on deactivate Signed-off-by: Jens Reinecke --- src/desktop/extension.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index db46e607..17a81664 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -104,5 +104,8 @@ export const deactivate = async (): Promise => { if (componentViewerTreeDataProvider) { componentViewerTreeDataProvider.clear(); } + if (corePeripheralsTreeDataProvider) { + corePeripheralsTreeDataProvider.clear(); + } logger.debug('CMSIS Debugger deactivated'); }; From 6f1eaaeae6898759a9380ad5b5eef3f3e5cfa2d7 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 19:33:21 +0100 Subject: [PATCH 16/47] extension.ts tests for core peripherals Signed-off-by: Jens Reinecke --- src/desktop/extension.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/desktop/extension.test.ts b/src/desktop/extension.test.ts index 05fbb265..ad989b58 100644 --- a/src/desktop/extension.test.ts +++ b/src/desktop/extension.test.ts @@ -24,6 +24,9 @@ import { LiveWatchTreeDataProvider } from '../views/live-watch/live-watch'; describe('extension', () => { describe('activate', () => { + const liveWatchCommands = [ 'cmsis-debugger.liveWatch.open', 'cmsis-debugger.liveWatch.focus' ]; + const componentViewerCommands = [ 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus' ]; + const corePeripheralsCommands = [ 'cmsis-debugger.corePeripherals.open', 'cmsis-debugger.corePeripherals.focus' ]; it('activates extension without asking to reload', async () => { const loggerSpy = jest.spyOn(logger, 'debug'); @@ -33,12 +36,15 @@ describe('extension', () => { }); it.each([ - { missingView: 'live watch view', availableCommands: [ 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus'] }, - { missingView: 'component viewer', availableCommands: [ 'cmsis-debugger.liveWatch.open', 'cmsis-debugger.liveWatch.focus'] } + { missingView: 'Live Watch view', availableCommands: [ ...componentViewerCommands, ...corePeripheralsCommands ] }, + { missingView: 'Component Viewer', availableCommands: [ ...liveWatchCommands, ...corePeripheralsCommands ] }, + { missingView: 'Core Peripherals', availableCommands: [ ...liveWatchCommands, ...componentViewerCommands ] } ])('activates extension and asks to reload because $missingView is not loaded', async ({ availableCommands }) => { const loggerSpy = jest.spyOn(logger, 'debug'); // Resolve once per each view in extension, do not permanently overload global mock (vscode.commands.getCommands as jest.Mock) + // Extend with each new view to match number of getCommands calls in activate function + .mockResolvedValueOnce(availableCommands) .mockResolvedValueOnce(availableCommands) .mockResolvedValueOnce(availableCommands); await activate(extensionContextFactory()); @@ -64,12 +70,12 @@ describe('extension', () => { describe('deactivate', () => { const loggerSpy = jest.spyOn(logger, 'debug'); - const componentViewerClearSpy = jest.spyOn(ComponentViewerTreeDataProvider.prototype, 'clear'); + const treeDataProviderClearSpy = jest.spyOn(ComponentViewerTreeDataProvider.prototype, 'clear'); const liveWatchDeactivateSpy = jest.spyOn(LiveWatchTreeDataProvider.prototype, 'deactivate'); beforeEach(() => { loggerSpy.mockClear(); - componentViewerClearSpy.mockClear(); + treeDataProviderClearSpy.mockClear(); liveWatchDeactivateSpy.mockClear(); }); @@ -77,7 +83,7 @@ describe('extension', () => { await activate(extensionContextFactory()); await deactivate(); expect(loggerSpy).toHaveBeenCalledWith('CMSIS Debugger deactivated'); - expect(componentViewerClearSpy).toHaveBeenCalled(); + expect(treeDataProviderClearSpy).toHaveBeenCalledTimes(2); // Component Viewer and Core Peripherals expect(liveWatchDeactivateSpy).toHaveBeenCalled(); }); From d6856a5994d7a17422725cf78d1e03efa32ab111 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 19:53:08 +0100 Subject: [PATCH 17/47] Make ComponentViewerBase abstract (avoid getScvdFilePaths() base implementation) Signed-off-by: Jens Reinecke --- .../component-viewer/component-viewer-base.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index 1abfccb0..c3376c6c 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -34,7 +34,7 @@ export interface ComponentViewerInstancesWrapper { dirtyWhileLocked: boolean; // Flag to indicate if an update was attempted while instance was locked, used to trigger an update when instance is unlocked } -export class ComponentViewerBase { +export abstract class ComponentViewerBase { private _activeSession: GDBTargetDebugSession | undefined; private _instances: ComponentViewerInstancesWrapper[] = []; private _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider; @@ -47,6 +47,15 @@ export class ComponentViewerBase { private _refreshTimerEnabled: boolean = true; private static readonly pendingUpdateDelayMs = 150; + /** + * Get SCVF file paths for a given debug session. Derived class implements to get SCVD files as needed + * for specific component viewer flavor. + * + * @param _session GDB target session to get SCVD Files for + * @returns promise to an array of SCVD file paths, or empty array if no SCVD files found + */ + protected abstract getScvdFilePaths(_session: GDBTargetDebugSession): Promise; + public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, @@ -149,17 +158,6 @@ export class ComponentViewerBase { this._componentViewerTreeDataProvider.refresh(); } - /** - * Get SCVF file paths for a given debug session. Derived class implements to get SCVD files as needed - * for specific component viewer flavor. - * - * @param _session GDB target session to get SCVD Files for - * @returns promise to an array of SCVD file paths, or empty array if no SCVD files found - */ - protected async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - return []; - } - protected async readScvdFiles(tracker: GDBTargetDebugTracker, session?: GDBTargetDebugSession): Promise { if (!session) { return; From 36cd23869f76f40c05526ba32941547e09babeea Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 20:20:39 +0100 Subject: [PATCH 18/47] Refactor for testability, revert making ComponentViewerBase abstract Signed-off-by: Jens Reinecke --- .../component-viewer/component-viewer-base.ts | 18 ++++----- .../component-viewer-scvd-collector.ts | 34 +++++++++++++++++ .../component-viewer/component-viewer.ts | 25 ++++--------- .../test/unit/component-viewer-base.test.ts | 23 ++++++++---- .../component-viewer-scvd-collector.test.ts | 30 +++++++++++++++ .../core-peripherals-scvd-collector.test.ts | 30 +++++++++++++++ .../core-peripherals-scvd-collector.ts | 37 +++++++++++++++++++ .../core-peripherals/core-peripherals.test.ts | 37 +++++++++++++++++++ .../core-peripherals/core-peripherals.ts | 28 ++++---------- 9 files changed, 206 insertions(+), 56 deletions(-) create mode 100644 src/views/component-viewer/component-viewer-scvd-collector.ts create mode 100644 src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts create mode 100644 src/views/core-peripherals/core-peripherals-scvd-collector.test.ts create mode 100644 src/views/core-peripherals/core-peripherals-scvd-collector.ts create mode 100644 src/views/core-peripherals/core-peripherals.test.ts diff --git a/src/views/component-viewer/component-viewer-base.ts b/src/views/component-viewer/component-viewer-base.ts index c3376c6c..f84e5026 100755 --- a/src/views/component-viewer/component-viewer-base.ts +++ b/src/views/component-viewer/component-viewer-base.ts @@ -25,6 +25,10 @@ import { perf, parsePerf } from './stats-config'; import { vscodeViewExists } from '../../vscode-utils'; import { EXTENSION_NAME, VIEW_PREFIX } from '../../manifest'; +export interface ScvdCollector { + getScvdFilePaths(session: GDBTargetDebugSession): Promise; +} + export type UpdateReason = 'sessionChanged' | 'refreshTimer' | 'stackTrace' | 'stackItemChanged' | 'unlockingInstance'; export interface ComponentViewerInstancesWrapper { @@ -34,7 +38,7 @@ export interface ComponentViewerInstancesWrapper { dirtyWhileLocked: boolean; // Flag to indicate if an update was attempted while instance was locked, used to trigger an update when instance is unlocked } -export abstract class ComponentViewerBase { +export class ComponentViewerBase { private _activeSession: GDBTargetDebugSession | undefined; private _instances: ComponentViewerInstancesWrapper[] = []; private _componentViewerTreeDataProvider: ComponentViewerTreeDataProvider; @@ -47,18 +51,10 @@ export abstract class ComponentViewerBase { private _refreshTimerEnabled: boolean = true; private static readonly pendingUpdateDelayMs = 150; - /** - * Get SCVF file paths for a given debug session. Derived class implements to get SCVD files as needed - * for specific component viewer flavor. - * - * @param _session GDB target session to get SCVD Files for - * @returns promise to an array of SCVD file paths, or empty array if no SCVD files found - */ - protected abstract getScvdFilePaths(_session: GDBTargetDebugSession): Promise; - public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, + protected readonly _scvdCollector: ScvdCollector, protected readonly _viewName: string, protected readonly _viewId: string ) { @@ -162,7 +158,7 @@ export abstract class ComponentViewerBase { if (!session) { return; } - const scvdFilesPaths = await this.getScvdFilePaths(session); + const scvdFilesPaths = await this._scvdCollector.getScvdFilePaths(session); if (scvdFilesPaths.length === 0) { return; } diff --git a/src/views/component-viewer/component-viewer-scvd-collector.ts b/src/views/component-viewer/component-viewer-scvd-collector.ts new file mode 100644 index 00000000..29d1425d --- /dev/null +++ b/src/views/component-viewer/component-viewer-scvd-collector.ts @@ -0,0 +1,34 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GDBTargetDebugSession } from '../../debug-session'; +import { ScvdCollector } from './component-viewer-base'; + +export class ComponentViewerScvdCollector implements ScvdCollector { + public async getScvdFilePaths(session: GDBTargetDebugSession): Promise { + const cbuildRunReader = await session.getCbuildRun(); + const pname = await session.getPname(); + if (!cbuildRunReader) { + return []; + } + // Get SCVD file paths from cbuild-run reader + const scvdFilesPaths: string [] = cbuildRunReader.getScvdFilePaths(undefined, pname); + if (scvdFilesPaths.length === 0) { + return []; + } + return scvdFilesPaths; + } +} diff --git a/src/views/component-viewer/component-viewer.ts b/src/views/component-viewer/component-viewer.ts index 9b6c46a2..4e20cf60 100644 --- a/src/views/component-viewer/component-viewer.ts +++ b/src/views/component-viewer/component-viewer.ts @@ -17,28 +17,19 @@ import * as vscode from 'vscode'; import { ComponentViewerBase } from './component-viewer-base'; import { ComponentViewerTreeDataProvider } from './component-viewer-tree-view'; -import { GDBTargetDebugSession } from '../../debug-session'; +import { ComponentViewerScvdCollector } from './component-viewer-scvd-collector'; export class ComponentViewer extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider ) { - super(context, componentViewerTreeDataProvider, 'Component Viewer', 'componentViewer'); + super( + context, + componentViewerTreeDataProvider, + new ComponentViewerScvdCollector(), + 'Component Viewer', + 'componentViewer' + ); } - - protected override async getScvdFilePaths(session: GDBTargetDebugSession): Promise { - const cbuildRunReader = await session.getCbuildRun(); - const pname = await session.getPname(); - if (!cbuildRunReader) { - return []; - } - // Get SCVD file paths from cbuild-run reader - const scvdFilesPaths: string [] = cbuildRunReader.getScvdFilePaths(undefined, pname); - if (scvdFilesPaths.length === 0) { - return []; - } - return scvdFilesPaths; - } - } diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index dfea3047..f5e171c3 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -25,7 +25,7 @@ import type { GDBTargetDebugSession } from '../../../../debug-session/gdbtarget- import { componentViewerLogger } from '../../../../logger'; import { extensionContextFactory } from '../../../../__test__/vscode.factory'; import { treeDataProviderFactory } from '../../__test__/component-viewer-parts.factory'; -import { ComponentViewerInstancesWrapper, UpdateReason } from '../../component-viewer-base'; +import { ComponentViewerInstancesWrapper, ScvdCollector, UpdateReason } from '../../component-viewer-base'; import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; @@ -65,18 +65,25 @@ function asMockedFunction( return fn as unknown as jest.MockedFunction<(...args: Args) => Return>; } +class TestClassScvdCollector implements ScvdCollector { + public async getScvdFilePaths(session: GDBTargetDebugSession): Promise { + // Lightweight implementation based on session logic + const cbuildRunReader = await session.getCbuildRun(); + return cbuildRunReader?.getScvdFilePaths() ?? []; + } +} + class TestClass extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider, ) { - super(context, componentViewerTreeDataProvider, 'Test Class', 'testClass'); - } - - protected override async getScvdFilePaths(session: GDBTargetDebugSession): Promise { - // Lightweight implementation based on session logic - const cbuildRunReader = await session.getCbuildRun(); - return cbuildRunReader?.getScvdFilePaths() ?? []; + super( + context, + componentViewerTreeDataProvider, + new TestClassScvdCollector(), + 'Test Class', + 'testClass'); } }; diff --git a/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts b/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts new file mode 100644 index 00000000..e30ed618 --- /dev/null +++ b/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentViewerScvdCollector } from '../../component-viewer-scvd-collector'; + +describe('ComponentViewerScvdCollector', () => { + let componentViewerScvdCollector: ComponentViewerScvdCollector; + + beforeEach(() => { + componentViewerScvdCollector = new ComponentViewerScvdCollector(); + }); + + it('exists', () => { + expect(componentViewerScvdCollector).toBeDefined(); + }); + +}); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts new file mode 100644 index 00000000..7710bb6f --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector'; + +describe('CorePeripheralsScvdCollector', () => { + let corePeripheralsScvdCollector: CorePeripheralsScvdCollector; + + beforeEach(() => { + corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(); + }); + + it('exists', () => { + expect(corePeripheralsScvdCollector).toBeDefined(); + }); + +}); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts new file mode 100644 index 00000000..adcc4d32 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { promisify } from 'util'; +import { GDBTargetDebugSession } from '../../debug-session'; +import { ScvdCollector } from '../component-viewer/component-viewer-base'; + +// Relative to dist folder at runtime +const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); + +export class CorePeripheralsScvdCollector implements ScvdCollector { + public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { + const filePaths = await promisify(fs.readdir)(CORE_PERIPHERAL_SCVD_BASE, { + encoding: 'buffer', + withFileTypes: true + }); + const scvdFilePaths = filePaths + .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) + .map((file) => path.join(CORE_PERIPHERAL_SCVD_BASE, file.name.toString())); + return scvdFilePaths; + } +} diff --git a/src/views/core-peripherals/core-peripherals.test.ts b/src/views/core-peripherals/core-peripherals.test.ts new file mode 100644 index 00000000..0a547312 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals.test.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { extensionContextFactory } from '../../__test__/vscode.factory'; +import { treeDataProviderFactory } from '../component-viewer/__test__/component-viewer-parts.factory'; +import { debugTrackerFactory } from '../component-viewer/__test__/debug-session.factory'; +import { CorePeripherals } from './core-peripherals'; + +describe('CorePeripherals', () => { + let corePeripherals: CorePeripherals; + + beforeEach(() => { + const context = extensionContextFactory(); + const componentViewerTreeDataProvider = treeDataProviderFactory(); + corePeripherals = new CorePeripherals(context, componentViewerTreeDataProvider); + }); + + it('correctly activates', async () => { + const tracker = debugTrackerFactory(); + expect(corePeripherals).toBeDefined(); + await corePeripherals.activate(tracker); + }); + +}); diff --git a/src/views/core-peripherals/core-peripherals.ts b/src/views/core-peripherals/core-peripherals.ts index b32a3758..b99c198d 100644 --- a/src/views/core-peripherals/core-peripherals.ts +++ b/src/views/core-peripherals/core-peripherals.ts @@ -14,34 +14,22 @@ * limitations under the License. */ -import * as fs from 'fs'; -import * as path from 'path'; import * as vscode from 'vscode'; import { ComponentViewerBase } from '../component-viewer/component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../component-viewer/component-viewer-tree-view'; -import { GDBTargetDebugSession } from '../../debug-session'; -import { promisify } from 'util'; - -// Relative to dist folder at runtime -const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); +import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector'; export class CorePeripherals extends ComponentViewerBase { public constructor( context: vscode.ExtensionContext, componentViewerTreeDataProvider: ComponentViewerTreeDataProvider ) { - super(context, componentViewerTreeDataProvider, 'Core Peripherals', 'corePeripherals'); - } - - protected override async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - const filePaths = await promisify(fs.readdir)(CORE_PERIPHERAL_SCVD_BASE, { - encoding: 'buffer', - withFileTypes: true - }); - const scvdFilePaths = filePaths - .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) - .map((file) => path.join(CORE_PERIPHERAL_SCVD_BASE, file.name.toString())); - return scvdFilePaths; + super( + context, + componentViewerTreeDataProvider, + new CorePeripheralsScvdCollector(), + 'Core Peripherals', + 'corePeripherals' + ); } - } From b3668b8449ea72de11254d8f88ae78d280204fe1 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 27 Feb 2026 20:29:44 +0100 Subject: [PATCH 19/47] Move debug-session.factory close to implementation files Signed-off-by: Jens Reinecke --- .../__test__/debug-session.factory.ts | 2 +- .../component-viewer/test/unit/component-viewer-base.test.ts | 2 +- src/views/component-viewer/test/unit/component-viewer.test.ts | 2 +- src/views/core-peripherals/core-peripherals.test.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/{views/component-viewer => debug-session}/__test__/debug-session.factory.ts (97%) diff --git a/src/views/component-viewer/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts similarity index 97% rename from src/views/component-viewer/__test__/debug-session.factory.ts rename to src/debug-session/__test__/debug-session.factory.ts index 4cd7116a..fa1a7595 100644 --- a/src/views/component-viewer/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -15,7 +15,7 @@ */ // generated with AI -import { GDBTargetDebugTracker, TargetState } from '../../../debug-session'; +import { GDBTargetDebugTracker, TargetState } from '..'; export type OnRefreshCallback = (session: Session) => void; diff --git a/src/views/component-viewer/test/unit/component-viewer-base.test.ts b/src/views/component-viewer/test/unit/component-viewer-base.test.ts index f5e171c3..c06856e3 100644 --- a/src/views/component-viewer/test/unit/component-viewer-base.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-base.test.ts @@ -29,7 +29,7 @@ import { ComponentViewerInstancesWrapper, ScvdCollector, UpdateReason } from '.. import { ComponentViewerBase } from '../../component-viewer-base'; import { ComponentViewerTreeDataProvider } from '../../component-viewer-tree-view'; import type { ScvdGuiInterface } from '../../model/scvd-gui-interface'; -import { debugSessionFactory, trackerFactory, OnRefreshCallback, Session, TrackerCallbacks } from '../../__test__/debug-session.factory'; +import { debugSessionFactory, trackerFactory, OnRefreshCallback, Session, TrackerCallbacks } from '../../../../debug-session/__test__/debug-session.factory'; const instanceFactory = jest.fn(() => ({ diff --git a/src/views/component-viewer/test/unit/component-viewer.test.ts b/src/views/component-viewer/test/unit/component-viewer.test.ts index bc91e4d4..7370d725 100644 --- a/src/views/component-viewer/test/unit/component-viewer.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer.test.ts @@ -16,7 +16,7 @@ import { extensionContextFactory } from '../../../../__test__/vscode.factory'; import { treeDataProviderFactory } from '../../__test__/component-viewer-parts.factory'; -import { debugTrackerFactory } from '../../__test__/debug-session.factory'; +import { debugTrackerFactory } from '../../../../debug-session/__test__/debug-session.factory'; import { ComponentViewer } from '../../component-viewer'; /** diff --git a/src/views/core-peripherals/core-peripherals.test.ts b/src/views/core-peripherals/core-peripherals.test.ts index 0a547312..6f008885 100644 --- a/src/views/core-peripherals/core-peripherals.test.ts +++ b/src/views/core-peripherals/core-peripherals.test.ts @@ -16,7 +16,7 @@ import { extensionContextFactory } from '../../__test__/vscode.factory'; import { treeDataProviderFactory } from '../component-viewer/__test__/component-viewer-parts.factory'; -import { debugTrackerFactory } from '../component-viewer/__test__/debug-session.factory'; +import { debugTrackerFactory } from '../../debug-session/__test__/debug-session.factory'; import { CorePeripherals } from './core-peripherals'; describe('CorePeripherals', () => { From 99b971e90c7eafdda52d4e0d0bbef2652f2da20c Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Sat, 28 Feb 2026 10:02:22 +0100 Subject: [PATCH 20/47] Core Peripherals SCVD Collector tests Signed-off-by: Jens Reinecke --- .../__test__/debug-session.factory.ts | 6 +++- .../core-peripherals-scvd-collector.test.ts | 29 ++++++++++++++----- .../core-peripherals-scvd-collector.ts | 7 +++-- 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index fa1a7595..659f9f2b 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -15,7 +15,7 @@ */ // generated with AI -import { GDBTargetDebugTracker, TargetState } from '..'; +import { GDBTargetDebugSession, GDBTargetDebugTracker, TargetState } from '..'; export type OnRefreshCallback = (session: Session) => void; @@ -87,3 +87,7 @@ export const debugSessionFactory = (id: string, paths: string[] = [], targetStat }, targetState, }); + +export const gdbTargetDebugSessionFactory = (id: string, paths: string[] = [], targetState: Session['targetState'] = 'unknown'): GDBTargetDebugSession => ( + debugSessionFactory(id, paths, targetState) as unknown as GDBTargetDebugSession +); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index 7710bb6f..3d31b7f0 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -14,17 +14,32 @@ * limitations under the License. */ +import * as path from 'path'; import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector'; +import { gdbTargetDebugSessionFactory } from '../../debug-session/__test__/debug-session.factory'; -describe('CorePeripheralsScvdCollector', () => { - let corePeripheralsScvdCollector: CorePeripheralsScvdCollector; +const TEST_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); +const EXPECTED_CORE_PERIPHERAL_FILES = [ + 'Memory_Protection_Unit.scvd', + 'Nested_Vectored_Interrupt_Controller.scvd', + 'System_Config_and_Control.scvd', + 'System_Tick_Timer.scvd', +]; +const EXPECTED_CORE_PERIPHERAL_FILE_PATHS = EXPECTED_CORE_PERIPHERAL_FILES.map( + file => path.resolve(TEST_BASE_PATH, file) +); - beforeEach(() => { - corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(); - }); - it('exists', () => { - expect(corePeripheralsScvdCollector).toBeDefined(); +describe('CorePeripheralsScvdCollector', () => { + + it('finds all expected SCVD files', async () => { + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(TEST_BASE_PATH); + const debugSession = gdbTargetDebugSessionFactory('TestSession', [], 'unknown'); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths.length).toBe(EXPECTED_CORE_PERIPHERAL_FILE_PATHS.length); + EXPECTED_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { + expect(scvdFilePaths.includes(filePath)).toBe(true); + }); }); }); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index adcc4d32..d1262c8e 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -24,14 +24,17 @@ import { ScvdCollector } from '../component-viewer/component-viewer-base'; const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); export class CorePeripheralsScvdCollector implements ScvdCollector { + public constructor(private readonly basePath: string = CORE_PERIPHERAL_SCVD_BASE) {} + public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - const filePaths = await promisify(fs.readdir)(CORE_PERIPHERAL_SCVD_BASE, { + const resolvedBasePath = path.resolve(this.basePath); + const filePaths = await promisify(fs.readdir)(resolvedBasePath, { encoding: 'buffer', withFileTypes: true }); const scvdFilePaths = filePaths .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) - .map((file) => path.join(CORE_PERIPHERAL_SCVD_BASE, file.name.toString())); + .map((file) => path.join(resolvedBasePath, file.name.toString())); return scvdFilePaths; } } From e54f0d7917235fa680064f66e1bfbb8e9242b352 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Sat, 28 Feb 2026 10:44:20 +0100 Subject: [PATCH 21/47] Fix and extend debug session factory Signed-off-by: Jens Reinecke --- .../__test__/debug-session.factory.ts | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 659f9f2b..00593d60 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -78,16 +78,32 @@ export const trackerFactory = (): TrackerCallbacks => { export const debugTrackerFactory = () => trackerFactory() as unknown as GDBTargetDebugTracker; -export const debugSessionFactory = (id: string, paths: string[] = [], targetState: Session['targetState'] = 'unknown'): Session => ({ - session: { id }, - getCbuildRun: async () => ({ getScvdFilePaths: () => paths }), - getPname: async () => undefined, - refreshTimer: { - onRefresh: jest.fn(), - }, - targetState, -}); +export const debugSessionFactory = ( + id: string, + paths: string[] = [], + targetState: Session['targetState'] = 'unknown', + pname: string | undefined = undefined, + hasCbuildRun = true +): Session => { + // Ensure same object returned for multiple calls to getCbuildRun. + const cbuildRunMock = hasCbuildRun ? { getScvdFilePaths: () => paths } : undefined; + return { + session: { id }, + getCbuildRun: async () => cbuildRunMock, + getPname: async () => pname, + refreshTimer: { + onRefresh: jest.fn(), + }, + targetState, + }; +}; -export const gdbTargetDebugSessionFactory = (id: string, paths: string[] = [], targetState: Session['targetState'] = 'unknown'): GDBTargetDebugSession => ( - debugSessionFactory(id, paths, targetState) as unknown as GDBTargetDebugSession +export const gdbTargetDebugSessionFactory = ( + id: string, + paths: string[] = [], + targetState: Session['targetState'] = 'unknown', + pname: string | undefined = undefined, + hasCbuildRun = true +): GDBTargetDebugSession => ( + debugSessionFactory(id, paths, targetState, pname, hasCbuildRun) as unknown as GDBTargetDebugSession ); From 869cf12c6254ea4e626d9f61dc0bd689a9c36a44 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Sat, 28 Feb 2026 10:56:00 +0100 Subject: [PATCH 22/47] ComponentViewerScvdCollector tests Signed-off-by: Jens Reinecke --- .../component-viewer-scvd-collector.test.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts b/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts index e30ed618..872dac89 100644 --- a/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts +++ b/src/views/component-viewer/test/unit/component-viewer-scvd-collector.test.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { gdbTargetDebugSessionFactory } from '../../../../debug-session/__test__/debug-session.factory'; import { ComponentViewerScvdCollector } from '../../component-viewer-scvd-collector'; describe('ComponentViewerScvdCollector', () => { @@ -23,8 +24,30 @@ describe('ComponentViewerScvdCollector', () => { componentViewerScvdCollector = new ComponentViewerScvdCollector(); }); - it('exists', () => { - expect(componentViewerScvdCollector).toBeDefined(); + it.each([ + { + inputFilePaths: ['path/to/scvd1.scvd', 'path/to/scvd2.scvd'], + pname: undefined, + }, + { + inputFilePaths: [], + pname: 'test-pname', + } + ])('returns scvd files as registered in the session', async ({ inputFilePaths, pname }) => { + const session = gdbTargetDebugSessionFactory('test-session-id', inputFilePaths, 'unknown', pname); + const cbuildRunReader = await session.getCbuildRun(); + expect(cbuildRunReader).toBeDefined(); + const cbuildRunGetScvdSpy = jest.spyOn(cbuildRunReader!, 'getScvdFilePaths'); + const receivedPaths = await componentViewerScvdCollector.getScvdFilePaths(session); + expect(cbuildRunGetScvdSpy).toHaveBeenCalledWith(undefined, pname); + expect(receivedPaths).toEqual(expect.arrayContaining(inputFilePaths)); + }); + + it('returns empty array if cbuildRun reader is undefined', async () => { + const inputFilePaths = ['path/to/scvd1.scvd', 'path/to/scvd2.scvd']; + const session = gdbTargetDebugSessionFactory('test-session-id', inputFilePaths, 'unknown', undefined, false); + const receivedPaths = await componentViewerScvdCollector.getScvdFilePaths(session); + expect(receivedPaths).toEqual([]); }); }); From 365cbca5aa6d18fe56afa4ec9a5f6d6eaadac7c3 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Sat, 28 Feb 2026 10:57:55 +0100 Subject: [PATCH 23/47] Remove redundant check Signed-off-by: Jens Reinecke --- src/views/component-viewer/component-viewer-scvd-collector.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/views/component-viewer/component-viewer-scvd-collector.ts b/src/views/component-viewer/component-viewer-scvd-collector.ts index 29d1425d..eeb64d7a 100644 --- a/src/views/component-viewer/component-viewer-scvd-collector.ts +++ b/src/views/component-viewer/component-viewer-scvd-collector.ts @@ -26,9 +26,6 @@ export class ComponentViewerScvdCollector implements ScvdCollector { } // Get SCVD file paths from cbuild-run reader const scvdFilesPaths: string [] = cbuildRunReader.getScvdFilePaths(undefined, pname); - if (scvdFilesPaths.length === 0) { - return []; - } return scvdFilesPaths; } } From 949d8b08a632ef199321e0ec5209f2958cdcd7ba Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 10:08:39 +0100 Subject: [PATCH 24/47] Enable Core Peripherals with hidden setting "cmsis-debugger.corePeripherals.enabled": true, Signed-off-by: Jens Reinecke --- package.json | 19 ++++++++++++------- src/desktop/extension.ts | 4 +++- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 51e2697b..162cdf56 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,8 @@ { "id": "cmsis-debugger.corePeripherals", "name": "Core Peripherals", - "icon": "media/trace-and-live-light.svg" + "icon": "media/trace-and-live-light.svg", + "when": "config.cmsis-debugger.corePeripherals.enabled" } ] }, @@ -175,23 +176,27 @@ "command": "vscode-cmsis-debugger.corePeripherals.lockComponent", "title": "Lock Component", "icon": "$(unlock)", - "category": "Core Peripherals" + "category": "Core Peripherals", + "when": "config.cmsis-debugger.corePeripherals.enabled" }, { "command": "vscode-cmsis-debugger.corePeripherals.unlockComponent", "title": "Unlock Component", "icon": "$(lock)", - "category": "Core Peripherals" + "category": "Core Peripherals", + "when": "config.cmsis-debugger.corePeripherals.enabled" }, { "command": "vscode-cmsis-debugger.corePeripherals.enablePeriodicUpdate", "title": "Enable Periodic Update", - "category": "Core Peripherals" + "category": "Core Peripherals", + "when": "config.cmsis-debugger.corePeripherals.enabled" }, { "command": "vscode-cmsis-debugger.corePeripherals.disablePeriodicUpdate", "title": "Disable Periodic Update", - "category": "Core Peripherals" + "category": "Core Peripherals", + "when": "config.cmsis-debugger.corePeripherals.enabled" } ], "menus": { @@ -396,12 +401,12 @@ }, { "command": "vscode-cmsis-debugger.corePeripherals.lockComponent", - "when": "view == cmsis-debugger.corePeripherals && viewItem == parentInstance", + "when": "config.cmsis-debugger.corePeripherals.enabled && view == cmsis-debugger.corePeripherals && viewItem == parentInstance", "group": "inline@1" }, { "command": "vscode-cmsis-debugger.corePeripherals.unlockComponent", - "when": "view == cmsis-debugger.corePeripherals && viewItem == locked.parentInstance", + "when": "config.cmsis-debugger.corePeripherals.enabled && view == cmsis-debugger.corePeripherals && viewItem == locked.parentInstance", "group": "inline@2" } ] diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index 17a81664..7f821f3f 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -79,9 +79,11 @@ export const activate = async (context: vscode.ExtensionContext): Promise if (!await componentViewer.activate(gdbtargetDebugTracker)) { canCompleteActivation = false; } + // Temporary guard: enable once solution is ready + const corePeripheralsEnabled = vscode.workspace.getConfiguration().get('cmsis-debugger.corePeripherals.enabled', false); // Core Peripherals logger.debug('Activating Core Peripherals'); - if (!await corePeripherals.activate(gdbtargetDebugTracker)) { + if (corePeripheralsEnabled && !await corePeripherals.activate(gdbtargetDebugTracker)) { canCompleteActivation = false; } From d1603cdb0fbbaa66bd8c16bbc09f61c3f132d618 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 10:18:44 +0100 Subject: [PATCH 25/47] Update tests to mock 'cmsis-debugger.corePeripherals.enabled' setting Signed-off-by: Jens Reinecke --- src/desktop/extension.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/desktop/extension.test.ts b/src/desktop/extension.test.ts index ad989b58..62eda2f0 100644 --- a/src/desktop/extension.test.ts +++ b/src/desktop/extension.test.ts @@ -28,6 +28,11 @@ describe('extension', () => { const componentViewerCommands = [ 'cmsis-debugger.componentViewer.open', 'cmsis-debugger.componentViewer.focus' ]; const corePeripheralsCommands = [ 'cmsis-debugger.corePeripherals.open', 'cmsis-debugger.corePeripherals.focus' ]; + beforeEach(() => { + // Temporary mock for config.cmsis-debugger.corePeripherals.enabled until removed again + (vscode.workspace.getConfiguration as jest.Mock).mockReturnValueOnce({ get: (command: string) => command === 'cmsis-debugger.corePeripherals.enabled' }); + }); + it('activates extension without asking to reload', async () => { const loggerSpy = jest.spyOn(logger, 'debug'); await activate(extensionContextFactory()); From 4bef31c7ca36dacef318871434916bc8e46abedd Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 11:47:51 +0100 Subject: [PATCH 26/47] Try-catch around file read Signed-off-by: Jens Reinecke --- .../core-peripherals-scvd-collector.ts | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index d1262c8e..b4f7e096 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -19,6 +19,7 @@ import * as path from 'path'; import { promisify } from 'util'; import { GDBTargetDebugSession } from '../../debug-session'; import { ScvdCollector } from '../component-viewer/component-viewer-base'; +import { componentViewerLogger } from '../../logger'; // Relative to dist folder at runtime const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); @@ -28,10 +29,18 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { const resolvedBasePath = path.resolve(this.basePath); - const filePaths = await promisify(fs.readdir)(resolvedBasePath, { - encoding: 'buffer', - withFileTypes: true - }); + const filePaths = []; + try { + const readFilePaths = await promisify(fs.readdir)(resolvedBasePath, { + encoding: 'buffer', + withFileTypes: true + }); + filePaths.push(...readFilePaths); + } catch (err) { + // Log error and return empty list if directory cannot be read, e.g. because it does not exist + componentViewerLogger.error(`Core Peripherals: Error reading SCVD files from ${resolvedBasePath}:`, err); + return []; + } const scvdFilePaths = filePaths .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) .map((file) => path.join(resolvedBasePath, file.name.toString())); From 414bfc20ad6806fd852ba9595397f8afc32fa4b8 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 6 Mar 2026 10:49:04 +0100 Subject: [PATCH 27/47] Feedback omarArm Signed-off-by: Jens Reinecke --- src/views/core-peripherals/core-peripherals-scvd-collector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index b4f7e096..1ae10464 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -16,7 +16,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { promisify } from 'util'; import { GDBTargetDebugSession } from '../../debug-session'; import { ScvdCollector } from '../component-viewer/component-viewer-base'; import { componentViewerLogger } from '../../logger'; @@ -31,7 +30,8 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { const resolvedBasePath = path.resolve(this.basePath); const filePaths = []; try { - const readFilePaths = await promisify(fs.readdir)(resolvedBasePath, { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const readFilePaths = await fs.promises.readdir(resolvedBasePath, { encoding: 'buffer', withFileTypes: true }); From 0ebf889c5b69516849555ff891cb720cd8d111fe Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 16:19:39 +0100 Subject: [PATCH 28/47] Add Core Peripheral Index schema Signed-off-by: Jens Reinecke --- .../core-peripheral-index.schema.json | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 configs/core-peripherals/core-peripheral-index.schema.json diff --git a/configs/core-peripherals/core-peripheral-index.schema.json b/configs/core-peripherals/core-peripheral-index.schema.json new file mode 100644 index 00000000..0191bddb --- /dev/null +++ b/configs/core-peripherals/core-peripheral-index.schema.json @@ -0,0 +1,75 @@ +{ + "$comment": "Index of core peripheral SCVD files and conditions when to use them.", + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://raw.githubusercontent.com/Open-CMSIS-Pack/vscode-cmsis-debugger/configs/core-peripherals/core-peripheral-index.schema.json", + "title": "Core Peripherals index", + "description": "Index listing core peripheral SCVD description files and CPU type/feature conditions under which each file is active.", + "version": "0.1.0", + "type": "object", + "properties": { + "core-peripherals": { + "$ref": "#/definitions/CorePeripherals" + } + }, + "required": ["core-peripherals"], + "additionalProperties": false, + "definitions": { + "CorePeripherals": { + "type": "array", + "description": "Array of core peripheral entries (SCVD files with optional activation conditions).", + "items": { + "$ref": "#/definitions/CorePeripheralEntry" + }, + "minItems": 0, + "uniqueItems": true + }, + "CorePeripheralEntry": { + "type": "object", + "description": "Single SCVD description entry with optional CPU type and CPU feature conditions.", + "properties": { + "file": { + "$ref": "#/definitions/ScvdFileType" + }, + "cpu-type": { + "$ref": "#/definitions/CpuType" + }, + "cpu-features": { + "$ref": "#/definitions/CpuFeatures" + }, + "description": { + "type": "string", + "description": "Optional human-readable description." + } + }, + "required": ["file"], + "additionalProperties": false + }, + "ScvdFileType": { + "type": "string", + "description": "Path or identifier of the SCVD description file." + }, + "CpuType": { + "description": "Specifies one or more CPU types. '*' is allowed as wildcard and is the default if omitted.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "CpuFeatures": { + "type": "object", + "description": "Map of CPU features and their value. Each feature key may only appear once. '*' is allowed as wildcard and is the default if omitted.", + "additionalProperties": { + "type": "string" + } + } + } +} From e13baf4ae5d27423bbc6e98099474eae073110cb Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 16:29:14 +0100 Subject: [PATCH 29/47] Core Peripheral Index file (initial version) Signed-off-by: Jens Reinecke --- configs/core-peripherals/core-peripheral-index.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 configs/core-peripherals/core-peripheral-index.yml diff --git a/configs/core-peripherals/core-peripheral-index.yml b/configs/core-peripherals/core-peripheral-index.yml new file mode 100644 index 00000000..ead1c461 --- /dev/null +++ b/configs/core-peripherals/core-peripheral-index.yml @@ -0,0 +1,11 @@ +core-peripherals: + - file: Memory_Protection_Unit.scvd + cpu-type: "*" + cpu-features: + mpu: present + - file: Nested_Vectored_Interrupt_Controller.scvd + cpu-type: "*" + - file: System_Config_and_Control.scvd + cpu-type: "*" + - file: System_Tick_Timer.scvd + cpu-type: "*" From f552dcef0452ca70374ee06b607f6e3d63d87e96 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 2 Mar 2026 16:35:06 +0100 Subject: [PATCH 30/47] renaming Signed-off-by: Jens Reinecke --- ...ral-index.schema.json => core-peripherals-index.schema.json} | 2 +- .../{core-peripheral-index.yml => core-peripherals-index.yml} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename configs/core-peripherals/{core-peripheral-index.schema.json => core-peripherals-index.schema.json} (96%) rename configs/core-peripherals/{core-peripheral-index.yml => core-peripherals-index.yml} (100%) diff --git a/configs/core-peripherals/core-peripheral-index.schema.json b/configs/core-peripherals/core-peripherals-index.schema.json similarity index 96% rename from configs/core-peripherals/core-peripheral-index.schema.json rename to configs/core-peripherals/core-peripherals-index.schema.json index 0191bddb..5bd8249c 100644 --- a/configs/core-peripherals/core-peripheral-index.schema.json +++ b/configs/core-peripherals/core-peripherals-index.schema.json @@ -1,7 +1,7 @@ { "$comment": "Index of core peripheral SCVD files and conditions when to use them.", "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://raw.githubusercontent.com/Open-CMSIS-Pack/vscode-cmsis-debugger/configs/core-peripherals/core-peripheral-index.schema.json", + "$id": "https://raw.githubusercontent.com/Open-CMSIS-Pack/vscode-cmsis-debugger/configs/core-peripherals/core-peripherals-index.schema.json", "title": "Core Peripherals index", "description": "Index listing core peripheral SCVD description files and CPU type/feature conditions under which each file is active.", "version": "0.1.0", diff --git a/configs/core-peripherals/core-peripheral-index.yml b/configs/core-peripherals/core-peripherals-index.yml similarity index 100% rename from configs/core-peripherals/core-peripheral-index.yml rename to configs/core-peripherals/core-peripherals-index.yml From 099fb80aafc5cea479576765ea27f0be7fb984a9 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 06:22:53 +0100 Subject: [PATCH 31/47] Schema update Signed-off-by: Jens Reinecke --- configs/core-peripherals/core-peripherals-index.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/core-peripherals/core-peripherals-index.schema.json b/configs/core-peripherals/core-peripherals-index.schema.json index 5bd8249c..ea18f842 100644 --- a/configs/core-peripherals/core-peripherals-index.schema.json +++ b/configs/core-peripherals/core-peripherals-index.schema.json @@ -36,7 +36,7 @@ "cpu-features": { "$ref": "#/definitions/CpuFeatures" }, - "description": { + "info": { "type": "string", "description": "Optional human-readable description." } From 9e7a229b3979769047ab906cb6624916207e9550 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 06:24:04 +0100 Subject: [PATCH 32/47] CorePeripheralsIndexReader and initial test Signed-off-by: Jens Reinecke --- ...core-peripherals-index-reader.test.ts.snap | 27 +++++++++ .../core-peripherals-index-reader.test.ts | 33 +++++++++++ .../core-peripherals-index-reader.ts | 59 +++++++++++++++++++ .../core-peripherals-index-types.ts | 30 ++++++++++ 4 files changed, 149 insertions(+) create mode 100644 src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap create mode 100644 src/views/core-peripherals/core-peripherals-index-reader.test.ts create mode 100644 src/views/core-peripherals/core-peripherals-index-reader.ts create mode 100644 src/views/core-peripherals/core-peripherals-index-types.ts diff --git a/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap b/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap new file mode 100644 index 00000000..65ef0b01 --- /dev/null +++ b/src/views/core-peripherals/__snapshots__/core-peripherals-index-reader.test.ts.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`CorePeripheralsIndexReader can read index file 1`] = ` +{ + "core-peripherals": [ + { + "cpu-features": { + "mpu": "present", + }, + "cpu-type": "*", + "file": "Memory_Protection_Unit.scvd", + }, + { + "cpu-type": "*", + "file": "Nested_Vectored_Interrupt_Controller.scvd", + }, + { + "cpu-type": "*", + "file": "System_Config_and_Control.scvd", + }, + { + "cpu-type": "*", + "file": "System_Tick_Timer.scvd", + }, + ], +} +`; diff --git a/src/views/core-peripherals/core-peripherals-index-reader.test.ts b/src/views/core-peripherals/core-peripherals-index-reader.test.ts new file mode 100644 index 00000000..8dc2b4e8 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-reader.test.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; + +// Tests are executed with different working directory, so different input path needed. +const TEST_INDEX_PATH = path.resolve(__dirname, '../../../configs/core-peripherals/core-peripherals-index.yml'); + +describe('CorePeripheralsIndexReader', () => { + + it('can read index file', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + expect(indexReader.hasContents()).toBe(true); + const contents = indexReader.getContents(); + expect(contents).toMatchSnapshot(); + }); + +}); diff --git a/src/views/core-peripherals/core-peripherals-index-reader.ts b/src/views/core-peripherals/core-peripherals-index-reader.ts new file mode 100644 index 00000000..f28e0a87 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-reader.ts @@ -0,0 +1,59 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import * as path from 'path'; +import * as yaml from 'yaml'; +import { FileReader, VscodeFileReader } from '../../desktop/file-reader'; +import { CorePeripheralsType } from './core-peripherals-index-types'; + +export class CorePeripheralsIndexReader { + private isParsed = false; + private contents: CorePeripheralsType | undefined; + private filePath: string | undefined; + private directory: string | undefined; + + constructor(private reader: FileReader = new VscodeFileReader()) {} + + public hasContents(): boolean { + return !!this.contents; + } + + public getContents(): CorePeripheralsType | undefined { + return this.contents; + } + + public async parse(filePath: string): Promise { + if (this.isParsed) { + return; + } + this.filePath = filePath; + this.directory = path.dirname(this.filePath); + this.isParsed = true; // Mark as parsed regardless of success or failure to avoid repeated parsing attempts + const fileContents = await this.reader.readFileToString(this.filePath); + this.contents = yaml.parse(fileContents) as CorePeripheralsType; + if (!this.contents) { + throw new Error(`Invalid 'core-peripherals-index' file: ${this.filePath}`); + } + } + + public getScvdFilePaths(): string[] { + const resolveFilePaths = this.contents?.['core-peripherals'].map( + entry => this.directory ? path.resolve(this.directory, entry.file) : entry.file + ); + return resolveFilePaths ?? []; + } +} diff --git a/src/views/core-peripherals/core-peripherals-index-types.ts b/src/views/core-peripherals/core-peripherals-index-types.ts new file mode 100644 index 00000000..71c317f2 --- /dev/null +++ b/src/views/core-peripherals/core-peripherals-index-types.ts @@ -0,0 +1,30 @@ +/** + * Copyright 2026 Arm Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface CpuFeaturesType { + [feature: string]: string; +} + +export interface CorePeripheralEntryType { + file: string; + 'cpu-type'?: string | string[]; + 'cpu-features'?: CpuFeaturesType; + info?: string; +}; + +export interface CorePeripheralsType { + 'core-peripherals': CorePeripheralEntryType[]; +}; From b5d8d6ff4236845c537d9a827513bf46b3c10c8c Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 06:24:41 +0100 Subject: [PATCH 33/47] Use index reader in SCVD collector for Core Peripherals Signed-off-by: Jens Reinecke --- .../core-peripherals-scvd-collector.ts | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index 1ae10464..ce866fb2 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -14,36 +14,32 @@ * limitations under the License. */ -import * as fs from 'fs'; import * as path from 'path'; import { GDBTargetDebugSession } from '../../debug-session'; import { ScvdCollector } from '../component-viewer/component-viewer-base'; import { componentViewerLogger } from '../../logger'; +import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; // Relative to dist folder at runtime -const CORE_PERIPHERAL_SCVD_BASE = path.join(__dirname, '..', 'configs', 'core-peripherals'); +const CORE_PERIPHERAL_SCVD_BASE = path.resolve(__dirname, '..', 'configs', 'core-peripherals'); export class CorePeripheralsScvdCollector implements ScvdCollector { - public constructor(private readonly basePath: string = CORE_PERIPHERAL_SCVD_BASE) {} + private indexFilePath: string; + private indexReader: CorePeripheralsIndexReader; + + public constructor(private readonly basePath: string = CORE_PERIPHERAL_SCVD_BASE) { + this.indexFilePath = path.resolve(this.basePath, 'core-peripherals-index.yml'); + this.indexReader = new CorePeripheralsIndexReader(); + } public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { - const resolvedBasePath = path.resolve(this.basePath); - const filePaths = []; - try { - // eslint-disable-next-line security/detect-non-literal-fs-filename - const readFilePaths = await fs.promises.readdir(resolvedBasePath, { - encoding: 'buffer', - withFileTypes: true - }); - filePaths.push(...readFilePaths); - } catch (err) { - // Log error and return empty list if directory cannot be read, e.g. because it does not exist - componentViewerLogger.error(`Core Peripherals: Error reading SCVD files from ${resolvedBasePath}:`, err); + // Only parsed the first time + await this.indexReader.parse(this.indexFilePath); + if (!this.indexReader.hasContents()) { + componentViewerLogger.warn(`Core Peripherals: No contents found in index file ${this.indexFilePath}`); return []; } - const scvdFilePaths = filePaths - .filter((file) => file.isFile() && file.name.toString().toLowerCase().endsWith('.scvd')) - .map((file) => path.join(resolvedBasePath, file.name.toString())); - return scvdFilePaths; + const filePaths = this.indexReader.getScvdFilePaths(); + return filePaths; } } From 0c44ff0475863d6a6c8b94b0a9c84c000996ff27 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 08:41:48 +0100 Subject: [PATCH 34/47] Preliminary update cbuildrun format: - rename processor type for topology - add processor type for processors node Signed-off-by: Jens Reinecke --- src/cbuild-run/cbuild-run-types.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/cbuild-run/cbuild-run-types.ts b/src/cbuild-run/cbuild-run-types.ts index 5fa7405d..82db6fd7 100644 --- a/src/cbuild-run/cbuild-run-types.ts +++ b/src/cbuild-run/cbuild-run-types.ts @@ -36,8 +36,26 @@ export interface MemoryType { 'from-pack'?: string; }; +// Preliminary type, to be implemented in CMSIS Toolbox first. +export interface ProcessorType { + core: string; + revision: string; + 'max-clock': number; + pname?: string; + punits?: number; + endian?: 'little' | 'big' | 'configurable'; + fpu?: 'sp' | 'dp' | 'none'; + mpu?: 'present' | 'none'; + dsp?: 'present' | 'none'; + trustzone?: 'present' | 'none'; + mve?: 'int' | 'fp' | 'none'; + cdecp?: number; + pacbti?: 'present' | 'none'; +} + export interface SystemResourcesType { memory?: MemoryType[]; + processors?: ProcessorType[]; }; export type SystemDescriptionTypeType = 'svd'|'scvd'; @@ -139,7 +157,7 @@ export interface PunitType { address: number; }; -export interface ProcessorType { +export interface TopologyProcessorType { pname?: string; punits?: PunitType[]; apid?: number; @@ -148,7 +166,7 @@ export interface ProcessorType { export interface DebugTopologyType { debugports?: DebugPortType[]; - processors?: ProcessorType[]; + processors?: TopologyProcessorType[]; swj?: boolean; dormant?: boolean; sdf?: string; From faa8b4d3cb9837d1be0afdb76b63b5a8137d991b Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 15:05:40 +0100 Subject: [PATCH 35/47] Add filtering in Core Peripherals SCVD Collector Signed-off-by: Jens Reinecke --- .../core-peripherals-index-reader.ts | 17 ++-- .../core-peripherals-scvd-collector.test.ts | 7 +- .../core-peripherals-scvd-collector.ts | 77 +++++++++++++++++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-index-reader.ts b/src/views/core-peripherals/core-peripherals-index-reader.ts index f28e0a87..912f9ed8 100644 --- a/src/views/core-peripherals/core-peripherals-index-reader.ts +++ b/src/views/core-peripherals/core-peripherals-index-reader.ts @@ -18,7 +18,7 @@ import * as path from 'path'; import * as yaml from 'yaml'; import { FileReader, VscodeFileReader } from '../../desktop/file-reader'; -import { CorePeripheralsType } from './core-peripherals-index-types'; +import { CorePeripheralEntryType, CorePeripheralsType } from './core-peripherals-index-types'; export class CorePeripheralsIndexReader { private isParsed = false; @@ -50,10 +50,17 @@ export class CorePeripheralsIndexReader { } } - public getScvdFilePaths(): string[] { - const resolveFilePaths = this.contents?.['core-peripherals'].map( - entry => this.directory ? path.resolve(this.directory, entry.file) : entry.file + public getCorePeripherals(): CorePeripheralEntryType[] { + const corePeripherals = this.contents?.['core-peripherals']; + if (!corePeripherals) { + return []; + } + const resolvedPeripherals = corePeripherals.map( + entry => ({ + ...entry, + file: this.directory ? path.resolve(this.directory, entry.file) : entry.file + }) ); - return resolveFilePaths ?? []; + return resolvedPeripherals ?? []; } } diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index 3d31b7f0..79a6394f 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -16,7 +16,9 @@ import * as path from 'path'; import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector'; -import { gdbTargetDebugSessionFactory } from '../../debug-session/__test__/debug-session.factory'; +import { GDBTargetDebugSession } from '../../debug-session'; +import { debugSessionFactory } from '../../__test__/vscode.factory'; +import { debugConfigurationFactory } from '../../debug-configuration/debug-configuration.factory'; const TEST_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); const EXPECTED_CORE_PERIPHERAL_FILES = [ @@ -34,7 +36,8 @@ describe('CorePeripheralsScvdCollector', () => { it('finds all expected SCVD files', async () => { const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(TEST_BASE_PATH); - const debugSession = gdbTargetDebugSessionFactory('TestSession', [], 'unknown'); + // Use real session implementation for this test + const debugSession = new GDBTargetDebugSession(debugSessionFactory(debugConfigurationFactory())); const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); expect(scvdFilePaths.length).toBe(EXPECTED_CORE_PERIPHERAL_FILE_PATHS.length); EXPECTED_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index ce866fb2..bf19c46b 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -19,6 +19,8 @@ import { GDBTargetDebugSession } from '../../debug-session'; import { ScvdCollector } from '../component-viewer/component-viewer-base'; import { componentViewerLogger } from '../../logger'; import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; +import { CorePeripheralEntryType } from './core-peripherals-index-types'; +import { ProcessorType } from '../../cbuild-run'; // Relative to dist folder at runtime const CORE_PERIPHERAL_SCVD_BASE = path.resolve(__dirname, '..', 'configs', 'core-peripherals'); @@ -32,14 +34,79 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { this.indexReader = new CorePeripheralsIndexReader(); } - public async getScvdFilePaths(_session: GDBTargetDebugSession): Promise { + private async getActiveProcessor(session: GDBTargetDebugSession, processors: ProcessorType[]): Promise { + const pname = await session.getPname(); + if (!pname) { + // No pname info available, return first processor in list as best effort + return processors[0]; + } + return processors.find(processor => processor.pname === pname); + } + + private filterCpuType(entry: CorePeripheralEntryType, processorType: string): boolean { + const cpuType = entry['cpu-type']; + if (!cpuType) { + // All CPU types supported + return true; + } + const processorTypeLowerCase = processorType.toLowerCase(); + if (typeof cpuType === 'string') { + // Single entry as string + return cpuType === '*' || cpuType.toLowerCase() === processorTypeLowerCase; + } + // Array with multiple entries + return cpuType.includes('*') || cpuType.some(type => type.toLowerCase() === processorTypeLowerCase); + } + + private filterCpuFeatures(entry: CorePeripheralEntryType, processor: ProcessorType): boolean { + const cpuFeatures = entry['cpu-features']; + if (!cpuFeatures) { + // No specific CPU features required + return true; + } + const entryFeatures = Object.entries(cpuFeatures); + const processorFeatures = Object.entries(processor); + return entryFeatures.every(([entryFeatureKey, entryFeatureValue]) => { + if (entryFeatureValue === '*') { + return true; + } + const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); + if (!processorFeature) { + // Required feature not found in processor info + // NOTE: All features that are not available mean not supported. Only (optional) exceptions are: punit and endian. + // But these are currently not relevant for filtering core peripherals, so we can ignore them for now. + return false; + } + const [, processorFeatureValue] = processorFeature; + return processorFeatureValue.toString().toLowerCase() === entryFeatureValue.toLowerCase(); + }); + } + + private filterCorePeripheralEntry(entry: CorePeripheralEntryType, processor: ProcessorType): boolean { + // Test if CPU type is included + if (!this.filterCpuType(entry, processor.core)) { + return false; + } + if (!this.filterCpuFeatures(entry, processor)) { + return false; + } + return true; + } + + public async getScvdFilePaths(session: GDBTargetDebugSession): Promise { // Only parsed the first time await this.indexReader.parse(this.indexFilePath); - if (!this.indexReader.hasContents()) { - componentViewerLogger.warn(`Core Peripherals: No contents found in index file ${this.indexFilePath}`); + const corePeripherals = this.indexReader.getCorePeripherals(); + if (corePeripherals.length === 0) { + componentViewerLogger.warn(`Core Peripherals: No core peripherals found in index file ${this.indexFilePath}`); return []; } - const filePaths = this.indexReader.getScvdFilePaths(); - return filePaths; + const cbuildRunReader = await session.getCbuildRun(); + const processors = cbuildRunReader?.getContents()?.['system-resources']?.processors; + const activeProcessor = processors ? await this.getActiveProcessor(session, processors) : undefined; + const filteredCorePeripherals = activeProcessor + ? corePeripherals.filter(entry => this.filterCorePeripheralEntry(entry, activeProcessor)) + : corePeripherals; + return filteredCorePeripherals.map(entry => entry.file); } } From 685689c6af114addd15be0398644028bcbf75cc2 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 15:52:47 +0100 Subject: [PATCH 36/47] Add README on how to write index file Signed-off-by: Jens Reinecke --- configs/core-peripherals/README.md | 79 ++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 configs/core-peripherals/README.md diff --git a/configs/core-peripherals/README.md b/configs/core-peripherals/README.md new file mode 100644 index 00000000..c5637f77 --- /dev/null +++ b/configs/core-peripherals/README.md @@ -0,0 +1,79 @@ +# Core Peripherals Index (`core-peripherals-index.yml`) + +This file lists SCVD files and optional conditions that decide whether each file is loaded for +a CPU connection. The input for the condition is based on the selected core and its available +features as described under `system-resources`>`processors` in the `*.cbuild-run.yml` file. + +## File format + +```yaml +core-peripherals: + - file: Nested_Vectored_Interrupt_Controller_M33_with_TZ.scvd + cpu-type: Cortex-M33 + cpu-features: + trustzone: present + info: Example for a CPU specific NVIC description. + + - file: My_M55_M85_CorePeripheral.scvd + cpu-type: + - Cortex-M55 + - Cortex-M85 + cpu-features: + mve: fp + dsp: present + fpu: dp + info: Example for a core peripheral description valid for multiple CPU types with available CPU features. + + - file: Memory_Protection_Unit.scvd + cpu-type: "*" + cpu-features: + mpu: present + fpu: "*" + info: Example for wildcard usage +``` + +- `file` (required): Path to an SCVD file, relative to this folder (`configs/core-peripherals`). +- `cpu-type` (optional): String or list of strings matching processor `core` from `system-resources`>`processors` in `*.cbuild-run.yml`. +- `cpu-features` (optional): Key/value map matched against processor properties. +- `info` (optional): Free-text description. + +## Filtering behavior + +### Processor selection + +- If the core connection's `pname` matches a child node of `system-resources`>`processors`, then that processor is used. +- If `pname` is missing, then a single-core system is assumed. Hence, the first processor in the list is used. +- If no processors are available, no filtering is applied and all SCVD files are loaded. + +### `cpu-type` matching + +- If omitted or set to the `"*"` wildcard, then entry is valid for all CPU types. +- Otherwise, SCVD files are loaded if the connection's core matches one of the given `core` values. +Matching is case-insensitive. + +### `cpu-features` matching + +- If omitted, no feature constraints are applied. +- All listed feature conditions must match (`AND` logic). +- Feature must exist in the selected processor object and values must match (case-insensitive). +- Feature value `"*"` matches any value for that key. + +## Feature keys and allowed values + +Use feature keys that can appear on a processor entry in `system-resources`>`processors` of `*.cbuild-run.yml`. + +- `fpu`: `sp`, `dp`, `none` +- `mpu`: `present`, `none` +- `dsp`: `present`, `none` +- `trustzone`: `present`, `none` +- `mve`: `int`, `fp`, `none` +- `pacbti`: `present`, `none` +- `endian`: `little`, `big`, `configurable` + +Additional notes for filter behavior: + +- `cpu-features` keys are matched exactly by key name (for example `mpu`, not `MPU`). +- Feature value comparison is case-insensitive after converting values to strings. +- If a key is listed in `cpu-features` but missing on the selected processor, the entry does not match. +This is to reflect that a missing processor feature usually means it is not implemented. +- Use `"*"` as a feature value to accept any value for that key. From 3607e637bc6dde6e712c8d551c10a1d5ddeaa698 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 19:14:14 +0100 Subject: [PATCH 37/47] Copilot feedback Signed-off-by: Jens Reinecke --- .../core-peripherals-index-reader.ts | 2 +- .../core-peripherals-scvd-collector.ts | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-index-reader.ts b/src/views/core-peripherals/core-peripherals-index-reader.ts index 912f9ed8..6a72915a 100644 --- a/src/views/core-peripherals/core-peripherals-index-reader.ts +++ b/src/views/core-peripherals/core-peripherals-index-reader.ts @@ -42,12 +42,12 @@ export class CorePeripheralsIndexReader { } this.filePath = filePath; this.directory = path.dirname(this.filePath); - this.isParsed = true; // Mark as parsed regardless of success or failure to avoid repeated parsing attempts const fileContents = await this.reader.readFileToString(this.filePath); this.contents = yaml.parse(fileContents) as CorePeripheralsType; if (!this.contents) { throw new Error(`Invalid 'core-peripherals-index' file: ${this.filePath}`); } + this.isParsed = true; } public getCorePeripherals(): CorePeripheralEntryType[] { diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index bf19c46b..51a1f332 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -40,7 +40,8 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { // No pname info available, return first processor in list as best effort return processors[0]; } - return processors.find(processor => processor.pname === pname); + const result = processors.find(processor => processor.pname === pname); + return result ?? processors[0]; // If no exact match found, return first processor as fallback } private filterCpuType(entry: CorePeripheralEntryType, processorType: string): boolean { @@ -73,11 +74,15 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); if (!processorFeature) { // Required feature not found in processor info - // NOTE: All features that are not available mean not supported. Only (optional) exceptions are: punit and endian. + // NOTE: All features that are not available mean not supported. Only (optional) exceptions are: punits and endian. // But these are currently not relevant for filtering core peripherals, so we can ignore them for now. return false; } const [, processorFeatureValue] = processorFeature; + if (processorFeatureValue === undefined || processorFeatureValue === null) { + // No valid value for processor feature, treat as not supported + return false; + } return processorFeatureValue.toString().toLowerCase() === entryFeatureValue.toLowerCase(); }); } @@ -94,8 +99,12 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { } public async getScvdFilePaths(session: GDBTargetDebugSession): Promise { - // Only parsed the first time - await this.indexReader.parse(this.indexFilePath); + try { + await this.indexReader.parse(this.indexFilePath); + } catch (error) { + componentViewerLogger.error(`Core Peripherals: Failed to parse index file ${this.indexFilePath}: ${error}`); + return []; + } const corePeripherals = this.indexReader.getCorePeripherals(); if (corePeripherals.length === 0) { componentViewerLogger.warn(`Core Peripherals: No core peripherals found in index file ${this.indexFilePath}`); From 9dceeea58723f67b95c918c803317a9ace07cb33 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Tue, 3 Mar 2026 19:21:26 +0100 Subject: [PATCH 38/47] Fix comment to silence qlty Signed-off-by: Jens Reinecke --- src/views/core-peripherals/core-peripherals-scvd-collector.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index 51a1f332..7c590eab 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -74,8 +74,8 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); if (!processorFeature) { // Required feature not found in processor info - // NOTE: All features that are not available mean not supported. Only (optional) exceptions are: punits and endian. - // But these are currently not relevant for filtering core peripherals, so we can ignore them for now. + // All features that are not available mean not supported. Only (optional) exceptions are: punits and endian. + // But these are currently not relevant for filtering core peripherals, so we can ignore them for now. return false; } const [, processorFeatureValue] = processorFeature; From 766213c35b9816a1539c7df35bfcbdd47e6e326b Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Wed, 4 Mar 2026 15:10:30 +0100 Subject: [PATCH 39/47] Extend Core Peripherals Index Reader tests Signed-off-by: Jens Reinecke --- .../core-peripherals-index-reader.test.ts | 20 +++++++++++++++++++ .../core-peripherals-index/empty-index.yml | 0 2 files changed, 20 insertions(+) create mode 100644 test-data/core-peripherals-index/empty-index.yml diff --git a/src/views/core-peripherals/core-peripherals-index-reader.test.ts b/src/views/core-peripherals/core-peripherals-index-reader.test.ts index 8dc2b4e8..a81ab299 100644 --- a/src/views/core-peripherals/core-peripherals-index-reader.test.ts +++ b/src/views/core-peripherals/core-peripherals-index-reader.test.ts @@ -19,6 +19,7 @@ import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; // Tests are executed with different working directory, so different input path needed. const TEST_INDEX_PATH = path.resolve(__dirname, '../../../configs/core-peripherals/core-peripherals-index.yml'); +const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals-index/empty-index.yml'); describe('CorePeripheralsIndexReader', () => { @@ -30,4 +31,23 @@ describe('CorePeripheralsIndexReader', () => { expect(contents).toMatchSnapshot(); }); + it('returns empty array if no core peripherals parsed', async () => { + const indexReader = new CorePeripheralsIndexReader(); + // Get peripherals without parsing file first, should return empty array. + expect(indexReader.hasContents()).toBe(false); + expect(indexReader.getContents()).toBeUndefined(); + expect(indexReader.getCorePeripherals()).toEqual([]); + }); + + it('throws for an empty index file', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(EMPTY_INDEX_PATH)).rejects.toThrow('Invalid \'core-peripherals-index\' file'); + }); + + it('parses only once', async () => { + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + // Clear spy calls and parse an empty file. It should not throw because it should not attempt to parse the file again. + await expect(indexReader.parse(EMPTY_INDEX_PATH)).resolves.not.toThrow(); + }); }); diff --git a/test-data/core-peripherals-index/empty-index.yml b/test-data/core-peripherals-index/empty-index.yml new file mode 100644 index 00000000..e69de29b From 8da1177c3e418f6719a5d0d1034dfbb2c745e362 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Wed, 4 Mar 2026 20:31:54 +0100 Subject: [PATCH 40/47] Add integrity test for core peripheral config folder Signed-off-by: Jens Reinecke --- .../core-peripherals-index-reader.test.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-index-reader.test.ts b/src/views/core-peripherals/core-peripherals-index-reader.test.ts index a81ab299..0f597ac1 100644 --- a/src/views/core-peripherals/core-peripherals-index-reader.test.ts +++ b/src/views/core-peripherals/core-peripherals-index-reader.test.ts @@ -14,15 +14,34 @@ * limitations under the License. */ +import * as fs from 'fs'; import * as path from 'path'; import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; +import { promisify } from 'util'; // Tests are executed with different working directory, so different input path needed. -const TEST_INDEX_PATH = path.resolve(__dirname, '../../../configs/core-peripherals/core-peripherals-index.yml'); -const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals-index/empty-index.yml'); +const TEST_INDEX_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); +const TEST_INDEX_PATH = path.resolve(TEST_INDEX_BASE_PATH, 'core-peripherals-index.yml'); +const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/empty-index/core-peripherals-index.yml'); describe('CorePeripheralsIndexReader', () => { + it('finds all core peripherals entries of index file in folder', async () => { + const filesInDir = await promisify(fs.readdir)(TEST_INDEX_BASE_PATH, { + encoding: 'buffer', + withFileTypes: true + }); + const scvdFilesInDir = filesInDir.filter(entry => entry.isFile() && entry.name.toString().toLowerCase().endsWith('.scvd')); + const scvdFilePathsInDir = scvdFilesInDir.map(file => path.resolve(TEST_INDEX_BASE_PATH, file.name.toString())); + const indexReader = new CorePeripheralsIndexReader(); + await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); + const indexEntries = indexReader.getCorePeripherals().map(entry => path.resolve(TEST_INDEX_BASE_PATH, entry.file)); + expect(indexEntries.length).toBe(scvdFilePathsInDir.length); + scvdFilePathsInDir.forEach(filePath => { + expect(indexEntries.includes(filePath)).toBe(true); + }); + }); + it('can read index file', async () => { const indexReader = new CorePeripheralsIndexReader(); await expect(indexReader.parse(TEST_INDEX_PATH)).resolves.not.toThrow(); @@ -50,4 +69,5 @@ describe('CorePeripheralsIndexReader', () => { // Clear spy calls and parse an empty file. It should not throw because it should not attempt to parse the file again. await expect(indexReader.parse(EMPTY_INDEX_PATH)).resolves.not.toThrow(); }); + }); From 9d249f5fe0e53e54a3b2fd7d6bc4efa511697da8 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Wed, 4 Mar 2026 20:32:25 +0100 Subject: [PATCH 41/47] Refine core peripheral scvd collector (filter behavior) Signed-off-by: Jens Reinecke --- .../core-peripherals-scvd-collector.ts | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index 7c590eab..e769eeec 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -41,7 +41,7 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { return processors[0]; } const result = processors.find(processor => processor.pname === pname); - return result ?? processors[0]; // If no exact match found, return first processor as fallback + return result; // If pname requested but not found in cbuild-run processors, then we fail. } private filterCpuType(entry: CorePeripheralEntryType, processorType: string): boolean { @@ -59,14 +59,16 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { return cpuType.includes('*') || cpuType.some(type => type.toLowerCase() === processorTypeLowerCase); } - private filterCpuFeatures(entry: CorePeripheralEntryType, processor: ProcessorType): boolean { + private filterCpuFeatures(entry: CorePeripheralEntryType, processor?: ProcessorType): boolean { const cpuFeatures = entry['cpu-features']; if (!cpuFeatures) { // No specific CPU features required return true; } const entryFeatures = Object.entries(cpuFeatures); - const processorFeatures = Object.entries(processor); + // If no processor, then use empty object as reference. This let's only pass entries without + // required features, or features with wildcard value. + const processorFeatures = Object.entries(processor ?? {}); return entryFeatures.every(([entryFeatureKey, entryFeatureValue]) => { if (entryFeatureValue === '*') { return true; @@ -87,9 +89,9 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { }); } - private filterCorePeripheralEntry(entry: CorePeripheralEntryType, processor: ProcessorType): boolean { + private filterCorePeripheralEntry(entry: CorePeripheralEntryType, processor?: ProcessorType): boolean { // Test if CPU type is included - if (!this.filterCpuType(entry, processor.core)) { + if (!this.filterCpuType(entry, processor?.core ?? '*')) { return false; } if (!this.filterCpuFeatures(entry, processor)) { @@ -111,11 +113,11 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { return []; } const cbuildRunReader = await session.getCbuildRun(); - const processors = cbuildRunReader?.getContents()?.['system-resources']?.processors; + const contents = cbuildRunReader?.getContents(); + const systemResources = contents?.['system-resources']; + const processors = systemResources?.processors; const activeProcessor = processors ? await this.getActiveProcessor(session, processors) : undefined; - const filteredCorePeripherals = activeProcessor - ? corePeripherals.filter(entry => this.filterCorePeripheralEntry(entry, activeProcessor)) - : corePeripherals; + const filteredCorePeripherals = corePeripherals.filter(entry => this.filterCorePeripheralEntry(entry, activeProcessor)); return filteredCorePeripherals.map(entry => entry.file); } } From 911162d08673c345e965f036a33d0d28b99eb334 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Wed, 4 Mar 2026 20:33:20 +0100 Subject: [PATCH 42/47] Tests for CorePeripheralScvdCollector Signed-off-by: Jens Reinecke --- .../__test__/debug-session.factory.ts | 5 +- .../core-peripherals-scvd-collector.test.ts | 123 ++++++++++++++++-- .../complex-index/core-peripherals-index.yml | 24 ++++ .../empty-index/core-peripherals-index.yml} | 0 .../no-peripherals/core-peripherals-index.yml | 1 + 5 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 test-data/core-peripherals/complex-index/core-peripherals-index.yml rename test-data/{core-peripherals-index/empty-index.yml => core-peripherals/empty-index/core-peripherals-index.yml} (100%) create mode 100644 test-data/core-peripherals/no-peripherals/core-peripherals-index.yml diff --git a/src/debug-session/__test__/debug-session.factory.ts b/src/debug-session/__test__/debug-session.factory.ts index 00593d60..b72bed5c 100644 --- a/src/debug-session/__test__/debug-session.factory.ts +++ b/src/debug-session/__test__/debug-session.factory.ts @@ -86,7 +86,10 @@ export const debugSessionFactory = ( hasCbuildRun = true ): Session => { // Ensure same object returned for multiple calls to getCbuildRun. - const cbuildRunMock = hasCbuildRun ? { getScvdFilePaths: () => paths } : undefined; + const cbuildRunMock = hasCbuildRun ? { + getContents: jest.fn(), + getScvdFilePaths: () => paths + } : undefined; return { session: { id }, getCbuildRun: async () => cbuildRunMock, diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index 79a6394f..faa1393b 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -16,33 +16,136 @@ import * as path from 'path'; import { CorePeripheralsScvdCollector } from './core-peripherals-scvd-collector'; -import { GDBTargetDebugSession } from '../../debug-session'; -import { debugSessionFactory } from '../../__test__/vscode.factory'; -import { debugConfigurationFactory } from '../../debug-configuration/debug-configuration.factory'; +import { gdbTargetDebugSessionFactory } from '../../debug-session/__test__/debug-session.factory'; const TEST_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); -const EXPECTED_CORE_PERIPHERAL_FILES = [ - 'Memory_Protection_Unit.scvd', +const EXPECTED_DEFAULT_CORE_PERIPHERAL_FILES = [ 'Nested_Vectored_Interrupt_Controller.scvd', 'System_Config_and_Control.scvd', 'System_Tick_Timer.scvd', ]; -const EXPECTED_CORE_PERIPHERAL_FILE_PATHS = EXPECTED_CORE_PERIPHERAL_FILES.map( +const EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS = EXPECTED_DEFAULT_CORE_PERIPHERAL_FILES.map( file => path.resolve(TEST_BASE_PATH, file) ); +const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/empty-index'); +const NO_PERIPHERALS_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/no-peripherals'); +const COMPLEX_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-peripherals/complex-index'); +const TEST_PROCESSOR_M33_TZ_DP = { + core: 'Cortex-M33', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core0', + fpu: 'dp', + mpu: 'present', + trustzone: 'present', +}; + +const TEST_PROCESSOR_M33_NO_TZ_DP = { + core: 'Cortex-M33', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core1', + fpu: 'dp', + mpu: 'present', + trustzone: 'none', +}; + +const TEST_PROCESSOR_M55_SP = { + core: 'Cortex-M55', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core2', + fpu: 'sp', + mpu: 'present', + trustzone: 'present', +}; + +const TEST_PROCESSOR_M85 = { + core: 'Cortex-M85', + revision: 'r0p0', + 'max-clock': 100, + pname: 'core3', + fpu: 'sp', + mpu: 'present', + trustzone: 'none', +}; + +const TEST_PROCESSOR_M0P = { + core: 'Cortex-M0+', + revision: 'r0p0', + 'max-clock': 100, +}; + +const TEST_PROCESSORS = [ + TEST_PROCESSOR_M33_TZ_DP, + TEST_PROCESSOR_M33_NO_TZ_DP, + TEST_PROCESSOR_M55_SP, + TEST_PROCESSOR_M85 +]; describe('CorePeripheralsScvdCollector', () => { - it('finds all expected SCVD files', async () => { + it('returns all SCVD files that have no conditions', async () => { const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(TEST_BASE_PATH); // Use real session implementation for this test - const debugSession = new GDBTargetDebugSession(debugSessionFactory(debugConfigurationFactory())); + const debugSession = gdbTargetDebugSessionFactory('session-id'); const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); - expect(scvdFilePaths.length).toBe(EXPECTED_CORE_PERIPHERAL_FILE_PATHS.length); - EXPECTED_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { + expect(scvdFilePaths.length).toBe(EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS.length); + EXPECTED_DEFAULT_CORE_PERIPHERAL_FILE_PATHS.forEach(filePath => { expect(scvdFilePaths.includes(filePath)).toBe(true); }); }); + it('returns no SCVD files for empty index file', async () => { + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(EMPTY_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id'); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual([]); + }); + + it('returns no SCVD files for index file without file entries', async () => { + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(NO_PERIPHERALS_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id'); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual([]); + }); + + it.each([ + { pname: 'core0', expected: [ 'TZ_MPU_Cortex-M33.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core1', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core2', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'core3', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + // No matching processor, load defaults without restrictions. + { pname: 'no-match', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + // No pname info, should fallback to first entry which is M33 with TZ and DP + { pname: undefined, expected: [ 'TZ_MPU_Cortex-M33.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + ])('filters SCVD files as expected for complex index file and multi-core setup (pname: $pname)', async ({ pname, expected }) => { + const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(COMPLEX_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id', [], 'unknown', pname); + const cbuildRunReader = await debugSession.getCbuildRun(); + (cbuildRunReader?.getContents as jest.Mock).mockReturnValue({ 'system-resources': { processors: TEST_PROCESSORS } }); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual(resolvedExpected); + }); + + it.each([ + // No matching processor, load defaults without restrictions. + { pname: 'no-match', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: undefined, expected: [ 'M0_M23_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd' ] }, + ])('filters SCVD files as expected for complex index file and single-core setup (pname: $pname)', async ({ pname, expected }) => { + const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); + const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(COMPLEX_INDEX_PATH); + // Use real session implementation for this test + const debugSession = gdbTargetDebugSessionFactory('session-id', [], 'unknown', pname); + const cbuildRunReader = await debugSession.getCbuildRun(); + (cbuildRunReader?.getContents as jest.Mock).mockReturnValue({ 'system-resources': { processors: [ TEST_PROCESSOR_M0P ] } }); + const scvdFilePaths = await corePeripheralsScvdCollector.getScvdFilePaths(debugSession); + expect(scvdFilePaths).toEqual(resolvedExpected); + }); + }); diff --git a/test-data/core-peripherals/complex-index/core-peripherals-index.yml b/test-data/core-peripherals/complex-index/core-peripherals-index.yml new file mode 100644 index 00000000..5a264cc2 --- /dev/null +++ b/test-data/core-peripherals/complex-index/core-peripherals-index.yml @@ -0,0 +1,24 @@ +core-peripherals: + - file: TZ_MPU_Cortex-M33.scvd + cpu-type: Cortex-M33 + cpu-features: + mpu: present + trustzone: present + - file: M0_M23_Nested_Vectored_Interrupt_Controller.scvd + cpu-type: + - Cortex-M0+ + - Cortex-M23 + - file: M55_M85_Nested_Vectored_Interrupt_Controller.scvd + cpu-type: + - Cortex-M55 + - Cortex-M85 + - file: FPU_SP.scvd + cpu-type: "*" + cpu-features: + fpu: sp + - file: FPU_All.scvd + cpu-type: "*" + cpu-features: + fpu: "*" + - file: System_Tick_Timer.scvd + cpu-type: "*" diff --git a/test-data/core-peripherals-index/empty-index.yml b/test-data/core-peripherals/empty-index/core-peripherals-index.yml similarity index 100% rename from test-data/core-peripherals-index/empty-index.yml rename to test-data/core-peripherals/empty-index/core-peripherals-index.yml diff --git a/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml b/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml new file mode 100644 index 00000000..e6eeb321 --- /dev/null +++ b/test-data/core-peripherals/no-peripherals/core-peripherals-index.yml @@ -0,0 +1 @@ +core-peripherals: From 4f4b82ca956b40a59c9a3784af9a2f6733fe9f1e Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Thu, 5 Mar 2026 09:06:52 +0100 Subject: [PATCH 43/47] Updated readme Signed-off-by: Jens Reinecke --- configs/core-peripherals/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/configs/core-peripherals/README.md b/configs/core-peripherals/README.md index c5637f77..71d7c819 100644 --- a/configs/core-peripherals/README.md +++ b/configs/core-peripherals/README.md @@ -43,7 +43,8 @@ core-peripherals: - If the core connection's `pname` matches a child node of `system-resources`>`processors`, then that processor is used. - If `pname` is missing, then a single-core system is assumed. Hence, the first processor in the list is used. -- If no processors are available, no filtering is applied and all SCVD files are loaded. +- If no processors are available, the collector treats the processor as unknown. In this case only entries without `cpu-type`/`cpu-features` +constraints or those using the "*" wildcard will match; entries requiring a specific `cpu-type` or specific feature values are excluded. ### `cpu-type` matching From c7eea9fd0316e04d2c3a1f34178df52e0f419eaa Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Thu, 5 Mar 2026 09:28:54 +0100 Subject: [PATCH 44/47] Refine wildcard handling to exclude `none` Signed-off-by: Jens Reinecke --- .../core-peripherals-scvd-collector.test.ts | 6 +++--- .../core-peripherals/core-peripherals-scvd-collector.ts | 8 +++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index faa1393b..bf47b8b7 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -119,7 +119,7 @@ describe('CorePeripheralsScvdCollector', () => { { pname: 'core2', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, { pname: 'core3', expected: [ 'M55_M85_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_SP.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, // No matching processor, load defaults without restrictions. - { pname: 'no-match', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, + { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd'] }, // No pname info, should fallback to first entry which is M33 with TZ and DP { pname: undefined, expected: [ 'TZ_MPU_Cortex-M33.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, ])('filters SCVD files as expected for complex index file and multi-core setup (pname: $pname)', async ({ pname, expected }) => { @@ -135,8 +135,8 @@ describe('CorePeripheralsScvdCollector', () => { it.each([ // No matching processor, load defaults without restrictions. - { pname: 'no-match', expected: [ 'FPU_All.scvd', 'System_Tick_Timer.scvd'] }, - { pname: undefined, expected: [ 'M0_M23_Nested_Vectored_Interrupt_Controller.scvd', 'FPU_All.scvd', 'System_Tick_Timer.scvd' ] }, + { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd'] }, + { pname: undefined, expected: [ 'M0_M23_Nested_Vectored_Interrupt_Controller.scvd', 'System_Tick_Timer.scvd' ] }, ])('filters SCVD files as expected for complex index file and single-core setup (pname: $pname)', async ({ pname, expected }) => { const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); const corePeripheralsScvdCollector = new CorePeripheralsScvdCollector(COMPLEX_INDEX_PATH); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index e769eeec..e368dbdf 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -24,6 +24,7 @@ import { ProcessorType } from '../../cbuild-run'; // Relative to dist folder at runtime const CORE_PERIPHERAL_SCVD_BASE = path.resolve(__dirname, '..', 'configs', 'core-peripherals'); +const FEATURE_NOT_PRESENT_VALUE = 'none'; export class CorePeripheralsScvdCollector implements ScvdCollector { private indexFilePath: string; @@ -76,9 +77,10 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); if (!processorFeature) { // Required feature not found in processor info - // All features that are not available mean not supported. Only (optional) exceptions are: punits and endian. - // But these are currently not relevant for filtering core peripherals, so we can ignore them for now. - return false; + // All features that are not available mean not supported. + // Only (optional) exceptions are: punits and endian. But these are currently not relevant for filtering core + // peripherals, so we can ignore them for now. + return entryFeatureValue === FEATURE_NOT_PRESENT_VALUE; } const [, processorFeatureValue] = processorFeature; if (processorFeatureValue === undefined || processorFeatureValue === null) { From 1708b62384f09b65fcc52b77238f4b66445a8231 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 6 Mar 2026 10:52:26 +0100 Subject: [PATCH 45/47] fs.promises vs promisify Signed-off-by: Jens Reinecke --- .../core-peripherals/core-peripherals-index-reader.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/core-peripherals/core-peripherals-index-reader.test.ts b/src/views/core-peripherals/core-peripherals-index-reader.test.ts index 0f597ac1..6d336d73 100644 --- a/src/views/core-peripherals/core-peripherals-index-reader.test.ts +++ b/src/views/core-peripherals/core-peripherals-index-reader.test.ts @@ -17,7 +17,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { CorePeripheralsIndexReader } from './core-peripherals-index-reader'; -import { promisify } from 'util'; // Tests are executed with different working directory, so different input path needed. const TEST_INDEX_BASE_PATH = path.resolve(__dirname, '../../../configs/core-peripherals'); @@ -27,7 +26,8 @@ const EMPTY_INDEX_PATH = path.resolve(__dirname, '../../../test-data/core-periph describe('CorePeripheralsIndexReader', () => { it('finds all core peripherals entries of index file in folder', async () => { - const filesInDir = await promisify(fs.readdir)(TEST_INDEX_BASE_PATH, { + // eslint-disable-next-line security/detect-non-literal-fs-filename + const filesInDir = await fs.promises.readdir(TEST_INDEX_BASE_PATH, { encoding: 'buffer', withFileTypes: true }); From 94e6339ebeef63e0fd3b5af7cc9f4a2df712209a Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Mon, 9 Mar 2026 09:49:00 +0100 Subject: [PATCH 46/47] readme update Signed-off-by: Jens Reinecke --- configs/core-peripherals/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configs/core-peripherals/README.md b/configs/core-peripherals/README.md index 71d7c819..fc97b841 100644 --- a/configs/core-peripherals/README.md +++ b/configs/core-peripherals/README.md @@ -44,7 +44,7 @@ core-peripherals: - If the core connection's `pname` matches a child node of `system-resources`>`processors`, then that processor is used. - If `pname` is missing, then a single-core system is assumed. Hence, the first processor in the list is used. - If no processors are available, the collector treats the processor as unknown. In this case only entries without `cpu-type`/`cpu-features` -constraints or those using the "*" wildcard will match; entries requiring a specific `cpu-type` or specific feature values are excluded. +constraints, those using the "*" wildcard, and those using value `none` will match; entries requiring a specific `cpu-type` or specific feature values are excluded. ### `cpu-type` matching From 4a7327cff642ed8bad0652e29e51893a9f05aa88 Mon Sep 17 00:00:00 2001 From: Jens Reinecke Date: Fri, 13 Mar 2026 15:02:50 +0100 Subject: [PATCH 47/47] More intuitive handling of `none` and '*' wildcard Signed-off-by: Jens Reinecke --- configs/core-peripherals/README.md | 6 ++-- .../core-peripherals-scvd-collector.test.ts | 2 +- .../core-peripherals-scvd-collector.ts | 29 +++++++++++++------ 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/configs/core-peripherals/README.md b/configs/core-peripherals/README.md index fc97b841..b65be4a9 100644 --- a/configs/core-peripherals/README.md +++ b/configs/core-peripherals/README.md @@ -57,7 +57,7 @@ Matching is case-insensitive. - If omitted, no feature constraints are applied. - All listed feature conditions must match (`AND` logic). - Feature must exist in the selected processor object and values must match (case-insensitive). -- Feature value `"*"` matches any value for that key. +- Feature value `"*"` matches any value for that key that indicates presence of the feature. ## Feature keys and allowed values @@ -75,6 +75,6 @@ Additional notes for filter behavior: - `cpu-features` keys are matched exactly by key name (for example `mpu`, not `MPU`). - Feature value comparison is case-insensitive after converting values to strings. -- If a key is listed in `cpu-features` but missing on the selected processor, the entry does not match. +- If a key is listed in `cpu-features` but missing on the selected processor, the entry only matches if it has the value `none`. This is to reflect that a missing processor feature usually means it is not implemented. -- Use `"*"` as a feature value to accept any value for that key. +- Use `"*"` as a feature value to accept any value for that key that indicates presence of the feature. diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts index bf47b8b7..6f18d20f 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.test.ts @@ -135,7 +135,7 @@ describe('CorePeripheralsScvdCollector', () => { it.each([ // No matching processor, load defaults without restrictions. - { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd'] }, + { pname: 'no-match', expected: [ 'System_Tick_Timer.scvd' ] }, { pname: undefined, expected: [ 'M0_M23_Nested_Vectored_Interrupt_Controller.scvd', 'System_Tick_Timer.scvd' ] }, ])('filters SCVD files as expected for complex index file and single-core setup (pname: $pname)', async ({ pname, expected }) => { const resolvedExpected = expected.map(file => path.resolve(COMPLEX_INDEX_PATH, file)); diff --git a/src/views/core-peripherals/core-peripherals-scvd-collector.ts b/src/views/core-peripherals/core-peripherals-scvd-collector.ts index e368dbdf..12ebb63c 100644 --- a/src/views/core-peripherals/core-peripherals-scvd-collector.ts +++ b/src/views/core-peripherals/core-peripherals-scvd-collector.ts @@ -71,19 +71,30 @@ export class CorePeripheralsScvdCollector implements ScvdCollector { // required features, or features with wildcard value. const processorFeatures = Object.entries(processor ?? {}); return entryFeatures.every(([entryFeatureKey, entryFeatureValue]) => { + const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); + const [, processorFeatureValue] = processorFeature ?? []; + const featureUndefined = processorFeatureValue === undefined || processorFeatureValue === null; + const featureNotPresent = featureUndefined || processorFeatureValue === FEATURE_NOT_PRESENT_VALUE; if (entryFeatureValue === '*') { + // Wildcard value + if (featureNotPresent) { + // Required feature not found in processor info + return false; + } + // Wildcard value matches any value as long as the feature is present in processor info. return true; } - const processorFeature = processorFeatures.find(([processorFeatureKey]) => processorFeatureKey === entryFeatureKey); - if (!processorFeature) { - // Required feature not found in processor info - // All features that are not available mean not supported. - // Only (optional) exceptions are: punits and endian. But these are currently not relevant for filtering core - // peripherals, so we can ignore them for now. - return entryFeatureValue === FEATURE_NOT_PRESENT_VALUE; + if (entryFeatureValue === FEATURE_NOT_PRESENT_VALUE) { + // Explicit "not present" value + if (featureNotPresent) { + // Required feature not found in processor info + return true; + } + // Processor has feature. + return false; } - const [, processorFeatureValue] = processorFeature; - if (processorFeatureValue === undefined || processorFeatureValue === null) { + // Explicit value to match + if (featureUndefined) { // No valid value for processor feature, treat as not supported return false; }