Files
arduino-ide/arduino-ide-extension/src/test/node/node-test-bindings.ts
2025-03-29 01:33:25 +09:00

327 lines
11 KiB
TypeScript

import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { waitForEvent } from '@theia/core/lib/common/promise-util';
import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/common/file-uri';
import { ProcessUtils } from '@theia/core/lib/node/process-utils';
import {
Container,
ContainerModule,
injectable,
interfaces,
} from '@theia/core/shared/inversify';
import deepmerge from 'deepmerge';
import { dump as dumpYaml } from 'js-yaml';
import { mkdirSync, promises as fs } from 'node:fs';
import { join } from 'node:path';
import { path as tempPath, track } from 'temp';
import {
ArduinoDaemon,
BoardsPackage,
BoardsService,
ConfigService,
ConfigState,
CoreService,
DetectedPorts,
IndexUpdateDidCompleteParams,
IndexUpdateDidFailParams,
IndexUpdateParams,
LibraryPackage,
LibraryService,
NotificationServiceClient,
NotificationServiceServer,
OutputMessage,
ProgressMessage,
ResponseService,
Sketch,
SketchesService,
} from '../../common/protocol';
import { ArduinoDaemonImpl } from '../../node/arduino-daemon-impl';
import { BoardDiscovery } from '../../node/board-discovery';
import { BoardsServiceImpl } from '../../node/boards-service-impl';
import { CliConfig, CLI_CONFIG, 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 {
MonitorServiceFactory,
MonitorServiceFactoryOptions,
} from '../../node/monitor-service-factory';
import { SettingsReader } from '../../node/settings-reader';
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
import {
ConfigDirUriProvider,
EnvVariablesServer,
} from '../../node/theia/env-variables/env-variables-server';
import { bindCommon } from '../common/common-test-bindings';
const tracked = track();
@injectable()
class SilentArduinoDaemon extends ArduinoDaemonImpl {
protected override onData(): void {
// NOOP
}
}
@injectable()
class TestBoardDiscovery extends BoardDiscovery {
mutableDetectedPorts: DetectedPorts = {};
override async start(): Promise<void> {
// NOOP
}
override async stop(): Promise<void> {
// NOOP
}
override get detectedPorts(): DetectedPorts {
return this.mutableDetectedPorts;
}
}
@injectable()
class TestNotificationServiceServer implements NotificationServiceServer {
readonly events: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
disposeClient(client: NotificationServiceClient): void {
this.events.push('disposeClient:');
}
notifyDidReinitialize(): void {
this.events.push('notifyDidReinitialize:');
}
notifyIndexUpdateWillStart(params: IndexUpdateParams): void {
this.events.push(`notifyIndexUpdateWillStart:${JSON.stringify(params)}`);
}
notifyIndexUpdateDidProgress(progressMessage: ProgressMessage): void {
this.events.push(
`notifyIndexUpdateDidProgress:${JSON.stringify(progressMessage)}`
);
}
notifyIndexUpdateDidComplete(params: IndexUpdateDidCompleteParams): void {
this.events.push(`notifyIndexUpdateDidComplete:${JSON.stringify(params)}`);
}
notifyIndexUpdateDidFail(params: IndexUpdateDidFailParams): void {
this.events.push(`notifyIndexUpdateDidFail:${JSON.stringify(params)}`);
}
notifyDaemonDidStart(port: number): void {
this.events.push(`notifyDaemonDidStart:${port}`);
}
notifyDaemonDidStop(): void {
this.events.push('notifyDaemonDidStop:');
}
notifyConfigDidChange(event: ConfigState): void {
this.events.push(`notifyConfigDidChange:${JSON.stringify(event)}`);
}
notifyPlatformDidInstall(event: { item: BoardsPackage }): void {
this.events.push(`notifyPlatformDidInstall:${JSON.stringify(event)}`);
}
notifyPlatformDidUninstall(event: { item: BoardsPackage }): void {
this.events.push(`notifyPlatformDidUninstall:${JSON.stringify(event)}`);
}
notifyLibraryDidInstall(event: {
item: LibraryPackage | 'zip-install';
}): void {
this.events.push(`notifyLibraryDidInstall:${JSON.stringify(event)}`);
}
notifyLibraryDidUninstall(event: { item: LibraryPackage }): void {
this.events.push(`notifyLibraryDidUninstall:${JSON.stringify(event)}`);
}
notifyDetectedPortsDidChange(event: { detectedPorts: DetectedPorts }): void {
this.events.push(`notifyAttachedBoardsDidChange:${JSON.stringify(event)}`);
}
notifyRecentSketchesDidChange(event: { sketches: Sketch[] }): void {
this.events.push(`notifyRecentSketchesDidChange:${JSON.stringify(event)}`);
}
dispose(): void {
this.events.push('dispose:');
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-vars
setClient(client: NotificationServiceClient | undefined): void {
this.events.push('setClient:');
}
}
@injectable()
class TestResponseService implements ResponseService {
readonly outputMessages: OutputMessage[] = [];
readonly progressMessages: ProgressMessage[] = [];
appendToOutput(message: OutputMessage): void {
this.outputMessages.push(message);
}
reportProgress(message: ProgressMessage): void {
this.progressMessages.push(message);
}
}
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;
}
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) => {
bindCommon(bind, unbind, isBound, rebind);
bind(CoreClientProvider).toSelf().inSingletonScope();
bind(CoreServiceImpl).toSelf().inSingletonScope();
bind(CoreService).toService(CoreServiceImpl);
bind(BoardsServiceImpl).toSelf().inSingletonScope();
bind(BoardsService).toService(BoardsServiceImpl);
bind(TestResponseService).toSelf().inSingletonScope();
bind(ResponseService).toService(TestResponseService);
bind(MonitorManager).toSelf().inSingletonScope();
bind(MonitorServiceFactory).toFactory(
({ container }) =>
(options: MonitorServiceFactoryOptions) => {
const child = container.createChild();
child
.bind<MonitorServiceFactoryOptions>(MonitorServiceFactoryOptions)
.toConstantValue({
...options,
});
child.bind(MonitorService).toSelf();
return child.get<MonitorService>(MonitorService);
}
);
bind(ConfigDirUriProvider).toConstantValue(configDirUriProvider);
bind(EnvVariablesServer).toSelf().inSingletonScope();
bind(TheiaEnvVariablesServer).toService(EnvVariablesServer);
bind(SilentArduinoDaemon).toSelf().inSingletonScope();
bind(ArduinoDaemon).toService(SilentArduinoDaemon);
bind(ArduinoDaemonImpl).toService(SilentArduinoDaemon);
bind(TestNotificationServiceServer).toSelf().inSingletonScope();
bind(NotificationServiceServer).toService(TestNotificationServiceServer);
bind(ConfigServiceImpl).toSelf().inSingletonScope();
bind(ConfigService).toService(ConfigServiceImpl);
bind(TestBoardDiscovery).toSelf().inSingletonScope();
bind(BoardDiscovery).toService(TestBoardDiscovery);
bind(IsTempSketch).toSelf().inSingletonScope();
bind(SketchesServiceImpl).toSelf().inSingletonScope();
bind(SketchesService).toService(SketchesServiceImpl);
bind(SettingsReader).toSelf().inSingletonScope();
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'),
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,
startCustomizations?: (
container: Container,
toDispose: DisposableCollection
) => Promise<void>
): Promise<void> {
const daemon = container.get<ArduinoDaemonImpl>(ArduinoDaemonImpl);
const configService = container.get<ConfigServiceImpl>(ConfigServiceImpl);
const coreClientProvider =
container.get<CoreClientProvider>(CoreClientProvider);
toDispose.push(Disposable.create(() => daemon.stop()));
configService.onStart();
daemon.onStart();
await Promise.all([
waitForEvent(daemon.onDaemonStarted, 10_000),
coreClientProvider.client,
]);
if (startCustomizations) {
await startCustomizations(container, toDispose);
}
}