mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-06 00:48:35 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'),
|
||||
]);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user