From 4b94c1a723e45acb2b89686770e4ee14a76c4dce Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 14:02:11 +0200 Subject: [PATCH 1/8] Refactor manage Layers --- src/desktop/extension.ts | 2 +- src/solutions/csolution.ts | 14 --- src/solutions/solution-converter.test.ts | 26 +++- src/solutions/solution-converter.ts | 24 ++-- src/solutions/solution-event-hub.test.ts | 28 ++++- src/solutions/solution-event-hub.ts | 26 +++- .../manage-layers-webview-main.ts | 114 ++++-------------- 7 files changed, 114 insertions(+), 120 deletions(-) diff --git a/src/desktop/extension.ts b/src/desktop/extension.ts index a9f7f11a..9d1cf288 100644 --- a/src/desktop/extension.ts +++ b/src/desktop/extension.ts @@ -172,7 +172,6 @@ export const activate = async (context: ExtensionContext): Promise { eventHub = new SolutionEventHub(); completedListener = jest.fn(); eventHub.onDidConvertCompleted(completedListener); + jest.spyOn(eventHub, 'fireConfigureSolutionDataReady'); // Initialize convertRequestData with default test values convertRequestData = { @@ -137,7 +138,6 @@ describe('SolutionConverter', () => { solutionManager.getCsolution.mockReturnValue(mockCSolution); converter = new SolutionConverterImpl( - solutionManager, eventHub, mockConfigurationProvider, outputChannelProvider, @@ -281,6 +281,7 @@ describe('SolutionConverter', () => { it('run solution convert and discover layers', async () => { mockCsolutionService.convertSolution.mockResolvedValue({ success: false, undefinedLayers: ['$Board-Layer'] }); + mockCsolutionService.discoverLayers.mockResolvedValue({ success: true, configurations: [{ variables: [] }] }); await fireAndWaitForConversion(); expect(mockCsolutionService.discoverLayers).toHaveBeenCalledTimes(1); @@ -291,6 +292,29 @@ describe('SolutionConverter', () => { detection: true }) ); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledTimes(1); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledWith( + expect.objectContaining({ + availableCompilers: [], + availableConfigurations: [{ variables: [] }], + }) + ); + }); + + it('emits configure event with compilers when selectCompiler is returned', async () => { + mockCsolutionService.convertSolution.mockResolvedValue({ success: true, selectCompiler: ['GCC', 'AC6'] }); + await fireAndWaitForConversion(); + + expect(completedListener).toHaveBeenCalledWith( + expect.objectContaining({ detection: true }) + ); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledTimes(1); + expect(eventHub.fireConfigureSolutionDataReady).toHaveBeenCalledWith( + expect.objectContaining({ + availableCompilers: ['GCC', 'AC6'], + availableConfigurations: undefined, + }) + ); }); it('run solution convert and check whether to update compile commands', async () => { diff --git a/src/solutions/solution-converter.ts b/src/solutions/solution-converter.ts index c745aefa..1a698ef7 100644 --- a/src/solutions/solution-converter.ts +++ b/src/solutions/solution-converter.ts @@ -19,7 +19,6 @@ import * as manifest from '../manifest'; import { ConfigurationProvider } from '../vscode-api/configuration-provider'; import { OutputChannelProvider } from '../vscode-api/output-channel-provider'; import { CmsisToolboxManager } from './cmsis-toolbox'; -import { SolutionManager } from './solution-manager'; import { CompileCommandsGenerator } from './intellisense/compile-commands-generator'; import { Mutex } from 'async-mutex'; import * as rpc from '../json-rpc/csolution-rpc-client'; @@ -39,7 +38,6 @@ export class SolutionConverterImpl implements SolutionConverter { private data: ConvertRequestData = { solutionPath: '', targetSet: '', updateRte: false, restartRpc: false }; constructor( - private readonly solutionManager: SolutionManager, private readonly eventHub: SolutionEventHub, private readonly configProvider: ConfigurationProvider, private readonly outputChannelProvider: OutputChannelProvider, @@ -143,7 +141,6 @@ export class SolutionConverterImpl implements SolutionConverter { let detection = false; let convertResult: rpc.ConvertSolutionResult = { success: false }; - const csolution = this.solutionManager.getCsolution(); if (!missingPacksResult || missingPacksResult.success) { // rpc method: ConvertSolution outputChannel.append('Convert solution... '); @@ -160,9 +157,18 @@ export class SolutionConverterImpl implements SolutionConverter { return; } - // compilers and variables detection handling: apply select-compiler and discover layer configurations if any - csolution?.setSelectCompiler(convertResult.selectCompiler); - detection = (!!convertResult.undefinedLayers && await this.checkDiscoverLayers()) || !!convertResult.selectCompiler; + // compilers and variables detection: gather locally and emit configure event + const availableCompilers = convertResult.selectCompiler ?? []; + detection = availableCompilers.length > 0; + let availableConfigurations: rpc.VariablesConfiguration[] | undefined; + if (convertResult.undefinedLayers) { + const discoverResult = await this.checkDiscoverLayers(); + availableConfigurations = discoverResult.configurations; + detection = detection || discoverResult.success; + } + if (detection) { + this.eventHub.fireConfigureSolutionDataReady({ availableCompilers, availableConfigurations }); + } } let logResult = undefined; @@ -244,9 +250,8 @@ export class SolutionConverterImpl implements SolutionConverter { return formattedOutput; } - private async checkDiscoverLayers(): Promise { + private async checkDiscoverLayers(): Promise<{ success: boolean; configurations: rpc.VariablesConfiguration[] | undefined }> { const outputChannel = this.outputChannelProvider.getOrCreate(manifest.CMSIS_SOLUTION_OUTPUT_CHANNEL); - this.solutionManager.getCsolution()?.setVariablesConfigurations(undefined); // rpc method: DiscoverLayers outputChannel.append('Discover Layers... '); const result = await this.cmsisToolboxManager.runCsolutionRpc( @@ -256,8 +261,7 @@ export class SolutionConverterImpl implements SolutionConverter { activeTarget: this.data?.targetSet ?? '', } ) as rpc.DiscoverLayersInfo; - this.solutionManager.getCsolution()?.setVariablesConfigurations(result.configurations); - return result.success; + return { success: result.success, configurations: result.configurations }; } private getSeverity(messages: rpc.LogMessages, lines?: string[]): Severity { diff --git a/src/solutions/solution-event-hub.test.ts b/src/solutions/solution-event-hub.test.ts index c65cfa0b..057dd34d 100644 --- a/src/solutions/solution-event-hub.test.ts +++ b/src/solutions/solution-event-hub.test.ts @@ -34,7 +34,7 @@ describe('EventHub', () => { it('should register emitters with context subscriptions', async () => { await eventHub.activate(mockContext); - expect(mockContext.subscriptions).toHaveLength(2); + expect(mockContext.subscriptions).toHaveLength(3); }); }); @@ -178,4 +178,30 @@ describe('EventHub', () => { expect(listener).toHaveBeenCalledTimes(2); }); }); + + describe('fireConfigureSolutionDataReady', () => { + it('should fire event with compilers and configurations', async () => { + const listener = jest.fn(); + eventHub.onDidConfigureSolutionDataReady(listener); + + const data = { availableCompilers: ['GCC', 'AC6'], availableConfigurations: undefined }; + await eventHub.fireConfigureSolutionDataReady(data); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener).toHaveBeenCalledWith(data); + }); + + it('should notify multiple listeners', async () => { + const listener1 = jest.fn(); + const listener2 = jest.fn(); + eventHub.onDidConfigureSolutionDataReady(listener1); + eventHub.onDidConfigureSolutionDataReady(listener2); + + const data = { availableCompilers: [], availableConfigurations: [{ variables: [] }] }; + await eventHub.fireConfigureSolutionDataReady(data); + + expect(listener1).toHaveBeenCalledWith(data); + expect(listener2).toHaveBeenCalledWith(data); + }); + }); }); diff --git a/src/solutions/solution-event-hub.ts b/src/solutions/solution-event-hub.ts index 5adbb687..de6e4613 100644 --- a/src/solutions/solution-event-hub.ts +++ b/src/solutions/solution-event-hub.ts @@ -15,7 +15,7 @@ */ import * as vscode from 'vscode'; import { constructor } from '../generic/constructor'; -import { LogMessages } from '../json-rpc/csolution-rpc-client'; +import { LogMessages, VariablesConfiguration } from '../json-rpc/csolution-rpc-client'; import { Severity } from './constants'; /** @@ -29,6 +29,14 @@ export interface ConvertRequestData { lockAbort?: boolean; } +/** + * Event data for configure solution readiness + */ +export interface ConfigureSolutionData { + availableCompilers: string[]; + availableConfigurations: VariablesConfiguration[] | undefined; +} + /** * Event data for solution conversion result */ @@ -69,6 +77,14 @@ export interface SolutionEventHub { * Event fired when solution conversion is completed */ readonly onDidConvertCompleted: vscode.Event; + /** + * Fire configure solution data ready event + */ + fireConfigureSolutionDataReady(data: ConfigureSolutionData): Promise; + /** + * Event fired when configure solution data is ready (compilers / layer configurations detected) + */ + readonly onDidConfigureSolutionDataReady: vscode.Event; } class SolutionEventHubImpl { @@ -79,9 +95,13 @@ class SolutionEventHubImpl { private readonly convertCompleteEmitter = new vscode.EventEmitter(); public readonly onDidConvertCompleted: vscode.Event = this.convertCompleteEmitter.event; + private readonly configureSolutionDataEmitter = new vscode.EventEmitter(); + public readonly onDidConfigureSolutionDataReady: vscode.Event = this.configureSolutionDataEmitter.event; + public async activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push(this.convertRequestEmitter); context.subscriptions.push(this.convertCompleteEmitter); + context.subscriptions.push(this.configureSolutionDataEmitter); } public async fireConvertRequest(data: ConvertRequestData): Promise { @@ -91,6 +111,10 @@ class SolutionEventHubImpl { public async fireConvertCompleted(data: ConvertResultData): Promise { this.convertCompleteEmitter.fire(data); } + + public async fireConfigureSolutionDataReady(data: ConfigureSolutionData): Promise { + this.configureSolutionDataEmitter.fire(data); + } } export const SolutionEventHub = constructor(SolutionEventHubImpl); diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index e2d49c42..80ed68c6 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -17,16 +17,16 @@ import { existsSync } from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { CTreeItem, ITreeItem } from '../../generic/tree-item'; import * as manifest from '../../manifest'; import { SolutionManager } from '../../solutions/solution-manager'; +import { ConfigureSolutionData, SolutionEventHub } from '../../solutions/solution-event-hub'; import { CommandsProvider } from '../../vscode-api/commands-provider'; import { MessageProvider } from '../../vscode-api/message-provider'; import { WebviewManager, WebviewManagerOptions } from '../webview-manager'; import { copyLayerToProject } from './board-layer'; import * as Messages from './messages'; import { ChangeLayerMessage } from './messages'; -import { ConfigurationVariable, LayerError, TargetConfiguration, VariableSet } from './view/state/reducer'; +import { ConfigurationVariable, TargetConfiguration, VariableSet } from './view/state/reducer'; import { LayerVariable, VariablesConfiguration, SettingsType } from '../../json-rpc/csolution-rpc-client'; function mapSet(settings: SettingsType[]): VariableSet[] { @@ -70,40 +70,6 @@ function getDataBoardLayers(configurations: VariablesConfiguration[]): TargetCon return layers; } -function checkError(error: ITreeItem) { - return error.getValueAsString(); -} - -function filterError(cBuildError: ITreeItem) { - const message = cBuildError.getValueAsString(); - const found = message.indexOf('no compatible software layer found') != -1; - return found; -} - -function checkCbuildForErrors(cBuild: ITreeItem): LayerError { - return { - name: cBuild.getValueAsString('cbuild'), - project: cBuild.getValueAsString('project'), - configuration: cBuild.getValueAsString('configuration'), - messages: cBuild.findChild(['messages', 'errors'])?. - getChildren(). - filter(filterError). - map(checkError), - }; -} - -function filterErrorsTag(cBuild: ITreeItem) { - const cbuildErrors = cBuild.getValueAsString('errors') == 'true' ? true : false; - const cbuildMessages = !!cBuild.findChild(['messages', 'errors'])?.getChildren().filter(filterError).length; - return cbuildErrors && cbuildMessages; -} - -function getAvailableCbuildErrors(avCBuilds: ITreeItem[]) { - return avCBuilds?. - filter(filterErrorsTag). - map(checkCbuildForErrors); -} - export const MANAGE_LAYERS_WEBVIEW_OPTIONS: Readonly = { title: 'Configure Solution', scriptPath: path.join('dist', 'views', 'configureSolution.js'), @@ -117,16 +83,17 @@ export const MANAGE_LAYERS_WEBVIEW_OPTIONS: Readonly = { export class ManageLayersWebviewMain { private readonly webviewManager: WebviewManager; + private latestConfigureData: ConfigureSolutionData | undefined; constructor( readonly context: vscode.ExtensionContext, private readonly commandsProvider: CommandsProvider, private readonly messageProvider: MessageProvider, private readonly solutionManager: SolutionManager, + private readonly eventHub: SolutionEventHub, webviewManager?: WebviewManager, ) { this.webviewManager = webviewManager || new WebviewManager(context, MANAGE_LAYERS_WEBVIEW_OPTIONS, this.commandsProvider); - this.solutionManager.onLoadedBuildFiles((() => this.onLoadedBuildFiles()).bind(this)); } private async sendPlatform() { @@ -137,7 +104,7 @@ export class ManageLayersWebviewMain { private async applyOrChangeLayer(layer: TargetConfiguration) { const csolution = this.solutionManager.getCsolution(); - if (csolution && csolution.variablesConfigurations) { + if (csolution && this.latestConfigureData?.availableConfigurations) { const activeTarget = csolution?.getActiveTargetType() || ''; const activeTargetTypeItem = csolution.getDefaultTargetTypeItem(activeTarget); if (!activeTargetTypeItem || !activeTarget) { @@ -163,8 +130,8 @@ export class ManageLayersWebviewMain { } } }); - await csolution.csolutionYml.save(); - await copyLayerToProject(layer, csolution.solutionDir); + await copyLayerToProject(layer, csolution.solutionDir); // first copy layer files + await csolution.csolutionYml.save(); // then save csolution.yml } } @@ -197,22 +164,13 @@ export class ManageLayersWebviewMain { public async activate(context: vscode.ExtensionContext): Promise { context.subscriptions.push( this.webviewManager.onDidReceiveMessage(this.handleMessage, this), + this.eventHub.onDidConfigureSolutionDataReady(this.onConfigureSolutionDataReady, this), ); this.webviewManager.activate(context); } - private onLoadedBuildFiles(): void { - const csolution = this.solutionManager.getCsolution(); - const activeTarget = csolution?.getActiveTargetType() || ''; - if (!csolution) { - this.webviewManager.sendMessage({ - type: 'BOARD_LAYER_NODATA', - activeTargetType: activeTarget, - availableCompilers: [], - }); - return; - } - + private onConfigureSolutionDataReady(data: ConfigureSolutionData): void { + this.latestConfigureData = data; this.getDataUserChoice(); } @@ -224,13 +182,13 @@ export class ManageLayersWebviewMain { this.webviewManager.createOrShowPanel(); } - private sendConfigurations(configurations: VariablesConfiguration[] | undefined, activeTarget: string, avComps: string[] | undefined) { + private sendConfigurations(configurations: VariablesConfiguration[] | undefined, activeTarget: string, avComps: string[]) { if (!configurations) { this.webviewManager.sendMessage({ type: 'BOARD_LAYER_NODATA', activeTargetType: activeTarget, - availableCompilers: avComps ?? [], + availableCompilers: avComps, }); return; } @@ -240,52 +198,24 @@ export class ManageLayersWebviewMain { type: 'BOARD_LAYER_DATA', layers, activeTargetType: activeTarget, - availableCompilers: avComps ?? [], + availableCompilers: avComps, }); } - private checkForLayerErrors(avCBuilds: ITreeItem[] | undefined): boolean { - if (!avCBuilds) { - return false; - } - - const layerErrors = getAvailableCbuildErrors(avCBuilds); - this.webviewManager.sendMessage({ - type: 'BOARD_LAYER_DATA_ERRORS', - layerErrors, - }); - - // send message to GUI with error strins - - return !!layerErrors?.length; - } - - private getDataUserChoice() { - const csolution = this.solutionManager.getCsolution(); - const cBuildIdxYml = csolution?.cbuildIdxYmlRoot; - if (!cBuildIdxYml) { - this.webviewManager.sendMessage({ - type: 'LOADING' - }); + private getDataUserChoice(): void { + if (!this.latestConfigureData) { return; } + const { availableCompilers, availableConfigurations } = this.latestConfigureData; + const activeTarget = this.solutionManager.getCsolution()?.getActiveTargetType() ?? ''; - const avComps = csolution?.selectCompiler; - const configurations = csolution?.variablesConfigurations; - const avCBuilds = cBuildIdxYml.findChild(['build-idx', 'cbuilds'])?.getChildren(); - const avErrors = this.checkForLayerErrors(avCBuilds); - const activeTarget = csolution?.getActiveTargetType() || ''; - - if (avComps?.length == 1) { // just one compiler, add it automagically - const availableCompilers = avComps; - if (availableCompilers.length) { - this.applyOrChangeCompiler(availableCompilers[0]); - return; // return here as the change triggers cbuild and another reload - } + if (availableCompilers.length === 1) { // just one compiler, add it automagically + this.applyOrChangeCompiler(availableCompilers[0]); + return; // return here as the change triggers cbuild and another reload } - this.showPanel(!!avComps || !!configurations || avErrors); - this.sendConfigurations(configurations, activeTarget, avComps); + this.showPanel(!!availableCompilers.length || !!availableConfigurations); + this.sendConfigurations(availableConfigurations, activeTarget, availableCompilers); } private localFolderExists(localPath: string): boolean { From e7d082b0b6a5d3d0faf81fb3d06fc9a2d4fa386b Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 14:30:47 +0200 Subject: [PATCH 2/8] resolve merge conflict --- src/solutions/solution-converter.ts | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/src/solutions/solution-converter.ts b/src/solutions/solution-converter.ts index f6ebd5fc..847cc6c9 100644 --- a/src/solutions/solution-converter.ts +++ b/src/solutions/solution-converter.ts @@ -162,21 +162,16 @@ export class SolutionConverterImpl implements SolutionConverter { detection = availableCompilers.length > 0; let availableConfigurations: rpc.VariablesConfiguration[] | undefined; if (convertResult.undefinedLayers) { - const discoverResult = await this.checkDiscoverLayers(); - availableConfigurations = discoverResult.configurations; - detection = detection || discoverResult.success; + const result = await this.checkDiscoverLayers(); + const discoverLayersOutput = !result.success && result.message ? [`error csolution: ${result.message.trim()}`] : []; + toolsOutputMessages = toolsOutputMessages.concat(discoverLayersOutput); + availableConfigurations = result.configurations; + detection = detection || result.success; } if (detection) { + // compilers and variables detection handling: apply select-compiler and discover layer configurations if any this.eventHub.fireConfigureSolutionDataReady({ availableCompilers, availableConfigurations }); } - // compilers and variables detection handling: apply select-compiler and discover layer configurations if any - csolution?.setSelectCompiler(convertResult.selectCompiler); - if (convertResult.undefinedLayers) { - const [discoverLayersDetected, discoverLayersOutput] = await this.checkDiscoverLayers(); - detection = discoverLayersDetected; - toolsOutputMessages = toolsOutputMessages.concat(discoverLayersOutput); - } - detection = detection || !!convertResult.selectCompiler; } let logResult = undefined; @@ -258,7 +253,7 @@ export class SolutionConverterImpl implements SolutionConverter { return formattedOutput; } - private async checkDiscoverLayers(): Promise<[boolean, string[]]> { + private async checkDiscoverLayers() { const outputChannel = this.outputChannelProvider.getOrCreate(manifest.CMSIS_SOLUTION_OUTPUT_CHANNEL); // rpc method: DiscoverLayers outputChannel.append('Discover Layers... '); @@ -269,10 +264,7 @@ export class SolutionConverterImpl implements SolutionConverter { activeTarget: this.data?.targetSet ?? '', } ) as rpc.DiscoverLayersInfo; - return { success: result.success, configurations: result.configurations }; - this.solutionManager.getCsolution()?.setVariablesConfigurations(result.configurations); - const formattedOutput = !result.success && result.message ? [`error csolution: ${result.message.trim()}`] : []; - return [result.success, formattedOutput]; + return result; } private getSeverity(messages: rpc.LogMessages, lines?: string[]): Severity { From 1737f996730fa493b499d7cf93880dee1ed7c910 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:03:46 +0200 Subject: [PATCH 3/8] Address copilot suggestions --- src/solutions/solution-converter.ts | 13 +++++++------ .../manage-layers/manage-layers-webview-main.ts | 17 ++++++++++------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/solutions/solution-converter.ts b/src/solutions/solution-converter.ts index 847cc6c9..7943e257 100644 --- a/src/solutions/solution-converter.ts +++ b/src/solutions/solution-converter.ts @@ -141,6 +141,8 @@ export class SolutionConverterImpl implements SolutionConverter { let detection = false; let convertResult: rpc.ConvertSolutionResult = { success: false }; + let availableCompilers: string[] = []; + let availableConfigurations: rpc.VariablesConfiguration[] | undefined; if (!missingPacksResult || missingPacksResult.success) { // rpc method: ConvertSolution outputChannel.append('Convert solution... '); @@ -158,9 +160,8 @@ export class SolutionConverterImpl implements SolutionConverter { } // compilers and variables detection: gather locally and emit configure event - const availableCompilers = convertResult.selectCompiler ?? []; + availableCompilers = convertResult.selectCompiler ?? []; detection = availableCompilers.length > 0; - let availableConfigurations: rpc.VariablesConfiguration[] | undefined; if (convertResult.undefinedLayers) { const result = await this.checkDiscoverLayers(); const discoverLayersOutput = !result.success && result.message ? [`error csolution: ${result.message.trim()}`] : []; @@ -168,10 +169,6 @@ export class SolutionConverterImpl implements SolutionConverter { availableConfigurations = result.configurations; detection = detection || result.success; } - if (detection) { - // compilers and variables detection handling: apply select-compiler and discover layer configurations if any - this.eventHub.fireConfigureSolutionDataReady({ availableCompilers, availableConfigurations }); - } } let logResult = undefined; @@ -216,6 +213,10 @@ export class SolutionConverterImpl implements SolutionConverter { logMessages: logResult, toolsOutputMessages, }); + // compilers and variables detection handling: + // apply select-compiler and discover layer configurations, reset state otherwise + this.eventHub.fireConfigureSolutionDataReady({ availableCompilers, availableConfigurations }); + } private async printErrorsWarnings(messages?: rpc.LogMessages): Promise { diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index 80ed68c6..ccaad2da 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -100,7 +100,6 @@ export class ManageLayersWebviewMain { this.webviewManager.sendMessage({ type: 'PLATFORM', data: { name: 'vscode' } }); } - private async applyOrChangeLayer(layer: TargetConfiguration) { const csolution = this.solutionManager.getCsolution(); @@ -130,6 +129,7 @@ export class ManageLayersWebviewMain { } } }); + this.latestConfigureData = undefined; await copyLayerToProject(layer, csolution.solutionDir); // first copy layer files await csolution.csolutionYml.save(); // then save csolution.yml } @@ -170,16 +170,19 @@ export class ManageLayersWebviewMain { } private onConfigureSolutionDataReady(data: ConfigureSolutionData): void { - this.latestConfigureData = data; - this.getDataUserChoice(); + if (data.availableCompilers.length === 0 && !data.availableConfigurations) { + this.latestConfigureData = undefined; + } else { + + this.latestConfigureData = data; + this.getDataUserChoice(); + } } private showPanel(show: boolean) { - if (!show) { - return; + if (show) { + this.webviewManager.createOrShowPanel(); } - - this.webviewManager.createOrShowPanel(); } private sendConfigurations(configurations: VariablesConfiguration[] | undefined, activeTarget: string, avComps: string[]) { From c2a86a4d371d87c0409c47cb76c65a0057be1585 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:15:59 +0200 Subject: [PATCH 4/8] unit tests for the view --- .../manage-layers-webview-main.test.ts | 245 ++++++++++++++++++ .../manage-layers-webview-main.ts | 12 +- 2 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 src/views/manage-layers/manage-layers-webview-main.test.ts diff --git a/src/views/manage-layers/manage-layers-webview-main.test.ts b/src/views/manage-layers/manage-layers-webview-main.test.ts new file mode 100644 index 00000000..c2ad65bc --- /dev/null +++ b/src/views/manage-layers/manage-layers-webview-main.test.ts @@ -0,0 +1,245 @@ +/** + * 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 + * + * https://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 'jest'; +import * as vscode from 'vscode'; +import { waitTimeout } from '../../__test__/test-waits'; +import { csolutionFactory, CSolutionMock } from '../../solutions/csolution.factory'; +import { SolutionEventHub } from '../../solutions/solution-event-hub'; +import { MockSolutionManager, solutionManagerFactory } from '../../solutions/solution-manager.factories'; +import { MockCommandsProvider, commandsProviderFactory } from '../../vscode-api/commands-provider.factories'; +import { MockMessageProvider, messageProviderFactory } from '../../vscode-api/message-provider.factories'; +import { getMockWebViewManager, MockWebviewManager } from '../__test__/mock-webview-manager'; +import { WebviewManager } from '../webview-manager'; +import { copyLayerToProject } from './board-layer'; +import { ManageLayersWebviewMain } from './manage-layers-webview-main'; +import * as Messages from './messages'; +import { TargetConfiguration } from './view/state/reducer'; + +jest.mock('./board-layer', () => ({ + copyLayerToProject: jest.fn().mockResolvedValue(undefined), +})); + +describe('ManageLayersWebviewMain', () => { + let manageLayersWebviewMain: ManageLayersWebviewMain; + let solutionManager: MockSolutionManager; + let csolution: CSolutionMock; + let eventHub: SolutionEventHub; + let commandsProvider: MockCommandsProvider; + let messageProvider: MockMessageProvider; + let webviewManager: MockWebviewManager; + let context: { subscriptions: vscode.Disposable[] }; + let compilerNode: { setValue: jest.Mock }; + let variablesNode: { getChildByValue: jest.Mock; createChild: jest.Mock }; + let variableItem: { setValue: jest.Mock }; + let targetTypeItem: { createChild: jest.Mock }; + let save: jest.Mock; + + const mockedCopyLayerToProject = jest.mocked(copyLayerToProject); + + const configureVariable = { + name: 'BOARD_LAYER', + clayer: 'Vendor::Board.Layer', + description: 'Board layer', + settings: [{ set: 'Debug' }], + path: '/source/layer', + file: 'board.clayer.yml', + 'copy-to': 'Config/Layer', + }; + + const configuredLayer: TargetConfiguration = { + variables: [{ + variableName: 'BOARD_LAYER', + variableValue: 'Vendor::Board.Layer', + description: 'Board layer', + settings: [{ set: 'Debug' }], + path: '/source/layer', + file: 'board.clayer.yml', + copyTo: 'Config/Layer', + copyToOrig: 'Config/Layer', + disabled: false, + }], + }; + + async function fireOutgoingMessage(message: Messages.OutgoingMessage): Promise { + webviewManager.didReceiveMessageEmitter.fire(message); + await waitTimeout(); + } + + async function fireConfigureSolutionDataReady(data: { availableCompilers: string[]; availableConfigurations: Array<{ variables: typeof configureVariable[] }> | undefined }): Promise { + await eventHub.fireConfigureSolutionDataReady(data); + await waitTimeout(); + } + + beforeEach(async () => { + compilerNode = { setValue: jest.fn() }; + variableItem = { setValue: jest.fn().mockReturnThis() }; + variablesNode = { + getChildByValue: jest.fn().mockReturnValue(undefined), + createChild: jest.fn().mockReturnValue(variableItem), + }; + targetTypeItem = { + createChild: jest.fn().mockReturnValue(variablesNode), + }; + save = jest.fn().mockResolvedValue(undefined); + + csolution = csolutionFactory({ + solutionDir: 'C:/workspace/solution', + getActiveTargetType: jest.fn().mockReturnValue('Debug'), + getDefaultTargetTypeItem: jest.fn().mockReturnValue(targetTypeItem as never), + csolutionYml: { + topItem: compilerNode, + save, + } as never, + }); + + solutionManager = solutionManagerFactory(); + solutionManager.getCsolution.mockReturnValue(csolution); + eventHub = new SolutionEventHub(); + commandsProvider = commandsProviderFactory(); + messageProvider = messageProviderFactory(); + webviewManager = getMockWebViewManager(); + context = { subscriptions: [] }; + mockedCopyLayerToProject.mockResolvedValue(undefined); + + manageLayersWebviewMain = new ManageLayersWebviewMain( + context as unknown as vscode.ExtensionContext, + commandsProvider, + messageProvider, + solutionManager, + eventHub, + webviewManager as unknown as WebviewManager, + ); + + await manageLayersWebviewMain.activate(context as unknown as vscode.ExtensionContext); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('activates and registers message/event subscriptions', () => { + expect(webviewManager.activate).toHaveBeenCalledWith(context); + expect(context.subscriptions).toHaveLength(2); + }); + + it('opens the panel and sends board layer data when configurations are available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + + expect(webviewManager.createOrShowPanel).toHaveBeenCalledTimes(1); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'BOARD_LAYER_DATA', + layers: [configuredLayer], + activeTargetType: 'Debug', + availableCompilers: ['AC6', 'GCC'], + }); + }); + + it('opens the panel and sends no-data when only multiple compilers are available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: undefined, + }); + + expect(webviewManager.createOrShowPanel).toHaveBeenCalledTimes(1); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'BOARD_LAYER_NODATA', + activeTargetType: 'Debug', + availableCompilers: ['AC6', 'GCC'], + }); + }); + + it('auto-applies compiler when exactly one compiler is available', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6'], + availableConfigurations: undefined, + }); + + expect(compilerNode.setValue).toHaveBeenCalledWith('compiler', 'AC6'); + expect(save).toHaveBeenCalledTimes(1); + expect(webviewManager.createOrShowPanel).not.toHaveBeenCalled(); + expect(webviewManager.sendMessage).not.toHaveBeenCalled(); + }); + + it('handles CHECK_LAYER_DOES_NOT_EXIST without sending a success acknowledgement', async () => { + await fireOutgoingMessage({ + type: 'CHECK_LAYER_DOES_NOT_EXIST', + layerFolder: '__definitely_missing_layer_path__', + variableId: 7, + }); + + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'RESULT_LAYER_EXISTS_CHECK', + variableId: 7, + result: false, + }); + expect(webviewManager.sendMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'REQUEST_SUCCESSFUL' })); + }); + + it('applies layer and compiler from APPLY_CONFIGURE', async () => { + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + webviewManager.sendMessage.mockClear(); + save.mockClear(); + + await fireOutgoingMessage({ + type: 'APPLY_CONFIGURE', + layer: configuredLayer, + compiler: 'GCC', + }); + + expect(targetTypeItem.createChild).toHaveBeenCalledWith('variables', true); + expect(variablesNode.getChildByValue).toHaveBeenCalledWith('BOARD_LAYER'); + expect(variablesNode.createChild).toHaveBeenCalledWith('-', false); + expect(variableItem.setValue).toHaveBeenNthCalledWith(1, 'BOARD_LAYER', '$SolutionDir()$/Config/Layer/board.clayer.yml'); + expect(variableItem.setValue).toHaveBeenNthCalledWith(2, 'copied-from', 'Vendor::Board.Layer'); + expect(mockedCopyLayerToProject).toHaveBeenCalledWith(configuredLayer, 'C:/workspace/solution'); + expect(compilerNode.setValue).toHaveBeenCalledWith('compiler', 'GCC'); + expect(save).toHaveBeenCalledTimes(2); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'REQUEST_SUCCESSFUL', + requestType: 'APPLY_CONFIGURE', + }); + }); + + it('reports apply failures without sending a success acknowledgement', async () => { + mockedCopyLayerToProject.mockRejectedValueOnce(new Error('copy failed')); + await fireConfigureSolutionDataReady({ + availableCompilers: ['AC6', 'GCC'], + availableConfigurations: [{ variables: [configureVariable] }], + }); + webviewManager.sendMessage.mockClear(); + + await fireOutgoingMessage({ + type: 'APPLY_CONFIGURE', + layer: configuredLayer, + compiler: 'GCC', + }); + + expect(messageProvider.showErrorMessage).toHaveBeenCalledWith('Failed to apply changes: copy failed'); + expect(webviewManager.sendMessage).toHaveBeenCalledWith({ + type: 'REQUEST_FAILED', + requestType: 'APPLY_CONFIGURE', + errorMessage: 'Failed to apply changes: copy failed', + }); + expect(webviewManager.sendMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'REQUEST_SUCCESSFUL' })); + }); +}); \ No newline at end of file diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index ccaad2da..d1a35be6 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -145,10 +145,11 @@ export class ManageLayersWebviewMain { } - private async applyConfiguration(message: ChangeLayerMessage): Promise { + private async applyConfiguration(message: ChangeLayerMessage): Promise { try { await this.applyOrChangeLayer(message.layer); await this.applyOrChangeCompiler(message.compiler); + return true; } catch (error) { const errorMessage = error instanceof Error ? error.message : error; this.messageProvider.showErrorMessage(`Failed to apply changes: ${errorMessage}`); @@ -157,7 +158,7 @@ export class ManageLayersWebviewMain { requestType: 'APPLY_CONFIGURE', errorMessage: `Failed to apply changes: ${errorMessage}` }); - return; + return false; } } @@ -232,9 +233,10 @@ export class ManageLayersWebviewMain { private async handleMessage(message: Messages.OutgoingMessage): Promise { try { + let successful = true; switch (message.type) { case 'APPLY_CONFIGURE': - await this.applyConfiguration(message); + successful = await this.applyConfiguration(message); break; case 'GET_PLATFORM': await this.sendPlatform(); @@ -251,7 +253,9 @@ export class ManageLayersWebviewMain { return; // early exit } } - this.webviewManager.sendMessage({ type: 'REQUEST_SUCCESSFUL', requestType: message.type }); + if (successful) { + this.webviewManager.sendMessage({ type: 'REQUEST_SUCCESSFUL', requestType: message.type }); + } } catch (err) { const _err = err as Error; this.messageProvider.showErrorMessage(`Solution service failure: ${_err.message}\n${_err.stack}`); From 5e6d6ec8d7746e393c5d575744231554ae4032af Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:17:48 +0200 Subject: [PATCH 5/8] Update src/views/manage-layers/manage-layers-webview-main.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/views/manage-layers/manage-layers-webview-main.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index d1a35be6..7dd7aaff 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -207,11 +207,13 @@ export class ManageLayersWebviewMain { } private getDataUserChoice(): void { + const activeTarget = this.solutionManager.getCsolution()?.getActiveTargetType() ?? ''; + if (!this.latestConfigureData) { + this.sendConfigurations(undefined, activeTarget, []); return; } const { availableCompilers, availableConfigurations } = this.latestConfigureData; - const activeTarget = this.solutionManager.getCsolution()?.getActiveTargetType() ?? ''; if (availableCompilers.length === 1) { // just one compiler, add it automagically this.applyOrChangeCompiler(availableCompilers[0]); From 6b4cfd22ab0ffd17cd9c0497feb5999179afe73e Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:27:40 +0200 Subject: [PATCH 6/8] Update src/views/manage-layers/manage-layers-webview-main.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/views/manage-layers/manage-layers-webview-main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index 7dd7aaff..8505c63e 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -129,9 +129,9 @@ export class ManageLayersWebviewMain { } } }); - this.latestConfigureData = undefined; await copyLayerToProject(layer, csolution.solutionDir); // first copy layer files await csolution.csolutionYml.save(); // then save csolution.yml + this.latestConfigureData = undefined; } } From 4326603d77d6cf9985d81ba282f7332ea4722ce8 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:28:33 +0200 Subject: [PATCH 7/8] Update src/views/manage-layers/manage-layers-webview-main.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/views/manage-layers/manage-layers-webview-main.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/views/manage-layers/manage-layers-webview-main.ts b/src/views/manage-layers/manage-layers-webview-main.ts index 8505c63e..13e6f6ef 100644 --- a/src/views/manage-layers/manage-layers-webview-main.ts +++ b/src/views/manage-layers/manage-layers-webview-main.ts @@ -173,6 +173,7 @@ export class ManageLayersWebviewMain { private onConfigureSolutionDataReady(data: ConfigureSolutionData): void { if (data.availableCompilers.length === 0 && !data.availableConfigurations) { this.latestConfigureData = undefined; + this.sendConfigurations(undefined, '', []); } else { this.latestConfigureData = data; From 52f863ba2566cc87c24a7a13409986af4bfbea70 Mon Sep 17 00:00:00 2001 From: Evgueni Driouk Date: Wed, 8 Apr 2026 15:38:48 +0200 Subject: [PATCH 8/8] lint --- src/views/manage-layers/manage-layers-webview-main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/manage-layers/manage-layers-webview-main.test.ts b/src/views/manage-layers/manage-layers-webview-main.test.ts index c2ad65bc..c2ede7e2 100644 --- a/src/views/manage-layers/manage-layers-webview-main.test.ts +++ b/src/views/manage-layers/manage-layers-webview-main.test.ts @@ -242,4 +242,4 @@ describe('ManageLayersWebviewMain', () => { }); expect(webviewManager.sendMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: 'REQUEST_SUCCESSFUL' })); }); -}); \ No newline at end of file +});