fix: invalid custom board option handling in FQBN

Closes arduino/arduino-ide#1588

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2024-02-08 16:30:09 +01:00
committed by Akos Kitta
parent 2a325a5b74
commit a088ba99f5
11 changed files with 586 additions and 139 deletions

View File

@@ -12,6 +12,7 @@ import { ILogger } from '@theia/core/lib/common/logger';
import { deepClone, deepFreeze } from '@theia/core/lib/common/objects';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, named } from '@theia/core/shared/inversify';
import { FQBN } from 'fqbn';
import {
BoardDetails,
BoardsService,
@@ -20,6 +21,7 @@ import {
Programmer,
isBoardIdentifierChangeEvent,
isProgrammer,
sanitizeFqbn,
} from '../../common/protocol';
import { notEmpty } from '../../common/utils';
import type {
@@ -29,6 +31,14 @@ import type {
import { NotificationCenter } from '../notification-center';
import { BoardsServiceProvider } from './boards-service-provider';
export interface SelectConfigOptionParams {
readonly fqbn: string;
readonly optionsToUpdate: readonly Readonly<{
option: string;
selectedValue: string;
}>[];
}
@injectable()
export class BoardsDataStore
implements
@@ -64,7 +74,12 @@ export class BoardsDataStore
this.toDispose.pushAll([
this.boardsServiceProvider.onBoardsConfigDidChange((event) => {
if (isBoardIdentifierChangeEvent(event)) {
this.updateSelectedBoardData(event.selectedBoard?.fqbn);
this.updateSelectedBoardData(
event.selectedBoard?.fqbn,
// If the change event comes from toolbar and the FQBN contains custom board options, change the currently selected options
// https://github.com/arduino/arduino-ide/issues/1588
event.reason === 'toolbar'
);
}
}),
this.notificationCenter.onPlatformDidInstall(async ({ item }) => {
@@ -116,7 +131,7 @@ export class BoardsDataStore
if (!fqbn) {
return undefined;
} else {
const data = await this.getData(fqbn);
const data = await this.getData(sanitizeFqbn(fqbn));
if (data === BoardsDataStore.Data.EMPTY) {
return undefined;
}
@@ -125,9 +140,22 @@ export class BoardsDataStore
}
private async updateSelectedBoardData(
fqbn: string | undefined
fqbn: string | undefined,
updateConfigOptions = false
): Promise<void> {
this._selectedBoardData = await this.getSelectedBoardData(fqbn);
if (fqbn && updateConfigOptions) {
const { options } = new FQBN(fqbn);
if (options) {
const optionsToUpdate = Object.entries(options).map(([key, value]) => ({
option: key,
selectedValue: value,
}));
const params = { fqbn, optionsToUpdate };
await this.selectConfigOption(params);
this._selectedBoardData = await this.getSelectedBoardData(fqbn); // reload the updated data
}
}
}
onStop(): void {
@@ -168,7 +196,7 @@ export class BoardsDataStore
return undefined;
}
const { configOptions } = await this.getData(fqbn);
return ConfigOption.decorate(fqbn, configOptions);
return new FQBN(fqbn).withConfigOptions(...configOptions).toString();
}
async getData(fqbn: string | undefined): Promise<BoardsDataStore.Data> {
@@ -201,48 +229,63 @@ export class BoardsDataStore
fqbn: string;
selectedProgrammer: Programmer;
}): Promise<boolean> {
const storedData = deepClone(await this.getData(fqbn));
const sanitizedFQBN = sanitizeFqbn(fqbn);
const storedData = deepClone(await this.getData(sanitizedFQBN));
const { programmers } = storedData;
if (!programmers.find((p) => Programmer.equals(selectedProgrammer, p))) {
return false;
}
const data = { ...storedData, selectedProgrammer };
await this.setData({ fqbn, data });
this.fireChanged({ fqbn, data });
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: { ...storedData, selectedProgrammer },
};
await this.setData(change);
this.fireChanged(change);
return true;
}
async selectConfigOption({
fqbn,
option,
selectedValue,
}: {
fqbn: string;
option: string;
selectedValue: string;
}): Promise<boolean> {
const data = deepClone(await this.getData(fqbn));
const { configOptions } = data;
const configOption = configOptions.find((c) => c.option === option);
if (!configOption) {
async selectConfigOption(params: SelectConfigOptionParams): Promise<boolean> {
const { fqbn, optionsToUpdate } = params;
if (!optionsToUpdate.length) {
return false;
}
let updated = false;
for (const value of configOption.values) {
const mutable: Mutable<ConfigValue> = value;
if (mutable.value === selectedValue) {
mutable.selected = true;
updated = true;
} else {
mutable.selected = false;
const sanitizedFQBN = sanitizeFqbn(fqbn);
const mutableData = deepClone(await this.getData(sanitizedFQBN));
let didChange = false;
for (const { option, selectedValue } of optionsToUpdate) {
const { configOptions } = mutableData;
const configOption = configOptions.find((c) => c.option === option);
if (configOption) {
const configOptionValueIndex = configOption.values.findIndex(
(configOptionValue) => configOptionValue.value === selectedValue
);
if (configOptionValueIndex >= 0) {
// unselect all
configOption.values
.map((value) => value as Mutable<ConfigValue>)
.forEach((value) => (value.selected = false));
const mutableConfigValue: Mutable<ConfigValue> =
configOption.values[configOptionValueIndex];
// make the new value `selected`
mutableConfigValue.selected = true;
didChange = true;
}
}
}
if (!updated) {
if (!didChange) {
return false;
}
await this.setData({ fqbn, data });
this.fireChanged({ fqbn, data });
const change: BoardsDataStoreChange = {
fqbn: sanitizedFQBN,
data: mutableData,
};
await this.setData(change);
this.fireChanged(change);
return true;
}

View File

@@ -12,6 +12,7 @@ import { Emitter } from '@theia/core/lib/common/event';
import { ILogger } from '@theia/core/lib/common/logger';
import { MessageService } from '@theia/core/lib/common/message-service';
import { nls } from '@theia/core/lib/common/nls';
import { deepClone } from '@theia/core/lib/common/objects';
import { Deferred } from '@theia/core/lib/common/promise-util';
import type { Mutable } from '@theia/core/lib/common/types';
import { inject, injectable, optional } from '@theia/core/shared/inversify';
@@ -21,31 +22,32 @@ import {
} from '@theia/output/lib/browser/output-channel';
import {
BoardIdentifier,
boardIdentifierEquals,
BoardUserField,
BoardWithPackage,
BoardsConfig,
BoardsConfigChangeEvent,
BoardsPackage,
BoardsService,
BoardUserField,
BoardWithPackage,
DetectedPorts,
Port,
PortIdentifier,
boardIdentifierEquals,
emptyBoardsConfig,
isBoardIdentifier,
isBoardIdentifierChangeEvent,
isPortIdentifier,
isPortIdentifierChangeEvent,
Port,
PortIdentifier,
portIdentifierEquals,
sanitizeFqbn,
serializePlatformIdentifier,
} from '../../common/protocol';
import {
BoardList,
BoardListHistory,
createBoardList,
EditBoardsConfigActionParams,
isBoardListHistory,
SelectBoardsConfigActionParams,
createBoardList,
isBoardListHistory,
} from '../../common/protocol/board-list';
import type { Defined } from '../../common/types';
import type {
@@ -104,6 +106,21 @@ type BoardListHistoryUpdateResult =
type BoardToSelect = BoardIdentifier | undefined | 'ignore-board';
type PortToSelect = PortIdentifier | undefined | 'ignore-port';
function sanitizeBoardToSelectFQBN(board: BoardToSelect): BoardToSelect {
if (isBoardIdentifier(board)) {
return sanitizeBoardIdentifierFQBN(board);
}
return board;
}
function sanitizeBoardIdentifierFQBN(board: BoardIdentifier): BoardIdentifier {
if (board.fqbn) {
const copy: Mutable<BoardIdentifier> = deepClone(board);
copy.fqbn = sanitizeFqbn(board.fqbn);
return copy;
}
return board;
}
interface UpdateBoardListHistoryParams {
readonly portToSelect: PortToSelect;
readonly boardToSelect: BoardToSelect;
@@ -136,6 +153,9 @@ export interface BoardListUIActions {
}
export type BoardListUI = BoardList & BoardListUIActions;
export type BoardsConfigChangeEventUI = BoardsConfigChangeEvent &
Readonly<{ reason?: UpdateBoardsConfigReason }>;
@injectable()
export class BoardListDumper implements Disposable {
@inject(OutputChannelManager)
@@ -190,7 +210,7 @@ export class BoardsServiceProvider
private _ready = new Deferred<void>();
private readonly boardsConfigDidChangeEmitter =
new Emitter<BoardsConfigChangeEvent>();
new Emitter<BoardsConfigChangeEventUI>();
readonly onBoardsConfigDidChange = this.boardsConfigDidChangeEmitter.event;
private readonly boardListDidChangeEmitter = new Emitter<BoardListUI>();
@@ -353,7 +373,8 @@ export class BoardsServiceProvider
portToSelect !== 'ignore-port' &&
!portIdentifierEquals(portToSelect, previousSelectedPort);
const boardDidChangeEvent = boardDidChange
? { selectedBoard: boardToSelect, previousSelectedBoard }
? // The change event must always contain any custom board options. Hence the board to select is not sanitized.
{ selectedBoard: boardToSelect, previousSelectedBoard }
: undefined;
const portDidChangeEvent = portDidChange
? { selectedPort: portToSelect, previousSelectedPort }
@@ -374,16 +395,31 @@ export class BoardsServiceProvider
return false;
}
this.maybeUpdateBoardListHistory({ portToSelect, boardToSelect });
this.maybeUpdateBoardsData({ boardToSelect, reason });
// unlike for the board change event, every persistent state must not contain custom board config options in the FQBN
const sanitizedBoardToSelect = sanitizeBoardToSelectFQBN(boardToSelect);
this.maybeUpdateBoardListHistory({
portToSelect,
boardToSelect: sanitizedBoardToSelect,
});
this.maybeUpdateBoardsData({
boardToSelect: sanitizedBoardToSelect,
reason,
});
if (isBoardIdentifierChangeEvent(event)) {
this._boardsConfig.selectedBoard = event.selectedBoard;
this._boardsConfig.selectedBoard = event.selectedBoard
? sanitizeBoardIdentifierFQBN(event.selectedBoard)
: event.selectedBoard;
}
if (isPortIdentifierChangeEvent(event)) {
this._boardsConfig.selectedPort = event.selectedPort;
}
if (reason) {
event = Object.assign(event, { reason });
}
this.boardsConfigDidChangeEmitter.fire(event);
this.refreshBoardList();
this.saveState();

View File

@@ -87,8 +87,7 @@ export class BoardsDataMenuUpdater extends Contribution {
execute: () =>
this.boardsDataStore.selectConfigOption({
fqbn,
option,
selectedValue: value.value,
optionsToUpdate: [{ option, selectedValue: value.value }],
}),
isToggled: () => value.selected,
};

View File

@@ -9,7 +9,6 @@ import {
BoardIdentifier,
BoardsService,
ExecutableService,
assertSanitizedFqbn,
isBoardIdentifierChangeEvent,
sanitizeFqbn,
} from '../../common/protocol';
@@ -159,14 +158,11 @@ export class InoLanguage extends SketchContribution {
this.notificationCenter.onDidReinitialize(() => forceRestart()),
this.boardDataStore.onDidChange((event) => {
if (this.languageServerFqbn) {
const sanitizedFqbn = sanitizeFqbn(this.languageServerFqbn);
if (!sanitizeFqbn) {
throw new Error(
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
);
}
const sanitizedFQBN = sanitizeFqbn(this.languageServerFqbn);
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison.
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328
const matchingChange = event.changes.find(
(change) => change.fqbn === sanitizedFqbn
(change) => sanitizedFQBN === sanitizeFqbn(change.fqbn)
);
const { boardsConfig } = this.boardsServiceProvider;
if (
@@ -228,7 +224,6 @@ export class InoLanguage extends SketchContribution {
}
return;
}
assertSanitizedFqbn(fqbn);
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
if (!fqbnWithConfig) {
throw new Error(

View File

@@ -1,7 +1,8 @@
import { Emitter } from '@theia/core/lib/common/event';
import { nls } from '@theia/core/lib/common/nls';
import { inject, injectable } from '@theia/core/shared/inversify';
import { CoreService, sanitizeFqbn } from '../../common/protocol';
import { FQBN } from 'fqbn';
import { CoreService } from '../../common/protocol';
import { ArduinoMenus } from '../menu/arduino-menus';
import { CurrentSketch } from '../sketches-service-client-impl';
import { ArduinoToolbar } from '../toolbar/arduino-toolbar';
@@ -173,7 +174,11 @@ export class UploadSketch extends CoreServiceContribution {
const [fqbn, { selectedProgrammer: programmer }, verify, verbose] =
await Promise.all([
verifyOptions.fqbn, // already decorated FQBN
this.boardsDataStore.getData(sanitizeFqbn(verifyOptions.fqbn)),
this.boardsDataStore.getData(
verifyOptions.fqbn
? new FQBN(verifyOptions.fqbn).toString(true)
: undefined
),
this.preferences.get('arduino.upload.verify'),
this.preferences.get('arduino.upload.verbose'),
]);

View File

@@ -1,4 +1,5 @@
import { nls } from '@theia/core/lib/common/nls';
import { FQBN } from 'fqbn';
import type { MaybePromise } from '@theia/core/lib/common/types';
import type URI from '@theia/core/lib/common/uri';
import {
@@ -367,40 +368,6 @@ export interface ConfigOption {
readonly values: ConfigValue[];
}
export namespace ConfigOption {
/**
* Appends the configuration options to the `fqbn` argument.
* Throws an error if the `fqbn` does not have the `segment(':'segment)*` format.
* The provided output format is always segment(':'segment)*(':'option'='value(','option'='value)*)?
*/
export function decorate(
fqbn: string,
configOptions: ConfigOption[]
): string {
if (!configOptions.length) {
return fqbn;
}
const toValue = (values: ConfigValue[]) => {
const selectedValue = values.find(({ selected }) => selected);
if (!selectedValue) {
console.warn(
`None of the config values was selected. Values were: ${JSON.stringify(
values
)}`
);
return undefined;
}
return selectedValue.value;
};
const options = configOptions
.map(({ option, values }) => [option, toValue(values)])
.filter(([, value]) => !!value)
.map(([option, value]) => `${option}=${value}`)
.join(',');
return `${fqbn}:${options}`;
}
export class ConfigOptionError extends Error {
constructor(message: string) {
super(message);
@@ -574,28 +541,13 @@ export namespace Board {
}
}
/**
* Throws an error if the `fqbn` argument is not sanitized. A sanitized FQBN has the `VENDOR:ARCHITECTURE:BOARD_ID` construct.
*/
export function assertSanitizedFqbn(fqbn: string): void {
if (fqbn.split(':').length !== 3) {
throw new Error(
`Expected a sanitized FQBN with three segments in the following format: 'VENDOR:ARCHITECTURE:BOARD_ID'. Got ${fqbn} instead.`
);
}
}
/**
* Converts the `VENDOR:ARCHITECTURE:BOARD_ID[:MENU_ID=OPTION_ID[,MENU2_ID=OPTION_ID ...]]` FQBN to
* `VENDOR:ARCHITECTURE:BOARD_ID` format.
* See the details of the `{build.fqbn}` entry in the [specs](https://arduino.github.io/arduino-cli/latest/platform-specification/#global-predefined-properties).
*/
export function sanitizeFqbn(fqbn: string | undefined): string | undefined {
if (!fqbn) {
return undefined;
}
const [vendor, arch, id] = fqbn.split(':');
return `${vendor}:${arch}:${id}`;
export function sanitizeFqbn(fqbn: string): string {
return new FQBN(fqbn).sanitize().toString();
}
export type PlatformIdentifier = Readonly<{ vendorId: string; arch: string }>;
@@ -752,8 +704,8 @@ export function boardIdentifierEquals(
return false; // TODO: This a strict now. Maybe compare name in the future.
}
if (left.fqbn && right.fqbn) {
const leftFqbn = options.looseFqbn ? sanitizeFqbn(left.fqbn) : left.fqbn;
const rightFqbn = options.looseFqbn ? sanitizeFqbn(right.fqbn) : right.fqbn;
const leftFqbn = new FQBN(left.fqbn).toString(options.looseFqbn);
const rightFqbn = new FQBN(right.fqbn).toString(options.looseFqbn);
return leftFqbn === rightFqbn;
}
// No more Genuino hack.

View File

@@ -267,24 +267,12 @@ export class BoardDiscovery
const { port, boards } = detectedPort;
const key = Port.keyOf(port);
if (eventType === EventType.Add) {
const alreadyDetectedPort = newState[key];
if (alreadyDetectedPort) {
console.warn(
`Detected a new port that has been already discovered. The old value will be overridden. Old value: ${JSON.stringify(
alreadyDetectedPort
)}, new value: ${JSON.stringify(detectedPort)}`
);
}
// Note that, the serial discovery might detect port details (such as addressLabel) in chunks.
// For example, first, the Teensy 4.1 port is detected with the `[no_device] Triple Serial` address label,
// Then, when more refinements are available, the same port is detected with `/dev/cu.usbmodem127902301 Triple Serial` address label.
// In such cases, an `add` event is received from the CLI, and the the detected port is overridden in the state.
newState[key] = { port, boards };
} else if (eventType === EventType.Remove) {
const alreadyDetectedPort = newState[key];
if (!alreadyDetectedPort) {
console.warn(
`Detected a port removal but it has not been discovered. This is most likely a bug! Detected port was: ${JSON.stringify(
detectedPort
)}`
);
}
delete newState[key];
}
}

View File

@@ -170,6 +170,36 @@ describe('board-service-provider', () => {
expect(events).deep.equals([expectedEvent]);
});
it('should ignore custom board configs from the FQBN', () => {
boardsServiceProvider['_boardsConfig'] = {
selectedBoard: uno,
selectedPort: unoSerialPort,
};
const events: BoardsConfigChangeEvent[] = [];
toDisposeAfterEach.push(
boardsServiceProvider.onBoardsConfigDidChange((event) =>
events.push(event)
)
);
const mkr1000WithCustomOptions = {
...mkr1000,
fqbn: `${mkr1000.fqbn}:c1=v1`,
};
const didUpdate = boardsServiceProvider.updateConfig(
mkr1000WithCustomOptions
);
expect(didUpdate).to.be.true;
const expectedEvent: BoardIdentifierChangeEvent = {
previousSelectedBoard: uno,
selectedBoard: mkr1000WithCustomOptions, // the even has the custom board options
};
expect(events).deep.equals([expectedEvent]);
// the persisted state does not have the config options property
expect(boardsServiceProvider.boardsConfig.selectedBoard?.fqbn).to.equal(
mkr1000.fqbn
);
});
it('should not update the board if did not change (board identifier)', () => {
boardsServiceProvider['_boardsConfig'] = {
selectedBoard: uno,

View File

@@ -15,11 +15,14 @@ import {
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { MessageService } from '@theia/core/lib/common/message-service';
import { wait } from '@theia/core/lib/common/promise-util';
import { wait, waitForEvent } from '@theia/core/lib/common/promise-util';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { BoardsDataStore } from '../../browser/boards/boards-data-store';
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
import {
BoardsServiceProvider,
UpdateBoardsConfigParams,
} from '../../browser/boards/boards-service-provider';
import { NotificationCenter } from '../../browser/notification-center';
import {
BoardDetails,
@@ -30,6 +33,7 @@ import {
} from '../../common/protocol/boards-service';
import { NotificationServiceServer } from '../../common/protocol/notification-service';
import { bindBrowser } from './browser-test-bindings';
import { unoSerialPort } from '../common/fixtures';
disableJSDOM();
@@ -256,8 +260,12 @@ describe('boards-data-store', function () {
const result = await boardsDataStore.selectConfigOption({
fqbn,
option: configOption1.option,
selectedValue: configOption1.values[1].value,
optionsToUpdate: [
{
option: configOption1.option,
selectedValue: configOption1.values[1].value,
},
],
});
expect(result).to.be.ok;
@@ -409,8 +417,12 @@ describe('boards-data-store', function () {
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
option: configOption1.option,
selectedValue: configOption1.values[1].value,
optionsToUpdate: [
{
option: configOption1.option,
selectedValue: configOption1.values[1].value,
},
],
});
expect(result).to.be.ok;
expect(didChangeCounter).to.be.equal(1);
@@ -430,6 +442,220 @@ describe('boards-data-store', function () {
});
});
it('should select multiple config options', async () => {
// reconfigure the board details mock for this test case to have multiple config options
toDisposeAfterEach.push(
mockBoardDetails([
{
fqbn,
...baseDetails,
configOptions: [configOption1, configOption2],
},
])
);
let data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [configOption1, configOption2],
programmers: [edbg, jlink],
});
let didChangeCounter = 0;
toDisposeAfterEach.push(
boardsDataStore.onDidChange(() => didChangeCounter++)
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
optionsToUpdate: [
{
option: configOption1.option,
selectedValue: configOption1.values[1].value,
},
{
option: configOption2.option,
selectedValue: configOption2.values[1].value,
},
],
});
expect(result).to.be.ok;
expect(didChangeCounter).to.be.equal(1);
data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [
{
...configOption1,
values: [
{ label: 'C1V1', selected: false, value: 'v1' },
{ label: 'C1V2', selected: true, value: 'v2' },
],
},
{
...configOption2,
values: [
{ label: 'C2V1', selected: false, value: 'v1' },
{ label: 'C2V2', selected: true, value: 'v2' },
],
},
],
programmers: [edbg, jlink],
});
});
it('should emit a did change event when updating with multiple config options and at least one of them is known (valid option + valid value)', async () => {
// reconfigure the board details mock for this test case to have multiple config options
toDisposeAfterEach.push(
mockBoardDetails([
{
fqbn,
...baseDetails,
configOptions: [configOption1, configOption2],
},
])
);
let data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [configOption1, configOption2],
programmers: [edbg, jlink],
});
let didChangeCounter = 0;
toDisposeAfterEach.push(
boardsDataStore.onDidChange(() => didChangeCounter++)
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
optionsToUpdate: [
{
option: 'an unknown option',
selectedValue: configOption1.values[1].value,
},
{
option: configOption1.option,
selectedValue: configOption1.values[1].value,
},
{
option: configOption2.option,
selectedValue: 'an unknown value',
},
],
});
expect(result).to.be.ok;
expect(didChangeCounter).to.be.equal(1);
data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [
{
...configOption1,
values: [
{ label: 'C1V1', selected: false, value: 'v1' },
{ label: 'C1V2', selected: true, value: 'v2' },
],
},
configOption2,
],
programmers: [edbg, jlink],
});
});
it('should not emit a did change event when updating with multiple config options and all of the are unknown', async () => {
let data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [configOption1],
programmers: [edbg, jlink],
});
let didChangeCounter = 0;
toDisposeAfterEach.push(
boardsDataStore.onDidChange(() => didChangeCounter++)
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
optionsToUpdate: [
{
option: 'an unknown option',
selectedValue: configOption1.values[1].value,
},
{
option: configOption1.option,
selectedValue: 'an unknown value',
},
],
});
expect(result).to.be.not.ok;
expect(didChangeCounter).to.be.equal(0);
data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [configOption1],
programmers: [edbg, jlink],
});
});
it("should automatically update the selected config options if the boards config change 'reason' is the 'toolbar' and the (CLI) detected FQBN has config options", async () => {
// reconfigure the board details mock for this test case to have multiple config options
toDisposeAfterEach.push(
mockBoardDetails([
{
fqbn,
...baseDetails,
configOptions: [configOption1, configOption2],
},
])
);
let data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [configOption1, configOption2],
programmers: [edbg, jlink],
});
let didChangeCounter = 0;
toDisposeAfterEach.push(
boardsDataStore.onDidChange(() => didChangeCounter++)
);
const boardsConfig = {
selectedPort: unoSerialPort, // the port value does not matter here, but the change must come from a toolbar as a boards config: with port+board,
selectedBoard: {
fqbn: `${board.fqbn}:${configOption1.option}=${configOption1.values[1].value},${configOption2.option}=${configOption2.values[1].value}`,
name: board.name,
},
};
const params: UpdateBoardsConfigParams = {
...boardsConfig,
reason: 'toolbar',
};
const updated = boardsServiceProvider.updateConfig(params);
expect(updated).to.be.ok;
await waitForEvent(boardsDataStore.onDidChange, 100);
expect(didChangeCounter).to.be.equal(1);
data = await boardsDataStore.getData(fqbn);
expect(data).to.be.deep.equal({
configOptions: [
{
...configOption1,
values: [
{ label: 'C1V1', selected: false, value: 'v1' },
{ label: 'C1V2', selected: true, value: 'v2' },
],
},
{
...configOption2,
values: [
{ label: 'C2V1', selected: false, value: 'v1' },
{ label: 'C2V2', selected: true, value: 'v2' },
],
},
],
programmers: [edbg, jlink],
});
});
it('should not select a config option if the option is absent', async () => {
const fqbn = 'a:b:c';
let data = await boardsDataStore.getData(fqbn);
@@ -444,8 +670,9 @@ describe('boards-data-store', function () {
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
option: 'missing',
selectedValue: configOption1.values[1].value,
optionsToUpdate: [
{ option: 'missing', selectedValue: configOption1.values[1].value },
],
});
expect(result).to.be.not.ok;
expect(didChangeCounter).to.be.equal(0);
@@ -470,8 +697,9 @@ describe('boards-data-store', function () {
);
const result = await boardsDataStore.selectConfigOption({
fqbn,
option: configOption1.option,
selectedValue: 'missing',
optionsToUpdate: [
{ option: configOption1.option, selectedValue: 'missing' },
],
});
expect(result).to.be.not.ok;
expect(didChangeCounter).to.be.equal(0);