Pluggable monitor (#982)

* backend structure WIP

* Scaffold interfaces and classes for pluggable monitors

* Implement MonitorService to handle pluggable monitor lifetime

* Rename WebSocketService to WebSocketProvider and uninjected it

* Moved some interfaces

* Changed upload settings

* Enhance MonitorManager APIs

* Fixed WebSocketChange event signature

* Add monitor proxy functions for the frontend

* Moved settings to MonitorService

* Remove several unnecessary serial monitor classes

* Changed how connection is handled on upload

* Proxied more monitor methods to frontend

* WebSocketProvider is not injectable anymore

* Add generic monitor settings storaging

* More serial classes removal

* Remove unused file

* Changed plotter contribution to use new manager proxy

* Changed MonitorWidget and children to use new monitor proxy

* Updated MonitorWidget to use new monitor proxy

* Fix backend logger bindings

* Delete unnecessary Symbol

* coreClientProvider is now set when constructing MonitorService

* Add missing binding

* Fix `MonitorManagerProxy` DI issue

* fix monitor connection

* delete duplex when connection is closed

* update arduino-cli to 0.22.0

* fix upload when monitor is open

* add MonitorSettingsProvider interface

* monitor settings provider stub

* updated pseudo code

* refactor monitor settings interfaces

* monitor service provider singleton

* add unit tests

* change MonitorService providers to injectable deps

* fix monitor settings client communication

* refactor monitor commands protocol

* use monitor settings provider properly

* add settings to monitor model

* add settings to monitor model

* reset serial monitor when port changes

* fix serial plotter opening

* refine monitor connection settings

* fix hanging web socket connections

* add serial plotter reset command

* send port to web socket clients

* monitor service wait for success serial port open

* fix reset loop

* update serial plotter version

* update arduino-cli version to 0.23.0-rc1 and regenerate grpc protocol

* remove useless plotter protocol file

* localize web socket errors

* clean-up code

* update translation file

* Fix duplicated editor tabs (#1012)

* Save dialog for closing temporary sketch and unsaved files (#893)

* Use normal `OnWillStop` event

* Align `CLOSE` command to rest of app

* Fixed FS path vs encoded URL comparision when handling stop request.

Ref: https://github.com/eclipse-theia/theia/issues/11226
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Fixed the translations again.

Removed `electron` from the `nls-extract`. It does not contain app code.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* Aligned the stop handler code to Theia.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

Co-authored-by: Akos Kitta <a.kitta@arduino.cc>

* fix serial monitor send line ending

* refactor monitor-service poll for test/readability

* localize web socket errors

* update translation file

* Fix duplicated editor tabs (#1012)

* i18n:check rerun

* Speed up IDE startup time.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>

* override coreClientProvider in monitor-service

* cleanup merged code

Co-authored-by: Francesco Stasi <f.stasi@me.com>
Co-authored-by: Silvano Cerza <silvanocerza@gmail.com>
Co-authored-by: Mark Sujew <mark.sujew@typefox.io>
Co-authored-by: David Simpson <45690499+davegarthsimpson@users.noreply.github.com>
Co-authored-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Alberto Iannaccone
2022-06-07 15:51:12 +02:00
committed by GitHub
parent 4c55807392
commit df8658eff9
43 changed files with 2329 additions and 1567 deletions

View File

@@ -0,0 +1,193 @@
import { expect } from 'chai';
import {
longestPrefixMatch,
reconcileSettings,
} from '../../node/monitor-settings/monitor-settings-utils';
import { PluggableMonitorSettings } from '../../node/monitor-settings/monitor-settings-provider';
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
describe('longestPrefixMatch', () => {
const settings = {
'arduino:avr:uno-port1-protocol1': {
name: 'Arduino Uno',
},
'arduino:avr:due-port1-protocol2': {
name: 'Arduino Due',
},
};
it('should return the exact prefix when found', async () => {
const prefix = 'arduino:avr:uno-port1-protocol1';
const { matchingPrefix } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingPrefix).to.equal(prefix);
});
it('should return the exact object when the prefix match', async () => {
const prefix = 'arduino:avr:uno-port1-protocol1';
const { matchingSettings } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingSettings).to.have.property('name').to.equal('Arduino Uno');
});
it('should return a partial matching prefix when a similar object is found', async () => {
const prefix = 'arduino:avr:due-port2-protocol2';
const { matchingPrefix } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingPrefix).to.equal('arduino:avr:due');
});
it('should return the closest object when the prefix partially match', async () => {
const prefix = 'arduino:avr:uno-port1-protocol2';
const { matchingSettings } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingSettings).to.have.property('name').to.equal('Arduino Uno');
});
it('should return an empty matching prefix when no similar object is found', async () => {
const prefix = 'arduino:avr:tre-port2-protocol2';
const { matchingPrefix } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingPrefix).to.equal('');
});
it('should return an empty object when no similar object is found', async () => {
const prefix = 'arduino:avr:tre-port1-protocol2';
const { matchingSettings } = longestPrefixMatch(
prefix,
settings as unknown as Record<string, PluggableMonitorSettings>
);
expect(matchingSettings).to.be.empty;
});
});
describe('reconcileSettings', () => {
const defaultSettings = {
setting1: {
id: 'setting1',
label: 'Label setting1',
type: 'enum',
values: ['a', 'b', 'c'],
selectedValue: 'b',
},
setting2: {
id: 'setting2',
label: 'Label setting2',
type: 'enum',
values: ['a', 'b', 'c'],
selectedValue: 'b',
},
setting3: {
id: 'setting3',
label: 'Label setting3',
type: 'enum',
values: ['a', 'b', 'c'],
selectedValue: 'b',
},
};
it('should return default settings if new settings are missing', async () => {
const newSettings: PluggableMonitorSettings = {};
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings).to.deep.equal(defaultSettings);
});
it('should add missing attributes copying it from the default settings', async () => {
const newSettings: PluggableMonitorSettings = JSON.parse(
JSON.stringify(defaultSettings)
);
delete newSettings.setting2;
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings).to.have.property('setting2');
});
it('should remove wrong settings attributes using the default settings as a reference', async () => {
const newSettings: PluggableMonitorSettings = JSON.parse(
JSON.stringify(defaultSettings)
);
newSettings['setting4'] = defaultSettings.setting3;
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings).not.to.have.property('setting4');
});
it('should reset non-value fields to those defiend in the default settings', async () => {
const newSettings: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
JSON.stringify(defaultSettings)
);
newSettings['setting2'].id = 'fake id';
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings.setting2)
.to.have.property('id')
.equal('setting2');
});
it('should accept a selectedValue if it is a valid one', async () => {
const newSettings: PluggableMonitorSettings = JSON.parse(
JSON.stringify(defaultSettings)
);
newSettings.setting2.selectedValue = 'c';
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings.setting2)
.to.have.property('selectedValue')
.to.equal('c');
});
it('should fall a back to the first valid setting when the selectedValue is not valid', async () => {
const newSettings: PluggableMonitorSettings = JSON.parse(
JSON.stringify(defaultSettings)
);
newSettings.setting2.selectedValue = 'z';
const reconciledSettings = reconcileSettings(newSettings, defaultSettings);
expect(reconciledSettings.setting2)
.to.have.property('selectedValue')
.to.equal('a');
});
it('should accept any value if default values are not set', async () => {
const wrongDefaults: DeepWriteable<PluggableMonitorSettings> = JSON.parse(
JSON.stringify(defaultSettings)
);
wrongDefaults.setting2.values = [];
const newSettings: PluggableMonitorSettings = JSON.parse(
JSON.stringify(wrongDefaults)
);
newSettings.setting2.selectedValue = 'z';
const reconciledSettings = reconcileSettings(newSettings, wrongDefaults);
expect(reconciledSettings.setting2)
.to.have.property('selectedValue')
.to.equal('z');
});
});

View File

@@ -1,167 +0,0 @@
import { SerialServiceImpl } from './../../node/serial/serial-service-impl';
import { IMock, It, Mock } from 'typemoq';
import { createSandbox } from 'sinon';
import * as sinonChai from 'sinon-chai';
import { expect, use } from 'chai';
use(sinonChai);
import { ILogger } from '@theia/core/lib/common/logger';
import { MonitorClientProvider } from '../../node/serial/monitor-client-provider';
import { WebSocketService } from '../../node/web-socket/web-socket-service';
import { MonitorServiceClient } from '../../node/cli-protocol/cc/arduino/cli/monitor/v1/monitor_grpc_pb';
import { Status } from '../../common/protocol';
describe('SerialServiceImpl', () => {
let subject: SerialServiceImpl;
let logger: IMock<ILogger>;
let serialClientProvider: IMock<MonitorClientProvider>;
let webSocketService: IMock<WebSocketService>;
beforeEach(() => {
logger = Mock.ofType<ILogger>();
logger.setup((b) => b.info(It.isAnyString()));
logger.setup((b) => b.warn(It.isAnyString()));
logger.setup((b) => b.error(It.isAnyString()));
serialClientProvider = Mock.ofType<MonitorClientProvider>();
webSocketService = Mock.ofType<WebSocketService>();
subject = new SerialServiceImpl(
logger.object,
serialClientProvider.object,
webSocketService.object
);
});
context('when a serial connection is requested', () => {
const sandbox = createSandbox();
beforeEach(() => {
subject.uploadInProgress = false;
sandbox.spy(subject, 'disconnect');
sandbox.spy(subject, 'updateWsConfigParam');
});
afterEach(function () {
sandbox.restore();
});
context('and an upload is in progress', () => {
beforeEach(async () => {
subject.uploadInProgress = true;
});
it('should not change the connection status', async () => {
await subject.connectSerialIfRequired();
expect(subject.disconnect).to.have.callCount(0);
});
});
context('and there is no upload in progress', () => {
beforeEach(async () => {
subject.uploadInProgress = false;
});
context('and there are 0 attached ws clients', () => {
it('should disconnect', async () => {
await subject.connectSerialIfRequired();
expect(subject.disconnect).to.have.been.calledOnce;
});
});
context('and there are > 0 attached ws clients', () => {
beforeEach(() => {
webSocketService
.setup((b) => b.getConnectedClientsNumber())
.returns(() => 1);
});
it('should not call the disconenct', async () => {
await subject.connectSerialIfRequired();
expect(subject.disconnect).to.have.callCount(0);
});
});
});
});
context('when a disconnection is requested', () => {
const sandbox = createSandbox();
beforeEach(() => { });
afterEach(function () {
sandbox.restore();
});
context('and a serialConnection is not set', () => {
it('should return a NOT_CONNECTED status', async () => {
const status = await subject.disconnect();
expect(status).to.be.equal(Status.NOT_CONNECTED);
});
});
context('and a serialConnection is set', async () => {
beforeEach(async () => {
sandbox.spy(subject, 'updateWsConfigParam');
await subject.disconnect();
});
it('should dispose the serialConnection', async () => {
const serialConnectionOpen = await subject.isSerialPortOpen();
expect(serialConnectionOpen).to.be.false;
});
it('should call updateWsConfigParam with disconnected status', async () => {
expect(subject.updateWsConfigParam).to.be.calledWith({
connected: false,
});
});
});
});
context('when a new config is passed in', () => {
const sandbox = createSandbox();
beforeEach(async () => {
subject.uploadInProgress = false;
webSocketService
.setup((b) => b.getConnectedClientsNumber())
.returns(() => 1);
serialClientProvider
.setup((b) => b.client())
.returns(async () => {
return {
streamingOpen: () => {
return {
on: (str: string, cb: any) => { },
write: (chunk: any, cb: any) => {
cb();
},
cancel: () => { },
};
},
} as MonitorServiceClient;
});
sandbox.spy(subject, 'disconnect');
await subject.setSerialConfig({
board: { name: 'test' },
port: { id: 'test|test', address: 'test', addressLabel: 'test', protocol: 'test', protocolLabel: 'test' },
});
});
afterEach(function () {
sandbox.restore();
subject.dispose();
});
it('should disconnect from previous connection', async () => {
expect(subject.disconnect).to.be.called;
});
it('should create the serialConnection', async () => {
const serialConnectionOpen = await subject.isSerialPortOpen();
expect(serialConnectionOpen).to.be.true;
});
});
});