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 { // NOOP } override async stop(): Promise { // 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); readonly configDirPath?: string; readonly additionalBindings?: ( bind: interfaces.Bind, rebind: interfaces.Rebind ) => 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) => { 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) .toConstantValue({ ...options, }); child.bind(MonitorService).toSelf(); return child.get(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 { 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'), 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, startCustomizations?: ( container: Container, toDispose: DisposableCollection ) => Promise ): Promise { const daemon = container.get(ArduinoDaemonImpl); const configService = container.get(ConfigServiceImpl); const coreClientProvider = container.get(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); } }