Skip to content

Commit 95d3d7f

Browse files
author
Akos Kitta
committed
fix: use exit code to detect missing programmer
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent be63203 commit 95d3d7f

File tree

5 files changed

+97
-48
lines changed

5 files changed

+97
-48
lines changed
Binary file not shown.

src/debug.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -429,17 +429,23 @@ async function cliExec<T = Record<string, unknown>>(
429429
try {
430430
const object = JSON.parse(err.stderr);
431431
if (isCommandError(object)) {
432-
cliError = new CliError(object.error);
432+
cliError = new CliError(object.error, err.exitCode);
433433
}
434434
} catch {}
435435
}
436436
throw cliError ?? err;
437437
}
438438
}
439439

440-
function isExecaError(arg: unknown): arg is Error & { stderr: string } {
440+
function isExecaError(
441+
arg: unknown
442+
): arg is Error & { stderr: string; exitCode: number | undefined } {
441443
return (
442-
arg instanceof Error && 'stderr' in arg && typeof arg.stderr === 'string'
444+
arg instanceof Error &&
445+
'stderr' in arg &&
446+
typeof arg.stderr === 'string' &&
447+
'exitCode' in arg &&
448+
(typeof arg.exitCode === 'number' || typeof arg.exitCode === 'undefined')
443449
);
444450
}
445451

@@ -456,12 +462,20 @@ function isCommandError(arg: unknown): arg is CommandError {
456462
}
457463

458464
export class CliError extends Error {
459-
constructor(message: string) {
465+
constructor(message: string, readonly exitCode: number | undefined) {
460466
super(message);
461467
Object.setPrototypeOf(this, CliError.prototype);
462468
}
463469
}
464470

471+
// https://github.com/arduino/arduino-cli/blob/b41f4044cac6ab7f7d853e368bc31e5d626d63d4/internal/cli/feedback/errorcodes.go#L57-L58
472+
const missingProgrammerCode = 11 as const;
473+
function isMissingProgrammerError(
474+
arg: unknown
475+
): arg is CliError & { exitCode: typeof missingProgrammerCode } {
476+
return arg instanceof CliError && arg.exitCode === missingProgrammerCode;
477+
}
478+
465479
// Remove index signature
466480
// https://stackoverflow.com/a/51956054/5529090
467481
type RemoveIndex<T> = {
@@ -495,5 +509,6 @@ export const __tests__ = {
495509
mergeLaunchConfig,
496510
updateLaunchConfigs,
497511
isCommandError,
512+
isMissingProgrammerError,
498513
cliExec,
499514
} as const;

src/test/suite/debug.slow-test.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@ import { __tests__ } from '../../debug';
66
import { exec } from '../../exec';
77
import { TestEnv } from '../testEnv';
88

9-
const { CliError, buildDebugInfoArgs, createLaunchConfig } = __tests__;
9+
const {
10+
CliError,
11+
buildDebugInfoArgs,
12+
createLaunchConfig,
13+
isMissingProgrammerError,
14+
} = __tests__;
1015

1116
describe('debug (slow)', () => {
1217
let testEnv: TestEnv;
@@ -35,7 +40,7 @@ describe('debug (slow)', () => {
3540
const actual = await createLaunchConfig({
3641
board: { fqbn },
3742
cliPath: testEnv.cliPath,
38-
cliConfigPath: testEnv.cliConfigPath,
43+
cliConfigPath: testEnv.cliConfigPaths['en'],
3944
sketchPath,
4045
programmer,
4146
});
@@ -93,13 +98,20 @@ describe('debug (slow)', () => {
9398
assert.ok(typeof object === 'object');
9499
});
95100

96-
it('should fail when the programmer is missing', async () => {
97-
await assert.rejects(
98-
cliExec(['debug', '-I', '-b', 'arduino:esp32:nano_nora', sketchPath]),
99-
(reason) =>
100-
reason instanceof CliError && /missing programmer/i.test(reason.message)
101-
);
102-
});
101+
['en', 'it'].map((locale) =>
102+
it(`should fail when the programmer is missing (locale: ${locale})`, async function () {
103+
if (!testEnv.cliConfigPaths[locale]) {
104+
this.skip();
105+
}
106+
await assert.rejects(
107+
cliExec(
108+
['debug', '-I', '-b', 'arduino:esp32:nano_nora', sketchPath],
109+
locale
110+
),
111+
(reason) => isMissingProgrammerError(reason)
112+
);
113+
})
114+
);
103115

104116
it('should fail when the programmer is unknown', async () => {
105117
await assert.rejects(
@@ -135,8 +147,12 @@ describe('debug (slow)', () => {
135147
return sketchPath;
136148
}
137149

138-
async function cliExec<T>(args: string[]): Promise<T> {
139-
return __tests__.cliExec(testEnv.cliPath, args, testEnv.cliConfigPath);
150+
async function cliExec<T>(args: string[], locale = 'en'): Promise<T> {
151+
return __tests__.cliExec(
152+
testEnv.cliPath,
153+
args,
154+
testEnv.cliConfigPaths[locale]
155+
);
140156
}
141157

142158
function fromDataDir(...paths: string[]): string {

src/test/suite/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,18 +75,24 @@ const cliFilename = `arduino-cli${os.platform() === 'win32' ? '.exe' : ''}`;
7575
const cliPath = path.join(testsPath, cliFilename);
7676

7777
async function setup(tracked: typeof temp): Promise<TestEnv> {
78-
const cliConfigPath = path.join(testsPath, 'arduino-cli.yaml');
78+
const cliConfigPaths = {
79+
en: path.join(testsPath, 'arduino-cli.yaml'),
80+
it: path.join(testsPath, 'arduino-cli-it.yaml'),
81+
} as const;
7982
const testEnv: TestEnv = {
80-
cliConfigPath,
83+
cliConfigPaths,
8184
cliPath,
8285
dataDirPath,
8386
userDirPath,
8487
tracked,
88+
get cliConfigPath() {
89+
return cliConfigPaths['en'];
90+
},
8591
};
8692
const params: PrepareTestEnvParams = {
8793
...testEnv,
8894
platformsToInstall: ['arduino:esp32', 'arduino:samd'], // samd is need to get the tools for the manually (Git) installed core
8995
};
9096
await prepareTestEnv(params);
91-
return params;
97+
return testEnv;
9298
}

src/test/testEnv.ts

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,24 @@ import { exec } from '../exec';
77

88
export interface TestEnv {
99
readonly cliPath: string;
10-
/**
11-
* Config will be overwritten if exists.
12-
*/
13-
readonly cliConfigPath: string;
1410
readonly userDirPath: string;
1511
readonly dataDirPath: string;
1612
readonly tracked: typeof temp;
13+
/**
14+
* The keys are locales values are the absolute filesystem path of the CLI configuration with the pre-configured `locale`.
15+
*/
16+
readonly cliConfigPaths: Readonly<{
17+
en: string;
18+
[local: string]: string;
19+
}>;
20+
21+
/**
22+
* Sugar for `cliConfigPaths['en']`
23+
*/
24+
get cliConfigPath(): string;
1725
}
1826

19-
export interface PrepareTestEnvParams extends TestEnv {
27+
export interface PrepareTestEnvParams extends Omit<TestEnv, 'cliConfigPath'> {
2028
readonly platformsToInstall?: readonly string[];
2129
readonly additionalUrls?: readonly string[];
2230
}
@@ -88,35 +96,39 @@ async function prepareWithCli(params: PrepareTestEnvParams): Promise<void> {
8896
log('Preparing test env with CLI');
8997
const cliExec = (args: string[], cliConfigPath?: string) =>
9098
__tests__.cliExec(params.cliPath, args, cliConfigPath);
91-
const { cliConfigPath, dataDirPath, userDirPath, additionalUrls } = params;
92-
await fs.mkdir(path.dirname(cliConfigPath), { recursive: true });
93-
await Promise.all([
94-
await cliExec([
95-
'config',
96-
'init',
97-
'--dest-file',
98-
cliConfigPath,
99-
'--overwrite',
100-
]),
101-
await fs.mkdir(dataDirPath, { recursive: true }),
102-
await fs.mkdir(userDirPath, { recursive: true }),
103-
]);
104-
await cliExec(
105-
['config', 'set', 'directories.user', userDirPath],
106-
cliConfigPath
107-
);
108-
await cliExec(
109-
['config', 'set', 'directories.data', dataDirPath],
110-
cliConfigPath
111-
);
112-
if (additionalUrls) {
99+
const { cliConfigPaths, dataDirPath, userDirPath, additionalUrls } = params;
100+
for (const [locale, cliConfigPath] of Object.entries(cliConfigPaths)) {
101+
await fs.mkdir(path.dirname(cliConfigPath), { recursive: true });
102+
await Promise.all([
103+
await cliExec([
104+
'config',
105+
'init',
106+
'--dest-file',
107+
cliConfigPath,
108+
'--overwrite',
109+
]),
110+
await fs.mkdir(dataDirPath, { recursive: true }),
111+
await fs.mkdir(userDirPath, { recursive: true }),
112+
]);
113+
await cliExec(
114+
['config', 'set', 'directories.user', userDirPath],
115+
cliConfigPath
116+
);
113117
await cliExec(
114-
['config', 'set', 'board_manager.additional_urls', ...additionalUrls],
118+
['config', 'set', 'directories.data', dataDirPath],
115119
cliConfigPath
116120
);
121+
if (additionalUrls) {
122+
await cliExec(
123+
['config', 'set', 'board_manager.additional_urls', ...additionalUrls],
124+
cliConfigPath
125+
);
126+
}
127+
await cliExec(['config', 'set', 'locale', locale], cliConfigPath);
128+
const config = await cliExec(['config', 'dump'], cliConfigPath);
129+
log(`Using CLI config (locale: ${locale}): ${JSON.stringify(config)}`);
117130
}
118-
const config = await cliExec(['config', 'dump'], cliConfigPath);
119-
log(`Using CLI config: ${JSON.stringify(config)}`);
131+
const cliConfigPath = cliConfigPaths['en'];
120132
await cliExec(['core', 'update-index'], cliConfigPath);
121133
log('Updated index');
122134
for (const platform of params.platformsToInstall ?? []) {

0 commit comments

Comments
 (0)