From 367ef9bfc0f7eb4c096fac4ba21ac7f323ef7d66 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 14 Apr 2023 17:33:45 +0200 Subject: [PATCH] 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 --- arduino-ide-extension/package.json | 2 +- .../src/browser/create/create-api.ts | 2 +- .../src/node/arduino-daemon-impl.ts | 16 +- .../src/node/arduino-ide-backend-module.ts | 6 +- .../src/node/core-client-provider.ts | 3 - .../env-variables/env-variables-server.ts | 119 ++++++- .../node/boards-service-impl.slow-test.ts | 11 +- .../node/core-client-provider.slow-test.ts | 337 ++++++++++++++++++ .../test/node/core-service-impl.slow-test.ts | 22 +- .../node/library-service-impl.slow-test.ts | 17 +- .../node/sketches-service-impl.slow-test.ts | 11 +- .../src/test/node/test-bindings.ts | 136 +++++-- yarn.lock | 5 - 13 files changed, 596 insertions(+), 91 deletions(-) create mode 100644 arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 9d9eb0a7b..7a49093f5 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -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", diff --git a/arduino-ide-extension/src/browser/create/create-api.ts b/arduino-ide-extension/src/browser/create/create-api.ts index b2aac2b02..e3508bb98 100644 --- a/arduino-ide-extension/src/browser/create/create-api.ts +++ b/arduino-ide-extension/src/browser/create/create-api.ts @@ -513,7 +513,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 diff --git a/arduino-ide-extension/src/node/arduino-daemon-impl.ts b/arduino-ide-extension/src/node/arduino-daemon-impl.ts index 327544555..6008cdc95 100644 --- a/arduino-ide-extension/src/node/arduino-daemon-impl.ts +++ b/arduino-ide-extension/src/node/arduino-daemon-impl.ts @@ -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(); private readonly onDaemonStoppedEmitter = new Emitter(); @@ -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.'); diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 5cd81f7c3..d40a462e2 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -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); diff --git a/arduino-ide-extension/src/node/core-client-provider.ts b/arduino-ide-extension/src/node/core-client-provider.ts index 05b32d357..eb6f020ab 100644 --- a/arduino-ide-extension/src/node/core-client-provider.ts +++ b/arduino-ide-extension/src/node/core-client-provider.ts @@ -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); diff --git a/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts b/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts index 7380cb71e..888817d35 100644 --- a/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts +++ b/arduino-ide-extension/src/node/theia/env-variables/env-variables-server.ts @@ -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 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 extends TheiaEnvVariablesServerImpl { - protected override readonly configDirUri = Promise.resolve( - FileUri.create( - join(homedir(), BackendApplicationConfigProvider.get().configDirName) - ).toString() - ); +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 { + return process.execPath; + } + + async getVariables(): Promise { + return Object.keys(this.envs).map((key) => this.envs[key]); + } + + async getValue(key: string): Promise { + if (isWindows) { + key = key.toLowerCase(); + } + return this.envs[key]; + } + + async getConfigDirUri(): Promise { + return this.configDirUriProvider.configDirUri().toString(); + } + + async getHomeDirUri(): Promise { + return this.homeDirUri; + } + + async getDrives(): Promise { + 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; + } } diff --git a/arduino-ide-extension/src/test/node/boards-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/boards-service-impl.slow-test.ts index fe89cc4b2..de243d2f6 100644 --- a/arduino-ide-extension/src/test/node/boards-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/boards-service-impl.slow-test.ts @@ -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); }); @@ -94,7 +89,7 @@ describe('boards-service-impl', () => { }); }); -function createContainer(): Container { +async function createContainer(): Promise { return createBaseContainer(); } diff --git a/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts b/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts new file mode 100644 index 000000000..5e7c92ebf --- /dev/null +++ b/arduino-ide-extension/src/test/node/core-client-provider.slow-test.ts @@ -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 +): Promise { + const coreClientProvider = + container.get(CoreClientProvider); + const coreService = container.get(CoreService); + const libraryService = container.get(LibraryService); + const boardsService = container.get(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((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 = {}, + otherExpect?: (services: Services) => MaybePromise +): Promise { + 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); + return FileUri.fsPath(configDirUriProvider.configDirUri()); + } finally { + const daemon = container.get(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 { + 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 { + await startDaemon(container, toDispose); +} + +async function createContainer( + params: + | { configDirPath: string; configOverrides: Partial } + | string = newTempConfigDirPath() +): Promise { + if (typeof params === 'string') { + return createBaseContainer({ configDirPath: params }); + } + const { configDirPath, configOverrides } = params; + const cliConfig = await createCliConfig(configDirPath, configOverrides); + return createBaseContainer({ configDirPath, cliConfig }); +} diff --git a/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts index 700673c32..faac7b6c7 100644 --- a/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts @@ -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 { + return createBaseContainer({ + additionalBindings: (bind, rebind) => { + bind(TestCommandRegistry).toSelf().inSingletonScope(); + rebind(CommandRegistry).toService(TestCommandRegistry); + }, }); } diff --git a/arduino-ide-extension/src/test/node/library-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/library-service-impl.slow-test.ts index 95613395e..694b86d96 100644 --- a/arduino-ide-extension/src/test/node/library-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/library-service-impl.slow-test.ts @@ -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); }); @@ -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 { + return createBaseContainer(); } async function start( diff --git a/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts index 7669971b1..90ceb6679 100644 --- a/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts @@ -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 { return createBaseContainer(); } diff --git a/arduino-ide-extension/src/test/node/test-bindings.ts b/arduino-ide-extension/src/test/node/test-bindings.ts index 80a3eea7c..279c260a5 100644 --- a/arduino-ide-extension/src/test/node/test-bindings.ts +++ b/arduino-ide-extension/src/test/node/test-bindings.ts @@ -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); + readonly configDirPath?: string; + readonly additionalBindings?: ( bind: interfaces.Bind, rebind: interfaces.Rebind - ) => void -): Container { + ) => void; +} + +export async function createBaseContainer( + params?: CreateBaseContainerParams +): Promise { + 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); } ); + 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 { + 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 = {} +): Promise { + 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, { + 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', - }); - } - } -} diff --git a/yarn.lock b/yarn.lock index 44833b671..42abbace7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"