test: gRPC core client init integration test

- Copied the env-variable server from Theia and made it possible to
customize it for the tests. Each test has its own `data` folder.
 - Relaxed the primary package and library index error detection.
This should make the init error detection locale independent.
 - Kill the daemon process subtree when stopping the daemon.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-04-14 17:33:45 +02:00 committed by Akos Kitta
parent 097c92d904
commit 51f69f6a59
13 changed files with 596 additions and 91 deletions

View File

@ -68,7 +68,7 @@
"cpy": "^8.1.2",
"cross-fetch": "^3.1.5",
"dateformat": "^3.0.3",
"deepmerge": "2.0.1",
"deepmerge": "^4.2.2",
"electron-updater": "^4.6.5",
"fast-json-stable-stringify": "^2.1.0",
"fast-safe-stringify": "^2.1.1",

View File

@ -512,7 +512,7 @@ export class CreateApi {
const result = await resultProvider(response);
const parseEnd = performance.now();
console.debug(
`HTTP ${fetchCount} ${method} ${url} [fetch: ${(
`HTTP ${fetchCount} ${method}${url} [fetch: ${(
fetchEnd - fetchStart
).toFixed(2)} ms, parse: ${(parseEnd - parseStart).toFixed(
2

View File

@ -16,6 +16,7 @@ import { ArduinoDaemon, NotificationServiceServer } from '../common/protocol';
import { CLI_CONFIG } from './cli-config';
import { getExecPath } from './exec-util';
import { SettingsReader } from './settings-reader';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
@injectable()
export class ArduinoDaemonImpl
@ -34,6 +35,9 @@ export class ArduinoDaemonImpl
@inject(SettingsReader)
private readonly settingsReader: SettingsReader;
@inject(ProcessUtils)
private readonly processUtils: ProcessUtils;
private readonly toDispose = new DisposableCollection();
private readonly onDaemonStartedEmitter = new Emitter<string>();
private readonly onDaemonStoppedEmitter = new Emitter<void>();
@ -84,8 +88,16 @@ export class ArduinoDaemonImpl
).unref();
this.toDispose.pushAll([
Disposable.create(() => daemon.kill()),
Disposable.create(() => this.fireDaemonStopped()),
Disposable.create(() => {
if (daemon.pid) {
this.processUtils.terminateProcessTree(daemon.pid);
this.fireDaemonStopped();
} else {
throw new Error(
'The CLI Daemon process does not have a PID. IDE2 could not stop the CLI daemon.'
);
}
}),
]);
this.fireDaemonStarted(port);
this.onData('Daemon is running.');

View File

@ -41,7 +41,10 @@ import {
} from '../common/protocol/arduino-daemon';
import { ConfigServiceImpl } from './config-service-impl';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { EnvVariablesServer } from './theia/env-variables/env-variables-server';
import {
ConfigDirUriProvider,
EnvVariablesServer,
} from './theia/env-variables/env-variables-server';
import { NodeFileSystemExt } from './node-filesystem-ext';
import {
FileSystemExt,
@ -236,6 +239,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DefaultWorkspaceServer).toSelf().inSingletonScope();
rebind(TheiaWorkspaceServer).toService(DefaultWorkspaceServer);
bind(ConfigDirUriProvider).toSelf().inSingletonScope();
bind(EnvVariablesServer).toSelf().inSingletonScope();
rebind(TheiaEnvVariablesServer).toService(EnvVariablesServer);

View File

@ -530,7 +530,6 @@ function isPrimaryPackageIndexMissingStatus(
{ directories: { data } }: DefaultCliConfig
): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('loading json index file') &&
message.includes(join(data, 'package_index.json'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return evaluate(status, predicate);
@ -551,8 +550,6 @@ function isLibraryIndexMissingStatus(
{ directories: { data } }: DefaultCliConfig
): boolean {
const predicate = ({ message }: RpcStatus.AsObject) =>
message.includes('index file') &&
message.includes('reading') &&
message.includes(join(data, 'library_index.json'));
// https://github.com/arduino/arduino-cli/blob/f0245bc2da6a56fccea7b2c9ea09e85fdcc52cb8/arduino/cores/packagemanager/package_manager.go#L247
return evaluate(status, predicate);

View File

@ -1,15 +1,112 @@
import { join } from 'path';
import { homedir } from 'os';
import { injectable } from '@theia/core/shared/inversify';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
EnvVariable,
EnvVariablesServer as TheiaEnvVariablesServer,
} from '@theia/core/lib/common/env-variables/env-variables-protocol';
import { isWindows } from '@theia/core/lib/common/os';
import URI from '@theia/core/lib/common/uri';
import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider';
import { EnvVariablesServerImpl as TheiaEnvVariablesServerImpl } from '@theia/core/lib/node/env-variables/env-variables-server';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
inject,
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { list as listDrives } from 'drivelist';
import { homedir } from 'os';
import { join } from 'path';
@injectable()
export class EnvVariablesServer extends TheiaEnvVariablesServerImpl {
protected override readonly configDirUri = Promise.resolve(
FileUri.create(
join(homedir(), BackendApplicationConfigProvider.get().configDirName)
).toString()
);
export class ConfigDirUriProvider {
private uri: URI | undefined;
configDirUri(): URI {
if (!this.uri) {
this.uri = FileUri.create(
join(homedir(), BackendApplicationConfigProvider.get().configDirName)
);
}
return this.uri;
}
}
// Copy-pasted from https://github.com/eclipse-theia/theia/blob/v1.31.1/packages/core/src/node/env-variables/env-variables-server.ts
// to simplify the binding of the config directory location for tests.
@injectable()
export class EnvVariablesServer implements TheiaEnvVariablesServer {
@inject(ConfigDirUriProvider)
private readonly configDirUriProvider: ConfigDirUriProvider;
private readonly envs: { [key: string]: EnvVariable } = {};
private readonly homeDirUri = FileUri.create(homedir()).toString();
constructor() {
const prEnv = process.env;
Object.keys(prEnv).forEach((key: string) => {
let keyName = key;
if (isWindows) {
keyName = key.toLowerCase();
}
this.envs[keyName] = { name: keyName, value: prEnv[key] };
});
}
@postConstruct()
protected init(): void {
console.log(
`Configuration directory URI: '${this.configDirUriProvider
.configDirUri()
.toString()}'`
);
}
async getExecPath(): Promise<string> {
return process.execPath;
}
async getVariables(): Promise<EnvVariable[]> {
return Object.keys(this.envs).map((key) => this.envs[key]);
}
async getValue(key: string): Promise<EnvVariable | undefined> {
if (isWindows) {
key = key.toLowerCase();
}
return this.envs[key];
}
async getConfigDirUri(): Promise<string> {
return this.configDirUriProvider.configDirUri().toString();
}
async getHomeDirUri(): Promise<string> {
return this.homeDirUri;
}
async getDrives(): Promise<string[]> {
const uris: string[] = [];
const drives = await listDrives();
for (const drive of drives) {
for (const mountpoint of drive.mountpoints) {
if (this.filterHiddenPartitions(mountpoint.path)) {
uris.push(FileUri.create(mountpoint.path).toString());
}
}
}
return uris;
}
/**
* Filters hidden and system partitions.
*/
private filterHiddenPartitions(path: string): boolean {
// OS X: This is your sleep-image. When your Mac goes to sleep it writes the contents of its memory to the hard disk. (https://bit.ly/2R6cztl)
if (path === '/private/var/vm') {
return false;
}
// Ubuntu: This system partition is simply the boot partition created when the computers mother board runs UEFI rather than BIOS. (https://bit.ly/2N5duHr)
if (path === '/boot/efi') {
return false;
}
return true;
}
}

View File

@ -2,21 +2,16 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { BoardSearch, BoardsService } from '../../common/protocol';
import {
configureBackendApplicationConfigProvider,
createBaseContainer,
startDaemon,
} from './test-bindings';
import { createBaseContainer, startDaemon } from './test-bindings';
describe('boards-service-impl', () => {
let boardService: BoardsService;
let toDispose: DisposableCollection;
before(async function () {
configureBackendApplicationConfigProvider();
this.timeout(20_000);
toDispose = new DisposableCollection();
const container = createContainer();
const container = await createContainer();
await start(container, toDispose);
boardService = container.get<BoardsService>(BoardsService);
});
@ -94,7 +89,7 @@ describe('boards-service-impl', () => {
});
});
function createContainer(): Container {
async function createContainer(): Promise<Container> {
return createBaseContainer();
}

View File

@ -0,0 +1,337 @@
import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { waitForEvent } from '@theia/core/lib/common/promise-util';
import type { MaybePromise } from '@theia/core/lib/common/types';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { promises as fs } from 'fs';
import { join } from 'path';
import { sync as deleteSync } from 'rimraf';
import {
BoardsService,
CoreService,
LibraryService,
} from '../../common/protocol';
import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
import { CLI_CONFIG, DefaultCliConfig } from '../../node/cli-config';
import { BoardListRequest } from '../../node/cli-protocol/cc/arduino/cli/commands/v1/board_pb';
import { CoreClientProvider } from '../../node/core-client-provider';
import { ConfigDirUriProvider } from '../../node/theia/env-variables/env-variables-server';
import { ErrnoException } from '../../node/utils/errors';
import {
createBaseContainer,
createCliConfig,
newTempConfigDirPath,
startDaemon,
} from './test-bindings';
const timeout = 5 * 60 * 1_000; // five minutes
describe('core-client-provider', () => {
let toDispose: DisposableCollection;
beforeEach(() => (toDispose = new DisposableCollection()));
afterEach(() => toDispose.dispose());
it("should update no indexes when the 'directories.data' exists", async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir();
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
expect(indexUpdateSummaryBeforeInit).to.be.not.undefined;
expect(indexUpdateSummaryBeforeInit).to.be.empty;
});
});
// The better translation the CLI has, the more likely IDE2 won't be able to detect primary package and library index errors.
// Instead of running the test against all supported locales, IDE2 runs the tests with locales that result in a bug.
['it', 'de'].map(([locale]) =>
it(`should recover when the 'directories.data' folder is missing independently from the CLI's locale ('${locale}')`, async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir({ locale });
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
expect(indexUpdateSummaryBeforeInit).to.be.not.undefined;
expect(indexUpdateSummaryBeforeInit).to.be.empty;
});
})
);
it("should recover when the 'directories.data' folder is missing", async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir();
deleteSync(join(configDirPath, 'data'));
const now = new Date().toISOString();
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
const libUpdateTimestamp = indexUpdateSummaryBeforeInit['library'];
expect(libUpdateTimestamp).to.be.not.empty;
expect(libUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0);
const platformUpdateTimestamp = indexUpdateSummaryBeforeInit['platform'];
expect(platformUpdateTimestamp).to.be.not.empty;
expect(platformUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0);
});
});
it("should recover when the primary package index file ('package_index.json') is missing", async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir();
const primaryPackageIndexPath = join(
configDirPath,
'data',
'Arduino15',
'package_index.json'
);
deleteSync(primaryPackageIndexPath);
const now = new Date().toISOString();
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
expect(indexUpdateSummaryBeforeInit['library']).to.be.undefined;
const platformUpdateTimestamp = indexUpdateSummaryBeforeInit['platform'];
expect(platformUpdateTimestamp).to.be.not.empty;
expect(platformUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0);
});
const rawJson = await fs.readFile(primaryPackageIndexPath, {
encoding: 'utf8',
});
expect(rawJson).to.be.not.empty;
const object = JSON.parse(rawJson);
expect(object).to.be.not.empty;
});
['serial-discovery', 'mdns-discovery'].map((tool) =>
it(`should recover when the '${join(
'packages',
'builtin',
'tools',
tool
)}' folder is missing`, async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir();
const builtinToolsPath = join(
configDirPath,
'data',
'Arduino15',
'packages',
'builtin',
'tools',
tool
);
deleteSync(builtinToolsPath);
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
expect(indexUpdateSummaryBeforeInit).to.be.not.undefined;
expect(indexUpdateSummaryBeforeInit).to.be.empty;
});
const toolVersions = await fs.readdir(builtinToolsPath);
expect(toolVersions.length).to.be.greaterThanOrEqual(1);
})
);
it("should recover when the library index file ('library_index.json') is missing", async function () {
this.timeout(timeout);
const configDirPath = await prepareTestConfigDir();
const libraryPackageIndexPath = join(
configDirPath,
'data',
'Arduino15',
'library_index.json'
);
deleteSync(libraryPackageIndexPath);
const now = new Date().toISOString();
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(container, ({ coreClientProvider }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
const libUpdateTimestamp = indexUpdateSummaryBeforeInit['library'];
expect(libUpdateTimestamp).to.be.not.empty;
expect(libUpdateTimestamp.localeCompare(now)).to.be.greaterThan(0);
expect(indexUpdateSummaryBeforeInit['platform']).to.be.undefined;
});
const rawJson = await fs.readFile(libraryPackageIndexPath, {
encoding: 'utf8',
});
expect(rawJson).to.be.not.empty;
const object = JSON.parse(rawJson);
expect(object).to.be.not.empty;
});
it('should recover when a 3rd party package index file is missing but the platform is not installed', async function () {
this.timeout(timeout);
const additionalUrls = [
'https://www.pjrc.com/teensy/package_teensy_index.json',
];
const assertTeensyAvailable = async (boardsService: BoardsService) => {
const boardsPackages = await boardsService.search({});
expect(
boardsPackages.filter(({ id }) => id === 'teensy:avr').length
).to.be.equal(1);
};
const configDirPath = await prepareTestConfigDir(
{ board_manager: { additional_urls: additionalUrls } },
({ boardsService }) => assertTeensyAvailable(boardsService)
);
const thirdPartyPackageIndexPath = join(
configDirPath,
'data',
'Arduino15',
'package_teensy_index.json'
);
deleteSync(thirdPartyPackageIndexPath);
const container = await startCli(configDirPath, toDispose);
await assertFunctionalCli(
container,
async ({ coreClientProvider, boardsService, coreService }) => {
const { indexUpdateSummaryBeforeInit } = coreClientProvider;
expect(indexUpdateSummaryBeforeInit).to.be.not.undefined;
expect(indexUpdateSummaryBeforeInit).to.be.empty;
// IDE2 cannot recover from a 3rd party package index issue.
// Only when the primary package or library index is corrupt.
// https://github.com/arduino/arduino-ide/issues/2021
await coreService.updateIndex({ types: ['platform'] });
await assertTeensyAvailable(boardsService);
}
);
});
});
interface Services {
coreClientProvider: CoreClientProvider;
coreService: CoreService;
libraryService: LibraryService;
boardsService: BoardsService;
}
async function assertFunctionalCli(
container: Container,
otherAsserts?: (services: Services) => MaybePromise<void>
): Promise<void> {
const coreClientProvider =
container.get<CoreClientProvider>(CoreClientProvider);
const coreService = container.get<CoreService>(CoreService);
const libraryService = container.get<LibraryService>(LibraryService);
const boardsService = container.get<BoardsService>(BoardsService);
expect(coreClientProvider).to.be.not.undefined;
expect(coreService).to.be.not.undefined;
expect(libraryService).to.be.not.undefined;
expect(boardsService).to.be.not.undefined;
const coreClient = coreClientProvider.tryGetClient;
expect(coreClient).to.be.not.undefined;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const { client, instance } = coreClient!;
const installedBoards = await boardsService.getInstalledBoards();
expect(installedBoards.length).to.be.equal(0);
const libraries = await libraryService.search({
query: 'cmaglie',
type: 'Contributed',
});
expect(libraries.length).to.be.greaterThanOrEqual(1);
expect(
libraries.filter(({ name }) => name === 'KonnektingFlashStorage').length
).to.be.greaterThanOrEqual(1);
// IDE2 runs `board list -w` equivalent, but running a single `board list`
// is sufficient for the tests to check if the serial discover tool is OK.
await new Promise<void>((resolve, reject) =>
client.boardList(new BoardListRequest().setInstance(instance), (err) => {
if (err) {
reject(err);
}
resolve(); // The response does not matter. Tests must be relaxed. Maybe there are environments without a serial port?
})
);
return otherAsserts?.({
coreClientProvider,
coreService,
libraryService,
boardsService,
});
}
/**
* Initializes the CLI by creating a temporary config folder including the correctly initialized
* `directories.data` folder so that tests can corrupt it and test it the CLI initialization can recover.
* The resolved path is pointing the temporary config folder. By the time the promise resolves, the CLI
* daemon is stopped. This function should be used to initialize a correct `directories.data` folder and
* the config folder.
*/
async function prepareTestConfigDir(
configOverrides: Partial<DefaultCliConfig> = {},
otherExpect?: (services: Services) => MaybePromise<void>
): Promise<string> {
const toDispose = new DisposableCollection();
const params = { configDirPath: newTempConfigDirPath(), configOverrides };
const container = await createContainer(params);
try {
await start(container, toDispose);
await assertFunctionalCli(container, otherExpect);
const configDirUriProvider =
container.get<ConfigDirUriProvider>(ConfigDirUriProvider);
return FileUri.fsPath(configDirUriProvider.configDirUri());
} finally {
const daemon = container.get<ArduinoDaemonImpl>(ArduinoDaemonImpl);
// Wait for the daemon stop event. All subprocesses (such as `serial-discovery` and `mdns-discovery`) must terminate.
// Otherwise, `EPERM: operation not permitted, unlink` is thrown on Windows when "corrupting" the `directories.data` folder for the tests.
await Promise.all([
waitForEvent(daemon.onDaemonStopped, 5_000),
Promise.resolve(toDispose.dispose()),
]);
}
}
async function startCli(
configDirPath: string,
toDispose: DisposableCollection
): Promise<Container> {
const cliConfigPath = join(configDirPath, CLI_CONFIG);
try {
await fs.readFile(cliConfigPath);
} catch (err) {
if (ErrnoException.isENOENT(err)) {
throw new Error(
`The CLI configuration was not found at ${cliConfigPath} when starting the tests.`
);
}
throw err;
}
const container = await createContainer(configDirPath);
await start(container, toDispose);
return container;
}
async function start(
container: Container,
toDispose: DisposableCollection
): Promise<void> {
await startDaemon(container, toDispose);
}
async function createContainer(
params:
| { configDirPath: string; configOverrides: Partial<DefaultCliConfig> }
| string = newTempConfigDirPath()
): Promise<Container> {
if (typeof params === 'string') {
return createBaseContainer({ configDirPath: params });
}
const { configDirPath, configOverrides } = params;
const cliConfig = await createCliConfig(configDirPath, configOverrides);
return createBaseContainer({ configDirPath, cliConfig });
}

View File

@ -10,11 +10,7 @@ import {
CoreService,
SketchesService,
} from '../../common/protocol';
import {
configureBackendApplicationConfigProvider,
createBaseContainer,
startDaemon,
} from './test-bindings';
import { createBaseContainer, startDaemon } from './test-bindings';
const testTimeout = 30_000;
const setupTimeout = 5 * 60 * 1_000; // five minutes
@ -25,14 +21,10 @@ describe('core-service-impl', () => {
let container: Container;
let toDispose: DisposableCollection;
before(() => {
configureBackendApplicationConfigProvider();
});
beforeEach(async function () {
this.timeout(setupTimeout);
toDispose = new DisposableCollection();
container = createContainer();
container = await createContainer();
await start(container, toDispose);
});
@ -97,10 +89,12 @@ async function start(
});
}
function createContainer(): Container {
return createBaseContainer((bind) => {
bind(TestCommandRegistry).toSelf().inSingletonScope();
bind(CommandRegistry).toService(TestCommandRegistry);
async function createContainer(): Promise<Container> {
return createBaseContainer({
additionalBindings: (bind, rebind) => {
bind(TestCommandRegistry).toSelf().inSingletonScope();
rebind(CommandRegistry).toService(TestCommandRegistry);
},
});
}

View File

@ -2,22 +2,16 @@ import { DisposableCollection } from '@theia/core/lib/common/disposable';
import { Container } from '@theia/core/shared/inversify';
import { expect } from 'chai';
import { LibrarySearch, LibraryService } from '../../common/protocol';
import { LibraryServiceImpl } from '../../node/library-service-impl';
import {
configureBackendApplicationConfigProvider,
createBaseContainer,
startDaemon,
} from './test-bindings';
import { createBaseContainer, startDaemon } from './test-bindings';
describe('library-service-impl', () => {
let libraryService: LibraryService;
let toDispose: DisposableCollection;
before(async function () {
configureBackendApplicationConfigProvider();
this.timeout(20_000);
toDispose = new DisposableCollection();
const container = createContainer();
const container = await createContainer();
await start(container, toDispose);
libraryService = container.get<LibraryService>(LibraryService);
});
@ -72,11 +66,8 @@ describe('library-service-impl', () => {
});
});
function createContainer(): Container {
return createBaseContainer((bind) => {
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryService).toService(LibraryServiceImpl);
});
async function createContainer(): Promise<Container> {
return createBaseContainer();
}
async function start(

View File

@ -11,11 +11,7 @@ import { sync as rimrafSync } from 'rimraf';
import { Sketch, SketchesService } from '../../common/protocol';
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
import { ErrnoException } from '../../node/utils/errors';
import {
configureBackendApplicationConfigProvider,
createBaseContainer,
startDaemon,
} from './test-bindings';
import { createBaseContainer, startDaemon } from './test-bindings';
const testTimeout = 10_000;
@ -24,9 +20,8 @@ describe('sketches-service-impl', () => {
let toDispose: DisposableCollection;
before(async () => {
configureBackendApplicationConfigProvider();
toDispose = new DisposableCollection();
container = createContainer();
container = await createContainer();
await start(container, toDispose);
});
@ -257,6 +252,6 @@ async function start(
await startDaemon(container, toDispose);
}
function createContainer(): Container {
async function createContainer(): Promise<Container> {
return createBaseContainer();
}

View File

@ -13,13 +13,20 @@ import { ILogger, Loggable } from '@theia/core/lib/common/logger';
import { LogLevel } from '@theia/core/lib/common/logger-protocol';
import { waitForEvent } from '@theia/core/lib/common/promise-util';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { BackendApplicationConfigProvider } from '@theia/core/lib/node/backend-application-config-provider';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node/file-uri';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
import {
Container,
ContainerModule,
injectable,
interfaces,
} from '@theia/core/shared/inversify';
import * as deepmerge from 'deepmerge';
import { promises as fs, mkdirSync } from 'fs';
import { dump as dumpYaml } from 'js-yaml';
import { join } from 'path';
import { path as tempPath, track } from 'temp';
import {
ArduinoDaemon,
AttachedBoardsChangeEvent,
@ -33,6 +40,7 @@ import {
IndexUpdateDidFailParams,
IndexUpdateParams,
LibraryPackage,
LibraryService,
NotificationServiceClient,
NotificationServiceServer,
OutputMessage,
@ -44,10 +52,12 @@ import {
import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
import { BoardDiscovery } from '../../node/board-discovery';
import { BoardsServiceImpl } from '../../node/boards-service-impl';
import { CLI_CONFIG, CliConfig, DefaultCliConfig } from '../../node/cli-config';
import { ConfigServiceImpl } from '../../node/config-service-impl';
import { CoreClientProvider } from '../../node/core-client-provider';
import { CoreServiceImpl } from '../../node/core-service-impl';
import { IsTempSketch } from '../../node/is-temp-sketch';
import { LibraryServiceImpl } from '../../node/library-service-impl';
import { MonitorManager } from '../../node/monitor-manager';
import { MonitorService } from '../../node/monitor-service';
import {
@ -56,7 +66,12 @@ import {
} from '../../node/monitor-service-factory';
import { SettingsReader } from '../../node/settings-reader';
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
import { EnvVariablesServer } from '../../node/theia/env-variables/env-variables-server';
import {
ConfigDirUriProvider,
EnvVariablesServer,
} from '../../node/theia/env-variables/env-variables-server';
const tracked = track();
@injectable()
class ConsoleLogger extends MockLogger {
@ -234,12 +249,64 @@ class TestResponseService implements ResponseService {
}
}
export function createBaseContainer(
containerCustomizations?: (
class TestConfigDirUriProvider extends ConfigDirUriProvider {
constructor(private readonly configDirPath: string) {
super();
}
override configDirUri(): URI {
return FileUri.create(this.configDirPath);
}
}
function shouldKeepTestFolder(): boolean {
return (
typeof process.env.ARDUINO_IDE__KEEP_TEST_FOLDER === 'string' &&
/true/i.test(process.env.ARDUINO_IDE__KEEP_TEST_FOLDER)
);
}
export function newTempConfigDirPath(
prefix = 'arduino-ide--slow-tests'
): string {
let tempDirPath;
if (shouldKeepTestFolder()) {
tempDirPath = tempPath(prefix);
mkdirSync(tempDirPath, { recursive: true });
console.log(
`Detected ARDUINO_IDE__KEEP_TEST_FOLDER=true, keeping temporary test configuration folders: ${tempDirPath}`
);
} else {
tempDirPath = tracked.mkdirSync();
}
return join(tempDirPath, '.testArduinoIDE');
}
interface CreateBaseContainerParams {
readonly cliConfig?: CliConfig | (() => Promise<CliConfig>);
readonly configDirPath?: string;
readonly additionalBindings?: (
bind: interfaces.Bind,
rebind: interfaces.Rebind
) => void
): Container {
) => void;
}
export async function createBaseContainer(
params?: CreateBaseContainerParams
): Promise<Container> {
const configDirUriProvider = new TestConfigDirUriProvider(
params?.configDirPath || newTempConfigDirPath()
);
if (params?.cliConfig) {
const config =
typeof params.cliConfig === 'function'
? await params.cliConfig()
: params.cliConfig;
await writeCliConfigFile(
FileUri.fsPath(configDirUriProvider.configDirUri()),
config
);
}
const container = new Container({ defaultScope: 'Singleton' });
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(CoreClientProvider).toSelf().inSingletonScope();
@ -263,6 +330,7 @@ export function createBaseContainer(
return child.get<MonitorService>(MonitorService);
}
);
bind(ConfigDirUriProvider).toConstantValue(configDirUriProvider);
bind(EnvVariablesServer).toSelf().inSingletonScope();
bind(TheiaEnvVariablesServer).toService(EnvVariablesServer);
bind(SilentArduinoDaemon).toSelf().inSingletonScope();
@ -274,6 +342,7 @@ export function createBaseContainer(
bind(NotificationServiceServer).toService(TestNotificationServiceServer);
bind(ConfigServiceImpl).toSelf().inSingletonScope();
bind(ConfigService).toService(ConfigServiceImpl);
bind(CommandRegistry).toSelf().inSingletonScope();
bind(CommandService).toService(CommandRegistry);
bindContributionProvider(bind, CommandContribution);
bind(TestBoardDiscovery).toSelf().inSingletonScope();
@ -282,14 +351,48 @@ export function createBaseContainer(
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(SettingsReader).toSelf().inSingletonScope();
if (containerCustomizations) {
containerCustomizations(bind, rebind);
}
bind(LibraryServiceImpl).toSelf().inSingletonScope();
bind(LibraryService).toService(LibraryServiceImpl);
bind(ProcessUtils).toSelf().inSingletonScope();
params?.additionalBindings?.(bind, rebind);
});
container.load(module);
return container;
}
async function writeCliConfigFile(
containerFolderPath: string,
cliConfig: CliConfig
): Promise<void> {
await fs.mkdir(containerFolderPath, { recursive: true });
const yaml = dumpYaml(cliConfig);
const cliConfigPath = join(containerFolderPath, CLI_CONFIG);
await fs.writeFile(cliConfigPath, yaml);
console.debug(`Created CLI configuration file at ${cliConfigPath}:
${yaml}
`);
}
export async function createCliConfig(
configDirPath: string,
configOverrides: Partial<DefaultCliConfig> = {}
): Promise<DefaultCliConfig> {
const directories = {
data: join(configDirPath, 'data', 'Arduino15'),
downloads: join(configDirPath, 'data', 'Arduino15', 'staging'),
builtin: join(configDirPath, 'data', 'Arduino15', 'libraries'),
user: join(configDirPath, 'user', 'Arduino'),
};
for (const directoryPath of Object.values(directories)) {
await fs.mkdir(directoryPath, { recursive: true });
}
const config = { directories };
const mergedOverrides = deepmerge(configOverrides, <DefaultCliConfig>{
logging: { level: 'trace' },
});
return deepmerge(config, mergedOverrides);
}
export async function startDaemon(
container: Container,
toDispose: DisposableCollection,
@ -313,18 +416,3 @@ export async function startDaemon(
await startCustomizations(container, toDispose);
}
}
export function configureBackendApplicationConfigProvider(): void {
try {
BackendApplicationConfigProvider.get();
} catch (err) {
if (
err instanceof Error &&
err.message.includes('BackendApplicationConfigProvider#set')
) {
BackendApplicationConfigProvider.set({
configDirName: '.testArduinoIDE',
});
}
}
}

View File

@ -6296,11 +6296,6 @@ deepmerge@*, deepmerge@^4.2.2:
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==
deepmerge@2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.0.1.tgz#25c1c24f110fb914f80001b925264dd77f3f4312"
integrity sha512-VIPwiMJqJ13ZQfaCsIFnp5Me9tnjURiaIFxfz7EH0Ci0dTSQpZtSLrqOicXqEd/z2r+z+Klk9GzmnRsgpgbOsQ==
default-compare@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/default-compare/-/default-compare-1.0.0.tgz#cb61131844ad84d84788fb68fd01681ca7781a2f"