mirror of
https://github.com/arduino/arduino-ide.git
synced 2025-11-09 10:28:32 +00:00
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:
committed by
GitHub
parent
4c55807392
commit
df8658eff9
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user