Skip to content

Commit f4e766a

Browse files
authored
Merge 0caf0ca into d79bc0d
2 parents d79bc0d + 0caf0ca commit f4e766a

File tree

3 files changed

+210
-18
lines changed

3 files changed

+210
-18
lines changed

Diff for: arduino-ide-extension/src/browser/contributions/ino-language.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { inject, injectable } from '@theia/core/shared/inversify';
66
import { Mutex } from 'async-mutex';
77
import {
88
ArduinoDaemon,
9-
assertSanitizedFqbn,
109
BoardsService,
1110
ExecutableService,
1211
sanitizeFqbn,
@@ -93,8 +92,10 @@ export class InoLanguage extends SketchContribution {
9392
`Failed to sanitize the FQBN of the running language server. FQBN with the board settings was: ${this.languageServerFqbn}`
9493
);
9594
}
95+
// The incoming FQBNs might contain custom boards configs, sanitize them before the comparison.
96+
// https://github.com/arduino/arduino-ide/pull/2113#pullrequestreview-1499998328
9697
const matchingFqbn = dataChangePerFqbn.find(
97-
(fqbn) => sanitizedFqbn === fqbn
98+
(fqbn) => sanitizedFqbn === sanitizeFqbn(fqbn)
9899
);
99100
const { boardsConfig } = this.boardsServiceProvider;
100101
if (
@@ -151,7 +152,6 @@ export class InoLanguage extends SketchContribution {
151152
}
152153
return;
153154
}
154-
assertSanitizedFqbn(fqbn);
155155
const fqbnWithConfig = await this.boardDataStore.appendConfigToFqbn(fqbn);
156156
if (!fqbnWithConfig) {
157157
throw new Error(

Diff for: arduino-ide-extension/src/common/protocol/boards-service.ts

+92-15
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export namespace BoardSearch {
180180
'Partner',
181181
'Arduino@Heart',
182182
] as const;
183-
export type Type = typeof TypeLiterals[number];
183+
export type Type = (typeof TypeLiterals)[number];
184184
export namespace Type {
185185
export function is(arg: unknown): arg is Type {
186186
return typeof arg === 'string' && TypeLiterals.includes(arg as Type);
@@ -347,7 +347,7 @@ export namespace Port {
347347

348348
export namespace Protocols {
349349
export const KnownProtocolLiterals = ['serial', 'network'] as const;
350-
export type KnownProtocol = typeof KnownProtocolLiterals[number];
350+
export type KnownProtocol = (typeof KnownProtocolLiterals)[number];
351351
export namespace KnownProtocol {
352352
export function is(protocol: unknown): protocol is KnownProtocol {
353353
return (
@@ -476,29 +476,106 @@ export namespace ConfigOption {
476476
fqbn: string,
477477
configOptions: ConfigOption[]
478478
): string {
479-
if (!configOptions.length) {
480-
return fqbn;
479+
const failInvalidFQBN = (): never => {
480+
throw new Error(`Invalid FQBN: ${fqbn}`);
481+
};
482+
483+
const [vendor, arch, id, rest] = fqbn.split(':');
484+
if (!vendor || !arch || !id) {
485+
return failInvalidFQBN();
481486
}
482487

483-
const toValue = (values: ConfigValue[]) => {
488+
const existingOptions: Record<string, string> = {};
489+
if (rest) {
490+
// If rest exists, it must have the key=value(,key=value)* format. Otherwise, fail.
491+
const tuples = rest.split(',');
492+
for (const tuple of tuples) {
493+
const segments = tuple.split('=');
494+
if (segments.length !== 2) {
495+
failInvalidFQBN();
496+
}
497+
const [option, value] = segments;
498+
if (!option || !value) {
499+
failInvalidFQBN();
500+
}
501+
if (existingOptions[option]) {
502+
console.warn(
503+
`Config value already exists for '${option}' on FQBN. Skipping it. Existing value: ${existingOptions[option]}, new value: ${value}, FQBN: ${fqbn}`
504+
);
505+
} else {
506+
existingOptions[option] = value;
507+
}
508+
}
509+
}
510+
511+
const newOptions: Record<string, string> = {};
512+
for (const configOption of configOptions) {
513+
const { option, values } = configOption;
514+
if (!option) {
515+
console.warn(
516+
`Detected empty option on config options. Skipping it. ${JSON.stringify(
517+
configOption
518+
)}`
519+
);
520+
continue;
521+
}
484522
const selectedValue = values.find(({ selected }) => selected);
485-
if (!selectedValue) {
523+
if (selectedValue) {
524+
const { value } = selectedValue;
525+
if (!value) {
526+
console.warn(
527+
`Detected empty selected value on config options. Skipping it. ${JSON.stringify(
528+
configOption
529+
)}`
530+
);
531+
continue;
532+
}
533+
if (newOptions[option]) {
534+
console.warn(
535+
`Config value already exists for '${option}' in config options. Skipping it. Existing value: ${
536+
newOptions[option]
537+
}, new value: ${value}, config option: ${JSON.stringify(
538+
configOption
539+
)}`
540+
);
541+
} else {
542+
newOptions[option] = value;
543+
}
544+
} else {
486545
console.warn(
487-
`None of the config values was selected. Values were: ${JSON.stringify(
488-
values
546+
`None of the config values was selected. Config options was: ${JSON.stringify(
547+
configOption
489548
)}`
490549
);
491-
return undefined;
492550
}
493-
return selectedValue.value;
494-
};
495-
const options = configOptions
496-
.map(({ option, values }) => [option, toValue(values)])
497-
.filter(([, value]) => !!value)
551+
}
552+
553+
// Collect all options from the FQBN. Call them existing.
554+
// Collect all incoming options to decorate the FQBN with. Call them new.
555+
// To keep the order, iterate through the existing ones and append to FQBN.
556+
// If a new(er) value exists for the same option, use the new value.
557+
// If there is a new value, "mark" it as visited by deleting it from new. Otherwise, use the existing value.
558+
// Append all new ones to the FQBN.
559+
const mergedOptions: Record<string, string> = {};
560+
for (const existing of Object.entries(existingOptions)) {
561+
const [option, value] = existing;
562+
const newValue = newOptions[option];
563+
if (newValue) {
564+
mergedOptions[option] = newValue;
565+
delete newOptions[option];
566+
} else {
567+
mergedOptions[option] = value;
568+
}
569+
}
570+
Array.from(Object.entries(newOptions)).forEach(
571+
([option, value]) => (mergedOptions[option] = value)
572+
);
573+
574+
const configSuffix = Object.entries(mergedOptions)
498575
.map(([option, value]) => `${option}=${value}`)
499576
.join(',');
500577

501-
return `${fqbn}:${options}`;
578+
return `${vendor}:${arch}:${id}${configSuffix ? `:${configSuffix}` : ''}`;
502579
}
503580

504581
export class ConfigOptionError extends Error {

Diff for: arduino-ide-extension/src/test/common/boards-service.test.ts

+115
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { expect } from 'chai';
44
import {
55
AttachedBoardsChangeEvent,
66
BoardInfo,
7+
ConfigOption,
78
getBoardInfo,
89
noNativeSerialPort,
910
nonSerialPort,
@@ -93,6 +94,120 @@ describe('boards-service', () => {
9394
});
9495
});
9596

97+
describe('ConfigOption#decorate', () => {
98+
['invalid', 'a:b:c:foo=', 'a:b:c:foo=bar=baz', 'a:b:c:foo+bar'].map(
99+
(fqbn) =>
100+
it(`should error when the FQBN is invalid (FQBN: ${fqbn})`, () => {
101+
expect(() => ConfigOption.decorate(fqbn, [])).to.throw(
102+
/^Invalid FQBN:*/
103+
);
104+
})
105+
);
106+
107+
it('should not append the trailing boards config part to FQBN if configs is empty', () => {
108+
const fqbn = 'a:b:c';
109+
const actual = ConfigOption.decorate(fqbn, []);
110+
expect(actual).to.be.equal(fqbn);
111+
});
112+
113+
it('should be noop when config options is empty', () => {
114+
const fqbn = 'a:b:c:menu1=value1';
115+
const actual = ConfigOption.decorate(fqbn, []);
116+
expect(actual).to.be.equal(fqbn);
117+
});
118+
119+
it('should not duplicate config options', () => {
120+
const fqbn = 'a:b:c:menu1=value1';
121+
const actual = ConfigOption.decorate(fqbn, [
122+
{
123+
label: 'Menu 1',
124+
option: 'menu1',
125+
values: [
126+
{ label: 'Value 1', value: 'value1', selected: true },
127+
{ label: 'Another Value', value: 'another1', selected: false },
128+
],
129+
},
130+
]);
131+
expect(actual).to.be.equal(fqbn);
132+
});
133+
134+
it('should update config options', () => {
135+
const fqbn = 'a:b:c:menu1=value1,menu2=value2';
136+
const actual = ConfigOption.decorate(fqbn, [
137+
{
138+
label: 'Menu 1',
139+
option: 'menu1',
140+
values: [
141+
{ label: 'Value 1', value: 'value1', selected: false },
142+
{ label: 'Another Value', value: 'another1', selected: true },
143+
],
144+
},
145+
]);
146+
expect(actual).to.be.equal('a:b:c:menu1=another1,menu2=value2');
147+
});
148+
149+
it('should favor first over rest when there are duplicate options (duplicate on FQBN)', () => {
150+
const fqbn = 'a:b:c:menu1=value1,menu1=value2';
151+
const actual = ConfigOption.decorate(fqbn, [
152+
{
153+
label: 'Menu 1',
154+
option: 'menu1',
155+
values: [
156+
{ label: 'Value 1', value: 'value1', selected: false },
157+
{ label: 'Another Value', value: 'another1', selected: true },
158+
],
159+
},
160+
]);
161+
expect(actual).to.be.equal('a:b:c:menu1=another1');
162+
});
163+
164+
it('should favor first over rest when there are duplicate options (duplicate in config)', () => {
165+
const fqbn = 'a:b:c';
166+
const actual = ConfigOption.decorate(fqbn, [
167+
{
168+
label: 'Menu 1',
169+
option: 'menu1',
170+
values: [
171+
{ label: 'Value 1', value: 'value1', selected: false },
172+
{ label: 'Another Value', value: 'another1', selected: true },
173+
],
174+
},
175+
{
176+
label: 'Menu 1',
177+
option: 'menu1',
178+
values: [
179+
{ label: 'Value 1', value: 'value1', selected: true },
180+
{ label: 'Another Value', value: 'another1', selected: false },
181+
],
182+
},
183+
]);
184+
expect(actual).to.be.equal('a:b:c:menu1=another1');
185+
});
186+
187+
it('should not change the existing config order', () => {
188+
const fqbn = 'a:b:c:menu1=value1';
189+
const actual = ConfigOption.decorate(fqbn, [
190+
{
191+
label: 'Menu 0',
192+
option: 'menu0',
193+
values: [
194+
{ label: 'Value 0', value: 'value0', selected: true },
195+
{ label: 'Another Value', value: 'another0', selected: false },
196+
],
197+
},
198+
{
199+
label: 'Menu 1',
200+
option: 'menu1',
201+
values: [
202+
{ label: 'Value 1', value: 'value1', selected: false },
203+
{ label: 'Another Value', value: 'another1', selected: true },
204+
],
205+
},
206+
]);
207+
expect(actual).to.be.equal('a:b:c:menu1=another1,menu0=value0');
208+
});
209+
});
210+
96211
describe('getBoardInfo', () => {
97212
const vid = '0x0';
98213
const pid = '0x1';

0 commit comments

Comments
 (0)