test: test Arduino state update for extensions

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2023-11-23 10:56:09 +01:00 committed by Akos Kitta
parent 5abdc18fcc
commit 42bf1a0e99
7 changed files with 716 additions and 11 deletions

View File

@ -21,7 +21,10 @@ import { BoardsServiceProvider } from '../boards/boards-service-provider';
import { CurrentSketch } from '../sketches-service-client-impl';
import { SketchContribution } from './contribution';
interface UpdateStateParams<T extends ArduinoState> {
/**
* (non-API) exported for tests
*/
export interface UpdateStateParams<T extends ArduinoState = ArduinoState> {
readonly key: keyof T;
readonly value: T[keyof T];
}

View File

@ -39,6 +39,7 @@ import {
uno,
unoSerialPort,
} from '../common/fixtures';
import { bindBrowser } from './browser-test-bindings';
disableJSDOM();
@ -390,7 +391,7 @@ describe('board-service-provider', () => {
const container = new Container({ defaultScope: 'Singleton' });
container.load(
new ContainerModule((bind, unbind, isBound, rebind) => {
bindCommon(bind);
bindBrowser(bind, unbind, isBound, rebind);
bind(MessageService).toConstantValue(<MessageService>{});
bind(BoardsService).toConstantValue(<BoardsService>{
getDetectedPorts() {

View File

@ -1,8 +1,25 @@
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { Container, ContainerModule } from '@theia/core/shared/inversify';
import { bindCommon } from '../common/common-test-bindings';
import {
Bind,
ConsoleLogger,
bindCommon,
} from '../common/common-test-bindings';
export function createBaseContainer(): Container {
export function createBaseContainer(bind: Bind = bindBrowser): Container {
const container = new Container({ defaultScope: 'Singleton' });
container.load(new ContainerModule((bind) => bindCommon(bind)));
container.load(new ContainerModule(bind));
return container;
}
export const bindBrowser: Bind = function (
...args: Parameters<Bind>
): ReturnType<Bind> {
bindCommon(...args);
const [bind, , , rebind] = args;
// IDE2's test console logger does not support `Loggable` arg.
// Rebind logger to suppress `[Function (anonymous)]` messages in tests when the storage service is initialized without `window.localStorage`.
// https://github.com/eclipse-theia/theia/blob/04c8cf07843ea67402131132e033cdd54900c010/packages/core/src/browser/storage-service.ts#L60
bind(MockLogger).toSelf().inSingletonScope();
rebind(ConsoleLogger).toService(MockLogger);
};

View File

@ -0,0 +1,670 @@
import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
const disableJSDOM = enableJSDOM();
import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
FrontendApplicationConfigProvider.set({});
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { OpenerService } from '@theia/core/lib/browser/opener-service';
import {
LocalStorageService,
StorageService,
} from '@theia/core/lib/browser/storage-service';
import { WindowService } from '@theia/core/lib/browser/window/window-service';
import {
Disposable,
DisposableCollection,
} from '@theia/core/lib/common/disposable';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { Emitter } from '@theia/core/lib/common/event';
import { MessageService } from '@theia/core/lib/common/message-service';
import { wait } from '@theia/core/lib/common/promise-util';
import URI from '@theia/core/lib/common/uri';
import {
Container,
ContainerModule,
injectable,
} from '@theia/core/shared/inversify';
import { EditorManager } from '@theia/editor/lib/browser/editor-manager';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { expect } from 'chai';
import type {
BoardDetails as ApiBoardDetails,
CompileSummary as ApiCompileSummary,
Port as ApiPort,
} from 'vscode-arduino-api';
import { URI as CodeURI } from 'vscode-uri';
import { ArduinoPreferences } from '../../browser/arduino-preferences';
import { BoardsDataStore } from '../../browser/boards/boards-data-store';
import { BoardsServiceProvider } from '../../browser/boards/boards-service-provider';
import { ConfigServiceClient } from '../../browser/config/config-service-client';
import { CommandRegistry } from '../../browser/contributions/contribution';
import {
UpdateArduinoState,
UpdateStateParams,
} from '../../browser/contributions/update-arduino-state';
import { DialogService } from '../../browser/dialog-service';
import { SettingsService } from '../../browser/dialogs/settings/settings';
import { HostedPluginSupport } from '../../browser/hosted/hosted-plugin-support';
import { NotificationCenter } from '../../browser/notification-center';
import {
CurrentSketch,
SketchesServiceClientImpl,
} from '../../browser/sketches-service-client-impl';
import { ApplicationConnectionStatusContribution } from '../../browser/theia/core/connection-status-service';
import { OutputChannelManager } from '../../browser/theia/output/output-channel';
import { WorkspaceService } from '../../browser/theia/workspace/workspace-service';
import { MainMenuManager } from '../../common/main-menu-manager';
import {
CompileSummary,
FileSystemExt,
SketchesService,
} from '../../common/protocol';
import {
BoardDetails,
BoardsService,
Port,
} from '../../common/protocol/boards-service';
import { NotificationServiceServer } from '../../common/protocol/notification-service';
import { never } from '../utils';
import { bindBrowser } from './browser-test-bindings';
disableJSDOM();
describe('update-arduino-state', function () {
this.slow(250);
let toDisposeAfterEach: DisposableCollection;
let boardsServiceProvider: BoardsServiceProvider;
let notificationCenter: NotificationCenter;
let commandRegistry: CommandRegistry;
let updateArduinoState: UpdateArduinoState;
let stateUpdateParams: UpdateStateParams[];
let boardDetailsMocks: Record<string, BoardDetails>;
let dataStoreMocks: Record<string, BoardsDataStore.Data>;
let currentSketchMock: CurrentSketch | undefined;
let sketchDirUriMock: URI | undefined;
let dataDirUriMock: URI | undefined;
let onCurrentSketchDidChangeEmitter: Emitter<CurrentSketch>;
let onDataDirDidChangeEmitter: Emitter<URI | undefined>;
let onSketchDirDidChangeEmitter: Emitter<URI | undefined>;
let onDataStoreDidChangeEmitter: Emitter<string[]>;
beforeEach(async () => {
toDisposeAfterEach = new DisposableCollection();
stateUpdateParams = [];
// reset mocks
boardDetailsMocks = {};
dataStoreMocks = {};
currentSketchMock = undefined;
sketchDirUriMock = undefined;
dataDirUriMock = undefined;
onCurrentSketchDidChangeEmitter = new Emitter();
onDataDirDidChangeEmitter = new Emitter();
onSketchDirDidChangeEmitter = new Emitter();
onDataStoreDidChangeEmitter = new Emitter();
toDisposeAfterEach.pushAll([
onCurrentSketchDidChangeEmitter,
onDataDirDidChangeEmitter,
onSketchDirDidChangeEmitter,
onDataStoreDidChangeEmitter,
]);
const container = createContainer();
commandRegistry = container.get<CommandRegistry>(CommandRegistry);
// This command is registered by vscode-arduino-api
commandRegistry.registerCommand(
{ id: 'arduinoAPI.updateState' },
{
execute: (params: UpdateStateParams) => stateUpdateParams.push(params),
}
);
// This command is contributed by the vscode-arduino-tools VSIX
commandRegistry.registerCommand(
{ id: 'arduino.languageserver.notifyBuildDidComplete' },
{
execute: () => {
/* NOOP */
},
}
);
container.get<FrontendApplicationStateService>(
FrontendApplicationStateService
).state = 'ready';
boardsServiceProvider = container.get<BoardsServiceProvider>(
BoardsServiceProvider
);
notificationCenter = container.get<NotificationCenter>(NotificationCenter);
updateArduinoState = container.get<UpdateArduinoState>(UpdateArduinoState);
toDisposeAfterEach.push(
Disposable.create(() => boardsServiceProvider.onStop())
);
boardsServiceProvider.onStart();
await boardsServiceProvider.ready;
updateArduinoState.onStart();
await wait(50);
stateUpdateParams = [];
});
afterEach(() => {
toDisposeAfterEach.dispose();
});
it('should automatically update the boards config (board+port) on ready', async () => {
const fqbn = 'a:b:c';
const board = { fqbn, name: 'ABC' };
const boardDetails = {
buildProperties: [],
configOptions: [],
debuggingSupported: false,
fqbn,
PID: '0',
VID: '0',
programmers: [],
requiredTools: [],
};
boardDetailsMocks = {
'a:b:c': boardDetails,
};
const port = { address: 'COM1', protocol: 'serial' };
boardsServiceProvider['_boardsConfig'] = {
selectedBoard: board,
selectedPort: port,
};
boardsServiceProvider['_detectedPorts'] = {
[Port.keyOf(port)]: {
port: {
address: 'COM1',
addressLabel: 'COM1 Port',
protocol: 'serial',
protocolLabel: 'Serial',
},
boards: [],
},
};
updateArduinoState.onReady();
await wait(50);
const params = stateUpdateParams.filter(
(param) =>
param.key === 'fqbn' ||
param.key === 'boardDetails' ||
param.key === 'port'
);
expect(params).to.be.deep.equal([
{ key: 'fqbn', value: 'a:b:c' },
{
key: 'boardDetails',
value: {
buildProperties: {},
configOptions: [],
fqbn: 'a:b:c',
programmers: [],
toolsDependencies: [],
} as ApiBoardDetails,
},
{
key: 'port',
value: {
address: 'COM1',
protocol: 'serial',
protocolLabel: 'Serial',
hardwareId: '',
label: 'COM1 Port',
properties: {},
} as ApiPort,
},
]);
});
it('should automatically update the sketch path on ready', async () => {
const uri = 'file:///path/to/my_sketch';
currentSketchMock = {
name: 'my_sketch',
uri,
mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino',
additionalFileUris: [],
otherSketchFileUris: [],
rootFolderFileUris: [],
};
updateArduinoState.onReady();
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'sketchPath'
);
expect(params).to.be.deep.equal([
{
key: 'sketchPath',
value: CodeURI.parse(uri).fsPath,
},
]);
});
it("should automatically update the 'directories.data' path on ready", async () => {
const uri = 'file:///path/to/data/dir';
dataDirUriMock = new URI(uri);
stateUpdateParams = [];
updateArduinoState.onReady();
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'dataDirPath'
);
expect(params).to.be.deep.equal([
{
key: 'dataDirPath',
value: CodeURI.parse(uri).fsPath,
},
]);
});
it("should automatically update the 'directories.user' path on ready", async () => {
const uri = 'file:///path/to/sketchbook';
sketchDirUriMock = new URI(uri);
updateArduinoState.onReady();
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'userDirPath'
);
expect(params).to.be.deep.equal([
{
key: 'userDirPath',
value: CodeURI.parse(uri).fsPath,
},
]);
});
it('should update the boards config (board only) when did change', async () => {
const fqbn = 'a:b:c';
const board = { fqbn, name: 'ABC' };
const boardDetails = {
buildProperties: [],
configOptions: [],
debuggingSupported: false,
fqbn,
PID: '0',
VID: '0',
programmers: [],
requiredTools: [],
};
boardDetailsMocks = {
'a:b:c': boardDetails,
};
boardsServiceProvider.updateConfig(board);
await wait(50);
const params = stateUpdateParams.filter(
(param) =>
param.key === 'fqbn' ||
param.key === 'boardDetails' ||
param.key === 'port'
);
expect(params).to.be.deep.equal([
{ key: 'fqbn', value: 'a:b:c' },
{
key: 'boardDetails',
value: {
buildProperties: {},
configOptions: [],
fqbn: 'a:b:c',
programmers: [],
toolsDependencies: [],
} as ApiBoardDetails,
},
{ key: 'port', value: undefined },
]);
});
it('should update the boards config (port only) when did change', async () => {
const port = { address: 'COM1', protocol: 'serial' };
notificationCenter.notifyDetectedPortsDidChange({
detectedPorts: {
[Port.keyOf(port)]: {
port: {
address: 'COM1',
addressLabel: 'COM1 Port',
protocol: 'serial',
protocolLabel: 'Serial',
},
boards: [],
},
},
});
boardsServiceProvider.updateConfig(port);
await wait(50);
const params = stateUpdateParams.filter(
(param) =>
param.key === 'fqbn' ||
param.key === 'boardDetails' ||
param.key === 'port'
);
expect(params).to.be.deep.equal([
{ key: 'fqbn', value: undefined },
{ key: 'boardDetails', value: undefined },
{
key: 'port',
value: {
address: 'COM1',
protocol: 'serial',
protocolLabel: 'Serial',
hardwareId: '',
label: 'COM1 Port',
properties: {},
} as ApiPort,
},
]);
});
it('should update the boards config (board+port) when did change', async () => {
const fqbn = 'a:b:c';
const board = { fqbn, name: 'ABC' };
const boardDetails = {
buildProperties: [],
configOptions: [],
debuggingSupported: false,
fqbn,
PID: '0',
VID: '0',
programmers: [],
requiredTools: [],
};
boardDetailsMocks = {
'a:b:c': boardDetails,
};
const port = { address: 'COM1', protocol: 'serial' };
boardsServiceProvider.updateConfig({
selectedBoard: board,
selectedPort: port,
});
notificationCenter.notifyDetectedPortsDidChange({
detectedPorts: {
[Port.keyOf(port)]: {
port: {
address: 'COM1',
addressLabel: 'COM1 Port',
protocol: 'serial',
protocolLabel: 'Serial',
},
boards: [],
},
},
});
await wait(50);
const params = stateUpdateParams.filter(
(param) =>
param.key === 'fqbn' ||
param.key === 'boardDetails' ||
param.key === 'port'
);
expect(params).to.be.deep.equal([
{ key: 'fqbn', value: 'a:b:c' },
{
key: 'boardDetails',
value: {
buildProperties: {},
configOptions: [],
fqbn: 'a:b:c',
programmers: [],
toolsDependencies: [],
} as ApiBoardDetails,
},
{
key: 'port',
value: {
address: 'COM1',
protocol: 'serial',
protocolLabel: 'Serial',
hardwareId: '',
label: 'COM1 Port',
properties: {},
} as ApiPort,
},
]);
});
it('should update the compile summary after a verify', async () => {
const summary: CompileSummary = {
buildPath: '/path/to/build',
buildProperties: [],
executableSectionsSize: [],
usedLibraries: [],
boardPlatform: undefined,
buildPlatform: undefined,
buildOutputUri: 'file:///path/to/build',
};
await commandRegistry.executeCommand(
'arduino.languageserver.notifyBuildDidComplete',
summary
);
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'compileSummary'
);
expect(params).to.be.deep.equal([
{
key: 'compileSummary',
value: {
buildPath: '/path/to/build',
buildProperties: {},
executableSectionsSize: [],
usedLibraries: [],
boardPlatform: undefined,
buildPlatform: undefined,
} as ApiCompileSummary,
},
]);
});
it('should update the current sketch when did change', async () => {
const uri = 'file:///path/to/my_sketch';
const sketch = {
name: 'my_sketch',
uri,
mainFileUri: 'file:///path/to/my_sketch/my_sketch.ino',
additionalFileUris: [],
otherSketchFileUris: [],
rootFolderFileUris: [],
};
onCurrentSketchDidChangeEmitter.fire(sketch);
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'sketchPath'
);
expect(params).to.be.deep.equal([
{
key: 'sketchPath',
value: CodeURI.parse(uri).fsPath,
},
]);
});
it("should update the 'directories.data' when did change", async () => {
const uri = new URI('file:///path/to/data/dir');
onDataDirDidChangeEmitter.fire(uri);
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'dataDirPath'
);
expect(params).to.be.deep.equal([
{
key: 'dataDirPath',
value: CodeURI.parse(uri.toString()).fsPath,
},
]);
});
it("should update the 'directories.user' when did change", async () => {
const uri = new URI('file:///path/to/sketchbook');
onSketchDirDidChangeEmitter.fire(uri);
await wait(50);
const params = stateUpdateParams.filter(
(param) => param.key === 'userDirPath'
);
expect(params).to.be.deep.equal([
{
key: 'userDirPath',
value: CodeURI.parse(uri.toString()).fsPath,
},
]);
});
it('should not update the board details when data store did change but the selected board does not match', async () => {
onDataStoreDidChangeEmitter.fire(['a:b:c']);
await wait(50);
expect(stateUpdateParams).to.be.empty;
});
it('should update the board details when the data store did change and the selected board matches', async () => {
const fqbn = 'a:b:c';
const board = { fqbn, name: 'ABC' };
const boardDetails = {
buildProperties: [],
configOptions: [],
debuggingSupported: false,
fqbn,
PID: '0',
VID: '0',
programmers: [],
requiredTools: [],
};
boardDetailsMocks = {
'a:b:c': boardDetails,
};
boardsServiceProvider['_boardsConfig'] = {
selectedBoard: board,
selectedPort: undefined,
};
onDataStoreDidChangeEmitter.fire(['a:b:c']);
await wait(50);
const params = stateUpdateParams.filter(
(param) =>
param.key === 'fqbn' ||
param.key === 'boardDetails' ||
param.key === 'port'
);
expect(params).to.be.deep.equal([
{
key: 'boardDetails',
value: {
buildProperties: {},
configOptions: [],
fqbn: 'a:b:c',
programmers: [],
toolsDependencies: [],
} as ApiBoardDetails,
},
]);
});
function createContainer(): Container {
const container = new Container({ defaultScope: 'Singleton' });
container.load(
new ContainerModule((bind, unbind, isBound, rebind) => {
bindBrowser(bind, unbind, isBound, rebind);
bind(MessageService).toConstantValue(<MessageService>{});
bind(BoardsService).toConstantValue(<BoardsService>{
getDetectedPorts() {
return {};
},
async getBoardDetails({ fqbn }) {
return boardDetailsMocks[fqbn];
},
});
bind(NotificationCenter).toSelf().inSingletonScope();
bind(NotificationServiceServer).toConstantValue(<
NotificationServiceServer
>{
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setClient(_) {
// nothing
},
});
bind(FrontendApplicationStateService).toSelf().inSingletonScope();
bind(BoardsDataStore).toConstantValue(<BoardsDataStore>{
async getData(fqbn) {
if (!fqbn) {
return BoardsDataStore.Data.EMPTY;
}
const data = dataStoreMocks[fqbn] ?? BoardsDataStore.Data.EMPTY;
return data;
},
get onChanged() {
return onDataStoreDidChangeEmitter.event;
},
});
bind(LocalStorageService).toSelf().inSingletonScope();
bind(WindowService).toConstantValue(<WindowService>{});
bind(StorageService).toService(LocalStorageService);
bind(BoardsServiceProvider).toSelf().inSingletonScope();
bind(NoopHostedPluginSupport).toSelf().inSingletonScope();
bind(HostedPluginSupport).toService(NoopHostedPluginSupport);
bind(UpdateArduinoState).toSelf().inSingletonScope();
bind(FileService).toConstantValue(<FileService>{});
bind(FileSystemExt).toConstantValue(<FileSystemExt>{});
bind(ConfigServiceClient).toConstantValue(<ConfigServiceClient>{
tryGetSketchDirUri() {
return sketchDirUriMock;
},
tryGetDataDirUri() {
return dataDirUriMock;
},
get onDidChangeSketchDirUri() {
return onSketchDirDidChangeEmitter.event;
},
get onDidChangeDataDirUri() {
return onDataDirDidChangeEmitter.event;
},
});
bind(SketchesService).toConstantValue(<SketchesService>{});
bind(OpenerService).toConstantValue(<OpenerService>{});
bind(SketchesServiceClientImpl).toConstantValue(<
SketchesServiceClientImpl
>{
tryGetCurrentSketch() {
return currentSketchMock;
},
onCurrentSketchDidChange: onCurrentSketchDidChangeEmitter.event,
});
bind(EditorManager).toConstantValue(<EditorManager>{});
bind(OutputChannelManager).toConstantValue(<OutputChannelManager>{});
bind(EnvVariablesServer).toConstantValue(<EnvVariablesServer>{});
bind(ApplicationConnectionStatusContribution).toConstantValue(
<ApplicationConnectionStatusContribution>{}
);
bind(WorkspaceService).toConstantValue(<WorkspaceService>{});
bind(LabelProvider).toConstantValue(<LabelProvider>{});
bind(SettingsService).toConstantValue(<SettingsService>{});
bind(ArduinoPreferences).toConstantValue(<ArduinoPreferences>{});
bind(DialogService).toConstantValue(<DialogService>{});
bind(MainMenuManager).toConstantValue(<MainMenuManager>{});
})
);
return container;
}
});
@injectable()
class NoopHostedPluginSupport implements HostedPluginSupport {
readonly didStart = Promise.resolve();
readonly onDidCloseConnection = never();
readonly onDidLoad = never();
}

View File

@ -9,14 +9,25 @@ import { LogLevel } from '@theia/core/lib/common/logger-protocol';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { injectable, interfaces } from '@theia/core/shared/inversify';
export function bindCommon(bind: interfaces.Bind): interfaces.Bind {
export interface Bind {
(
bind: interfaces.Bind,
unbind: interfaces.Unbind,
isBound: interfaces.IsBound,
rebind: interfaces.Rebind
): void;
}
export const bindCommon: Bind = function (
...args: Parameters<Bind>
): ReturnType<Bind> {
const [bind] = args;
bind(ConsoleLogger).toSelf().inSingletonScope();
bind(ILogger).toService(ConsoleLogger);
bind(CommandRegistry).toSelf().inSingletonScope();
bind(CommandService).toService(CommandRegistry);
bindContributionProvider(bind, CommandContribution);
return bind;
}
};
@injectable()
export class ConsoleLogger extends MockLogger {

View File

@ -222,7 +222,7 @@ export async function createBaseContainer(
}
const container = new Container({ defaultScope: 'Singleton' });
const module = new ContainerModule((bind, unbind, isBound, rebind) => {
bindCommon(bind);
bindCommon(bind, unbind, isBound, rebind);
bind(CoreClientProvider).toSelf().inSingletonScope();
bind(CoreServiceImpl).toSelf().inSingletonScope();
bind(CoreService).toService(CoreServiceImpl);

View File

@ -1,3 +1,6 @@
export function tick(): Promise<void> {
return new Promise((res) => setTimeout(res, 1));
import { Emitter, Event } from '@theia/core/lib/common/event';
const neverEmitter = new Emitter<unknown>();
export function never<T = void>(): Event<T> {
return neverEmitter.event as Event<T>;
}