diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 08b884ab1..624f96027 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -17,7 +17,7 @@ "build": "tsc && ncp ./src/node/cli-protocol/ ./lib/node/cli-protocol/ && yarn lint", "watch": "tsc -w", "test": "mocha \"./lib/test/**/*.test.js\"", - "test:slow": "mocha \"./lib/test/**/*.slow-test.js\"", + "test:slow": "mocha \"./lib/test/**/*.slow-test.js\" --slow 5000", "test:watch": "mocha --watch --watch-files lib \"./lib/test/**/*.test.js\"" }, "dependencies": { diff --git a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts index 8b7626720..7067225dc 100644 --- a/arduino-ide-extension/src/browser/boards/boards-list-widget.ts +++ b/arduino-ide-extension/src/browser/boards/boards-list-widget.ts @@ -30,7 +30,6 @@ export class BoardsListWidget extends ListWidget { searchable: service, installable: service, itemLabel: (item: BoardsPackage) => item.name, - itemDeprecated: (item: BoardsPackage) => item.deprecated, itemRenderer, filterRenderer, defaultSearchOptions: { query: '', type: 'All' }, diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index c1ca83b35..4a0318d26 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -41,7 +41,6 @@ export class LibraryListWidget extends ListWidget< searchable: service, installable: service, itemLabel: (item: LibraryPackage) => item.name, - itemDeprecated: (item: LibraryPackage) => item.deprecated, itemRenderer, filterRenderer, defaultSearchOptions: { query: '', type: 'All', topic: 'All' }, diff --git a/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx b/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx index 2c49bbbcc..0f0dc9430 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/component-list.tsx @@ -147,7 +147,6 @@ export namespace ComponentList { export interface Props { readonly items: T[]; readonly itemLabel: (item: T) => string; - readonly itemDeprecated: (item: T) => boolean; readonly itemRenderer: ListItemRenderer; readonly install: (item: T, version?: Installable.Version) => Promise; readonly uninstall: (item: T) => Promise; diff --git a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx index 35289f073..07a1379ef 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/filterable-list-container.tsx @@ -82,12 +82,11 @@ export class FilterableListContainer< } protected renderComponentList(): React.ReactNode { - const { itemLabel, itemDeprecated, itemRenderer } = this.props; + const { itemLabel, itemRenderer } = this.props; return ( items={this.state.items} itemLabel={itemLabel} - itemDeprecated={itemDeprecated} itemRenderer={itemRenderer} install={this.install.bind(this)} uninstall={this.uninstall.bind(this)} @@ -109,9 +108,7 @@ export class FilterableListContainer< protected search(searchOptions: S): void { const { searchable } = this.props; - searchable - .search(searchOptions) - .then((items) => this.setState({ items: this.props.sort(items) })); + searchable.search(searchOptions).then((items) => this.setState({ items })); } protected async install( @@ -127,7 +124,7 @@ export class FilterableListContainer< run: ({ progressId }) => install({ item, progressId, version }), }); const items = await searchable.search(this.state.searchOptions); - this.setState({ items: this.props.sort(items) }); + this.setState({ items }); } protected async uninstall(item: T): Promise { @@ -155,7 +152,7 @@ export class FilterableListContainer< run: ({ progressId }) => uninstall({ item, progressId }), }); const items = await searchable.search(this.state.searchOptions); - this.setState({ items: this.props.sort(items) }); + this.setState({ items }); } } @@ -168,7 +165,6 @@ export namespace FilterableListContainer { readonly container: ListWidget; readonly searchable: Searchable; readonly itemLabel: (item: T) => string; - readonly itemDeprecated: (item: T) => boolean; readonly itemRenderer: ListItemRenderer; readonly filterRenderer: FilterRenderer; readonly resolveFocus: (element: HTMLElement | undefined) => void; @@ -192,7 +188,6 @@ export namespace FilterableListContainer { progressId: string; }) => Promise; readonly commandService: CommandService; - readonly sort: (items: T[]) => T[]; } export interface State { diff --git a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx index 25fea1050..a6cf5ffbf 100644 --- a/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx +++ b/arduino-ide-extension/src/browser/widgets/component-list/list-widget.tsx @@ -53,11 +53,9 @@ export abstract class ListWidget< */ protected firstActivate = true; - protected readonly defaultSortComparator: (left: T, right: T) => number; - constructor(protected options: ListWidget.Options) { super(); - const { id, label, iconClass, itemDeprecated, itemLabel } = options; + const { id, label, iconClass } = options; this.id = id; this.title.label = label; this.title.caption = label; @@ -67,15 +65,6 @@ export abstract class ListWidget< this.node.tabIndex = 0; // To be able to set the focus on the widget. this.scrollOptions = undefined; this.toDispose.push(this.searchOptionsChangeEmitter); - - this.defaultSortComparator = (left, right): number => { - // always put deprecated items at the bottom of the list - if (itemDeprecated(left)) { - return 1; - } - - return itemLabel(left).localeCompare(itemLabel(right)); - }; } @postConstruct() @@ -144,30 +133,6 @@ export abstract class ListWidget< return this.options.installable.uninstall({ item, progressId }); } - protected filterableListSort = (items: T[]): T[] => { - const isArduinoTypeComparator = (left: T, right: T) => { - const aIsArduinoType = left.types.includes('Arduino'); - const bIsArduinoType = right.types.includes('Arduino'); - - if (aIsArduinoType && !bIsArduinoType && !left.deprecated) { - return -1; - } - - if (!aIsArduinoType && bIsArduinoType && !right.deprecated) { - return 1; - } - - return 0; - }; - - return items.sort((left, right) => { - return ( - isArduinoTypeComparator(left, right) || - this.defaultSortComparator(left, right) - ); - }); - }; - render(): React.ReactNode { return ( @@ -178,14 +143,12 @@ export abstract class ListWidget< install={this.install.bind(this)} uninstall={this.uninstall.bind(this)} itemLabel={this.options.itemLabel} - itemDeprecated={this.options.itemDeprecated} itemRenderer={this.options.itemRenderer} filterRenderer={this.options.filterRenderer} searchOptionsDidChange={this.searchOptionsChangeEmitter.event} messageService={this.messageService} commandService={this.commandService} responseService={this.responseService} - sort={this.filterableListSort} /> ); } @@ -218,7 +181,6 @@ export namespace ListWidget { readonly installable: Installable; readonly searchable: Searchable; readonly itemLabel: (item: T) => string; - readonly itemDeprecated: (item: T) => boolean; readonly itemRenderer: ListItemRenderer; readonly filterRenderer: FilterRenderer; readonly defaultSearchOptions: S; diff --git a/arduino-ide-extension/src/common/protocol/arduino-component.ts b/arduino-ide-extension/src/common/protocol/arduino-component.ts index 2cdfe38a2..20d49f9be 100644 --- a/arduino-ide-extension/src/common/protocol/arduino-component.ts +++ b/arduino-ide-extension/src/common/protocol/arduino-component.ts @@ -2,7 +2,7 @@ import { Installable } from './installable'; export interface ArduinoComponent { readonly name: string; - readonly deprecated: boolean; + readonly deprecated?: boolean; readonly author: string; readonly summary: string; readonly description: string; diff --git a/arduino-ide-extension/src/common/protocol/searchable.ts b/arduino-ide-extension/src/common/protocol/searchable.ts index 30d3cd2dd..2caf53730 100644 --- a/arduino-ide-extension/src/common/protocol/searchable.ts +++ b/arduino-ide-extension/src/common/protocol/searchable.ts @@ -1,4 +1,5 @@ import URI from '@theia/core/lib/common/uri'; +import type { ArduinoComponent } from './arduino-component'; export interface Searchable { search(options: O): Promise; @@ -31,3 +32,31 @@ export namespace Searchable { } } } + +// IDE2 must keep the library search order from the CLI but do additional boosting +// https://github.com/arduino/arduino-ide/issues/1106 +// This additional search result boosting considers the following groups: 'Arduino', '', 'Arduino-Retired', and 'Retired'. +// If two libraries fall into the same group, the original index is the tiebreaker. +export type SortGroup = 'Arduino' | '' | 'Arduino-Retired' | 'Retired'; +const sortGroupOrder: Record = { + Arduino: 0, + '': 1, + 'Arduino-Retired': 2, + Retired: 3, +}; + +export function sortComponents( + components: T[], + group: (component: T) => SortGroup +): T[] { + return components + .map((component, index) => ({ ...component, index })) + .sort((left, right) => { + const leftGroup = group(left); + const rightGroup = group(right); + if (leftGroup === rightGroup) { + return left.index - right.index; + } + return sortGroupOrder[leftGroup] - sortGroupOrder[rightGroup]; + }); +} diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index 03c59eb6a..78549ab70 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -17,6 +17,8 @@ import { BoardWithPackage, BoardUserField, BoardSearch, + sortComponents, + SortGroup, } from '../common/protocol'; import { PlatformInstallRequest, @@ -405,7 +407,8 @@ export class BoardsServiceImpl } const filter = this.typePredicate(options); - return [...packages.values()].filter(filter); + const boardsPackages = [...packages.values()].filter(filter); + return sortComponents(boardsPackages, boardsPackageSortGroup); } private typePredicate( @@ -559,3 +562,14 @@ function isMissingPlatformError(error: unknown): boolean { } return false; } + +function boardsPackageSortGroup(boardsPackage: BoardsPackage): SortGroup { + const types: string[] = []; + if (boardsPackage.types.includes('Arduino')) { + types.push('Arduino'); + } + if (boardsPackage.deprecated) { + types.push('Retired'); + } + return types.join('-') as SortGroup; +} diff --git a/arduino-ide-extension/src/node/library-service-impl.ts b/arduino-ide-extension/src/node/library-service-impl.ts index db36b2c50..9d345b2d6 100644 --- a/arduino-ide-extension/src/node/library-service-impl.ts +++ b/arduino-ide-extension/src/node/library-service-impl.ts @@ -1,4 +1,14 @@ -import { injectable, inject } from '@theia/core/shared/inversify'; +import { ILogger, notEmpty } from '@theia/core'; +import { FileUri } from '@theia/core/lib/node'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { duration } from '../common/decorators'; +import { + NotificationServiceServer, + ResponseService, + sortComponents, + SortGroup, +} from '../common/protocol'; +import { Installable } from '../common/protocol/installable'; import { LibraryDependency, LibraryLocation, @@ -6,29 +16,24 @@ import { LibrarySearch, LibraryService, } from '../common/protocol/library-service'; -import { CoreClientAware } from './core-client-provider'; import { BoardDiscovery } from './board-discovery'; import { InstalledLibrary, Library, + LibraryInstallLocation, LibraryInstallRequest, LibraryListRequest, LibraryListResponse, LibraryLocation as GrpcLibraryLocation, LibraryRelease, LibraryResolveDependenciesRequest, - LibraryUninstallRequest, - ZipLibraryInstallRequest, LibrarySearchRequest, LibrarySearchResponse, - LibraryInstallLocation, + LibraryUninstallRequest, + ZipLibraryInstallRequest, } from './cli-protocol/cc/arduino/cli/commands/v1/lib_pb'; -import { Installable } from '../common/protocol/installable'; -import { ILogger, notEmpty } from '@theia/core'; -import { FileUri } from '@theia/core/lib/node'; -import { ResponseService, NotificationServiceServer } from '../common/protocol'; +import { CoreClientAware } from './core-client-provider'; import { ExecuteWithProgress } from './grpc-progressible'; -import { duration } from '../common/decorators'; @injectable() export class LibraryServiceImpl @@ -108,7 +113,10 @@ export class LibraryServiceImpl const typePredicate = this.typePredicate(options); const topicPredicate = this.topicPredicate(options); - return items.filter((item) => typePredicate(item) && topicPredicate(item)); + const libraries = items.filter( + (item) => typePredicate(item) && topicPredicate(item) + ); + return sortComponents(libraries, librarySortGroup); } private typePredicate( @@ -448,7 +456,6 @@ function toLibrary( name: '', exampleUris: [], installable: false, - deprecated: false, location: 0, ...pkg, @@ -462,3 +469,14 @@ function toLibrary( types: lib.getTypesList(), }; } + +// Libraries do not have a deprecated property. The deprecated information is inferred if 'Retired' is in 'types' +function librarySortGroup(library: LibraryPackage): SortGroup { + const types: string[] = []; + for (const type of ['Arduino', 'Retired']) { + if (library.types.includes(type)) { + types.push(type); + } + } + return types.join('-') as SortGroup; +} 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 new file mode 100644 index 000000000..dd9ec71ae --- /dev/null +++ b/arduino-ide-extension/src/test/node/boards-service-impl.slow-test.ts @@ -0,0 +1,114 @@ +import { Disposable } 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'; + +describe('boards-service-impl', () => { + let boardService: BoardsService; + let toDispose: Disposable[] = []; + + before(async function () { + configureBackendApplicationConfigProvider(); + this.timeout(20_000); + toDispose = []; + const container = createContainer(); + await start(container, toDispose); + boardService = container.get(BoardsService); + }); + + after(() => { + let disposable = toDispose.pop(); + while (disposable) { + try { + disposable?.dispose(); + } catch {} + disposable = toDispose.pop(); + } + }); + + describe('search', () => { + it('should run search', async function () { + const result = await boardService.search({}); + expect(result).is.not.empty; + }); + + it("should boost a result when 'types' includes 'arduino', and lower the score if deprecated", async function () { + const result = await boardService.search({}); + const arduinoIndexes: number[] = []; + const otherIndexes: number[] = []; + const deprecatedArduinoIndexes: number[] = []; + const deprecatedOtherIndexes: number[] = []; + const arduino: BoardSearch.Type = 'Arduino'; + result.forEach((platform, index) => { + if (platform.types.includes(arduino)) { + if (platform.deprecated) { + deprecatedArduinoIndexes.push(index); + } else { + arduinoIndexes.push(index); + } + } else { + if (platform.deprecated) { + deprecatedOtherIndexes.push(index); + } else { + otherIndexes.push(index); + } + } + }); + arduinoIndexes.forEach( + (index) => + expect(otherIndexes.every((otherIndex) => otherIndex > index)).to.be + .true + ); + otherIndexes.forEach( + (index) => + expect( + deprecatedArduinoIndexes.every( + (deprecatedArduinoIndex) => deprecatedArduinoIndex > index + ) + ).to.be.true + ); + deprecatedArduinoIndexes.forEach( + (index) => + expect( + deprecatedOtherIndexes.every( + (deprecatedOtherIndex) => deprecatedOtherIndex > index + ) + ).to.be.true + ); + }); + + it("should boost 'arduino' and deprecated to the end of the results", async function () { + const query = 'OS'; + const result = await boardService.search({ query }); + expect(result.length).greaterThan(1); + const lastIndex = result.length - 1; + const last = result[lastIndex]; + expect(last.id).to.be.equal('arduino:mbed'); + expect(last.deprecated).to.be.true; + const windowsIoTCoreIndex = result.findIndex( + (platform) => platform.id === 'Microsoft:win10' + ); + expect(windowsIoTCoreIndex).to.be.greaterThanOrEqual(0); + expect(windowsIoTCoreIndex).to.be.lessThan(lastIndex); + const first = result[0]; + expect(typeof first.deprecated).to.be.equal('boolean'); + expect(first.deprecated).to.be.false; + }); + }); +}); + +function createContainer(): Container { + return createBaseContainer(); +} + +async function start( + container: Container, + toDispose: Disposable[] +): Promise { + return startDaemon(container, toDispose); +} 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 173b719a2..fa73f6549 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 @@ -1,61 +1,20 @@ import { CancellationTokenSource } from '@theia/core/lib/common/cancellation'; -import { - CommandContribution, - CommandRegistry, - CommandService, -} from '@theia/core/lib/common/command'; -import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { CommandRegistry } from '@theia/core/lib/common/command'; import { Disposable } from '@theia/core/lib/common/disposable'; -import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables'; -import { ILogger, Loggable } from '@theia/core/lib/common/logger'; -import { LogLevel } from '@theia/core/lib/common/logger-protocol'; import { isWindows } from '@theia/core/lib/common/os'; -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 { FileUri } from '@theia/core/lib/node/file-uri'; -import { - Container, - ContainerModule, - injectable, -} from '@theia/core/shared/inversify'; +import { Container, injectable } from '@theia/core/shared/inversify'; import { expect } from 'chai'; import { - ArduinoDaemon, - AttachedBoardsChangeEvent, - AvailablePorts, - BoardsPackage, BoardsService, - ConfigService, - ConfigState, CoreService, - IndexUpdateDidCompleteParams, - IndexUpdateDidFailParams, - IndexUpdateParams, - LibraryPackage, - 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 { 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 { MonitorManager } from '../../node/monitor-manager'; -import { MonitorService } from '../../node/monitor-service'; import { - MonitorServiceFactory, - MonitorServiceFactoryOptions, -} from '../../node/monitor-service-factory'; -import { SketchesServiceImpl } from '../../node/sketches-service-impl'; -import { EnvVariablesServer } from '../../node/theia/env-variables/env-variables-server'; + configureBackendApplicationConfigProvider, + createBaseContainer, + startDaemon, +} from './test-bindings'; const testTimeout = 30_000; const setupTimeout = 5 * 60 * 1_000; // five minutes @@ -67,7 +26,7 @@ describe('core-service-impl', () => { let toDispose: Disposable[]; before(() => { - BackendApplicationConfigProvider.set({ configDirName: '.testArduinoIDE' }); + configureBackendApplicationConfigProvider(); }); beforeEach(async function () { @@ -135,159 +94,22 @@ async function start( container: Container, toDispose: Disposable[] ): Promise { - const daemon = container.get(ArduinoDaemonImpl); - const configService = container.get(ConfigServiceImpl); - toDispose.push(Disposable.create(() => daemon.stop())); - configService.onStart(); - daemon.onStart(); - await waitForEvent(daemon.onDaemonStarted, 10_000); - const boardService = container.get(BoardsService); - const searchResults = await boardService.search({ query: avr }); - const platform = searchResults.find(({ id }) => id === avr); - if (!platform) { - throw new Error(`Could not find platform: ${avr}`); - } - await boardService.install({ item: platform, skipPostInstall: true }); + await startDaemon(container, toDispose, async (container) => { + const boardService = container.get(BoardsService); + const searchResults = await boardService.search({ query: avr }); + const platform = searchResults.find(({ id }) => id === avr); + if (!platform) { + throw new Error(`Could not find platform: ${avr}`); + } + await boardService.install({ item: platform, skipPostInstall: true }); + }); } function createContainer(): Container { - const container = new Container({ defaultScope: 'Singleton' }); - const module = new ContainerModule((bind) => { - 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(EnvVariablesServer).toSelf().inSingletonScope(); - bind(TheiaEnvVariablesServer).toService(EnvVariablesServer); - bind(SilentArduinoDaemon).toSelf().inSingletonScope(); - bind(ArduinoDaemon).toService(SilentArduinoDaemon); - bind(ArduinoDaemonImpl).toService(SilentArduinoDaemon); - bind(ConsoleLogger).toSelf().inSingletonScope(); - bind(ILogger).toService(ConsoleLogger); - bind(TestNotificationServiceServer).toSelf().inSingletonScope(); - bind(NotificationServiceServer).toService(TestNotificationServiceServer); - bind(ConfigServiceImpl).toSelf().inSingletonScope(); - bind(ConfigService).toService(ConfigServiceImpl); + return createBaseContainer((bind) => { bind(TestCommandRegistry).toSelf().inSingletonScope(); bind(CommandRegistry).toService(TestCommandRegistry); - bind(CommandService).toService(CommandRegistry); - bindContributionProvider(bind, CommandContribution); - bind(TestBoardDiscovery).toSelf().inSingletonScope(); - bind(BoardDiscovery).toService(TestBoardDiscovery); - bind(IsTempSketch).toSelf().inSingletonScope(); - bind(SketchesServiceImpl).toSelf().inSingletonScope(); - bind(SketchesService).toService(SketchesServiceImpl); }); - container.load(module); - return container; -} - -@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); - } -} - -@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: string): 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)}`); - } - notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): 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 TestBoardDiscovery extends BoardDiscovery { - mutableAvailablePorts: AvailablePorts = {}; - - override async start(): Promise { - // NOOP - } - override async stop(): Promise { - // NOOP - } - override get availablePorts(): AvailablePorts { - return this.mutableAvailablePorts; - } } @injectable() @@ -314,88 +136,3 @@ class TestCommandRegistry extends CommandRegistry { return undefined; } } - -@injectable() -class ConsoleLogger extends MockLogger { - override log( - logLevel: number, - arg2: string | Loggable | Error, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...params: any[] - ): Promise { - if (arg2 instanceof Error) { - return this.error(String(arg2), params); - } - switch (logLevel) { - case LogLevel.INFO: - return this.info(arg2, params); - case LogLevel.WARN: - return this.warn(arg2, params); - case LogLevel.TRACE: - return this.trace(arg2, params); - case LogLevel.ERROR: - return this.error(arg2, params); - case LogLevel.FATAL: - return this.fatal(arg2, params); - default: - return this.info(arg2, params); - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override async info(arg: string | Loggable, ...params: any[]): Promise { - if (params.length) { - console.info(arg, ...params); - } else { - console.info(arg); - } - } - - override async trace( - arg: string | Loggable, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...params: any[] - ): Promise { - if (params.length) { - console.trace(arg, ...params); - } else { - console.trace(arg); - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override async warn(arg: string | Loggable, ...params: any[]): Promise { - if (params.length) { - console.warn(arg, ...params); - } else { - console.warn(arg); - } - } - - override async error( - arg: string | Loggable, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...params: any[] - ): Promise { - if (params.length) { - console.error(arg, ...params); - } else { - console.error(arg); - } - } - - override async fatal( - arg: string | Loggable, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...params: any[] - ): Promise { - return this.error(arg, params); - } -} - -@injectable() -class SilentArduinoDaemon extends ArduinoDaemonImpl { - protected override onData(): void { - // NOOP - } -} 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 new file mode 100644 index 000000000..cd0f85737 --- /dev/null +++ b/arduino-ide-extension/src/test/node/library-service-impl.slow-test.ts @@ -0,0 +1,95 @@ +import { Disposable } 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'; + +describe('library-service-impl', () => { + let libraryService: LibraryService; + let toDispose: Disposable[] = []; + + before(async function () { + configureBackendApplicationConfigProvider(); + this.timeout(20_000); + toDispose = []; + const container = createContainer(); + await start(container, toDispose); + libraryService = container.get(LibraryService); + }); + + after(() => { + let disposable = toDispose.pop(); + while (disposable) { + try { + disposable?.dispose(); + } catch {} + disposable = toDispose.pop(); + } + }); + + describe('search', () => { + it('should run search', async function () { + const result = await libraryService.search({}); + expect(result).is.not.empty; + }); + + it("should boost a result when 'types' includes 'arduino'", async function () { + const result = await libraryService.search({}); + const arduinoIndexes: number[] = []; + const otherIndexes: number[] = []; + // Special `"types": ["Arduino", "Retired"]` case handling: https://github.com/arduino/arduino-ide/issues/1106#issuecomment-1419392742 + const retiredIndexes: number[] = []; + const arduino: LibrarySearch.Type = 'Arduino'; + const retired: LibrarySearch.Type = 'Retired'; + result + .filter((library) => library.types.length === 1) + .forEach((library, index) => { + if (library.types.includes(arduino)) { + if (library.types.includes(retired)) { + retiredIndexes.push(index); + } else { + arduinoIndexes.push(index); + } + } else { + otherIndexes.push(index); + } + }); + arduinoIndexes.forEach( + (index) => + expect(otherIndexes.every((otherIndex) => otherIndex > index)).to.be + .true + ); + otherIndexes.forEach( + (index) => + expect(retiredIndexes.every((retiredIndex) => retiredIndex > index)) + .to.be.true + ); + }); + }); + + it("should boost library 'SD' to the top if the query term is 'SD'", async function () { + const query = 'SD'; + const result = await libraryService.search({ query }); + expect(result.length).greaterThan(1); + expect(result[0].name).to.be.equal(query); + }); +}); + +function createContainer(): Container { + return createBaseContainer((bind) => { + bind(LibraryServiceImpl).toSelf().inSingletonScope(); + bind(LibraryService).toService(LibraryServiceImpl); + }); +} + +async function start( + container: Container, + toDispose: Disposable[] +): Promise { + return startDaemon(container, toDispose); +} diff --git a/arduino-ide-extension/src/test/node/test-bindings.ts b/arduino-ide-extension/src/test/node/test-bindings.ts new file mode 100644 index 000000000..1aef2ff72 --- /dev/null +++ b/arduino-ide-extension/src/test/node/test-bindings.ts @@ -0,0 +1,320 @@ +import { + CommandContribution, + CommandRegistry, + CommandService, +} from '@theia/core/lib/common/command'; +import { bindContributionProvider } from '@theia/core/lib/common/contribution-provider'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { EnvVariablesServer as TheiaEnvVariablesServer } from '@theia/core/lib/common/env-variables'; +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 { + Container, + ContainerModule, + injectable, + interfaces, +} from '@theia/core/shared/inversify'; +import { + ArduinoDaemon, + AttachedBoardsChangeEvent, + AvailablePorts, + BoardsPackage, + BoardsService, + ConfigService, + ConfigState, + CoreService, + IndexUpdateDidCompleteParams, + IndexUpdateDidFailParams, + IndexUpdateParams, + LibraryPackage, + 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 { 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 { MonitorManager } from '../../node/monitor-manager'; +import { MonitorService } from '../../node/monitor-service'; +import { + MonitorServiceFactory, + MonitorServiceFactoryOptions, +} from '../../node/monitor-service-factory'; +import { SketchesServiceImpl } from '../../node/sketches-service-impl'; +import { EnvVariablesServer } from '../../node/theia/env-variables/env-variables-server'; + +@injectable() +class ConsoleLogger extends MockLogger { + override log( + logLevel: number, + arg2: string | Loggable | Error, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...params: any[] + ): Promise { + if (arg2 instanceof Error) { + return this.error(String(arg2), params); + } + switch (logLevel) { + case LogLevel.INFO: + return this.info(arg2, params); + case LogLevel.WARN: + return this.warn(arg2, params); + case LogLevel.TRACE: + return this.trace(arg2, params); + case LogLevel.ERROR: + return this.error(arg2, params); + case LogLevel.FATAL: + return this.fatal(arg2, params); + default: + return this.info(arg2, params); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override async info(arg: string | Loggable, ...params: any[]): Promise { + if (params.length) { + console.info(arg, ...params); + } else { + console.info(arg); + } + } + + override async trace( + arg: string | Loggable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...params: any[] + ): Promise { + if (params.length) { + console.trace(arg, ...params); + } else { + console.trace(arg); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override async warn(arg: string | Loggable, ...params: any[]): Promise { + if (params.length) { + console.warn(arg, ...params); + } else { + console.warn(arg); + } + } + + override async error( + arg: string | Loggable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...params: any[] + ): Promise { + if (params.length) { + console.error(arg, ...params); + } else { + console.error(arg); + } + } + + override async fatal( + arg: string | Loggable, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...params: any[] + ): Promise { + return this.error(arg, params); + } +} + +@injectable() +class SilentArduinoDaemon extends ArduinoDaemonImpl { + protected override onData(): void { + // NOOP + } +} + +@injectable() +class TestBoardDiscovery extends BoardDiscovery { + mutableAvailablePorts: AvailablePorts = {}; + + override async start(): Promise { + // NOOP + } + override async stop(): Promise { + // NOOP + } + override get availablePorts(): AvailablePorts { + return this.mutableAvailablePorts; + } +} + +@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: string): 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)}`); + } + notifyAttachedBoardsDidChange(event: AttachedBoardsChangeEvent): 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); + } +} + +export function createBaseContainer( + containerCustomizations?: ( + bind: interfaces.Bind, + rebind: interfaces.Rebind + ) => void +): Container { + const container = new Container({ defaultScope: 'Singleton' }); + const module = new ContainerModule((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(EnvVariablesServer).toSelf().inSingletonScope(); + bind(TheiaEnvVariablesServer).toService(EnvVariablesServer); + bind(SilentArduinoDaemon).toSelf().inSingletonScope(); + bind(ArduinoDaemon).toService(SilentArduinoDaemon); + bind(ArduinoDaemonImpl).toService(SilentArduinoDaemon); + bind(ConsoleLogger).toSelf().inSingletonScope(); + bind(ILogger).toService(ConsoleLogger); + bind(TestNotificationServiceServer).toSelf().inSingletonScope(); + bind(NotificationServiceServer).toService(TestNotificationServiceServer); + bind(ConfigServiceImpl).toSelf().inSingletonScope(); + bind(ConfigService).toService(ConfigServiceImpl); + bind(CommandService).toService(CommandRegistry); + bindContributionProvider(bind, CommandContribution); + bind(TestBoardDiscovery).toSelf().inSingletonScope(); + bind(BoardDiscovery).toService(TestBoardDiscovery); + bind(IsTempSketch).toSelf().inSingletonScope(); + bind(SketchesServiceImpl).toSelf().inSingletonScope(); + bind(SketchesService).toService(SketchesServiceImpl); + if (containerCustomizations) { + containerCustomizations(bind, rebind); + } + }); + container.load(module); + return container; +} + +export async function startDaemon( + container: Container, + toDispose: Disposable[], + startCustomizations?: ( + container: Container, + toDispose: Disposable[] + ) => Promise +): Promise { + const daemon = container.get(ArduinoDaemonImpl); + const configService = container.get(ConfigServiceImpl); + toDispose.push(Disposable.create(() => daemon.stop())); + configService.onStart(); + daemon.onStart(); + await waitForEvent(daemon.onDaemonStarted, 10_000); + if (startCustomizations) { + 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', + }); + } + } +}