mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-04-19 12:57:17 +00:00
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:
parent
097c92d904
commit
51f69f6a59
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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.');
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 });
|
||||
}
|
@ -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);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user