From ace9713aa8f355cbc73d0bba4722b7b7ccf9efb8 Mon Sep 17 00:00:00 2001 From: Sandro Weber <webers@in.tum.de> Date: Mon, 1 Feb 2021 23:14:19 +0100 Subject: [PATCH] lots more test coverage --- .../experiment-list-element.css | 26 ++- .../experiment-list-element.js | 24 ++- src/components/user-menu/user-menu.js | 19 +- src/mocks/handlers.js | 20 ++- src/mocks/mock_available-servers.json | 2 +- src/mocks/mock_server-config.json | 14 ++ src/mocks/mock_user.json | 4 + .../experiment-execution-service.test.js | 166 ++++++++++++++++++ .../server-resources-service.test.js | 82 +++++++++ .../execution/experiment-execution-service.js | 70 +++++--- .../execution/server-resources-service.js | 65 ++----- .../experiment-storage-service.test.js | 18 ++ .../storage/experiment-storage-service.js | 6 +- src/services/nrp-analytics-service.js | 4 +- src/services/proxy/nrp-user-service.js | 6 +- 15 files changed, 391 insertions(+), 135 deletions(-) create mode 100644 src/mocks/mock_server-config.json create mode 100644 src/mocks/mock_user.json create mode 100644 src/services/experiments/execution/__tests__/experiment-execution-service.test.js create mode 100644 src/services/experiments/execution/__tests__/server-resources-service.test.js diff --git a/src/components/experiment-list/experiment-list-element.css b/src/components/experiment-list/experiment-list-element.css index 8ce8b3a..f45970b 100644 --- a/src/components/experiment-list/experiment-list-element.css +++ b/src/components/experiment-list/experiment-list-element.css @@ -12,27 +12,15 @@ } .flex-container .selected { - background: linear-gradient( - to right, - rgba(255, 255, 255, 0.8) 0%, - rgba(245, 245, 245, 0.8) 100% - ); + background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(245, 245, 245, 0.8) 100%); } .flex-container.hover-wizard { - background: linear-gradient( - to right, - rgba(255, 255, 255, 0.8) 0%, - rgba(230, 239, 255, 0.8) 100% - ); + background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%); } .flex-container.selected-wizard { - background: linear-gradient( - to right, - rgba(255, 255, 255, 0.8) 0%, - rgba(230, 239, 255, 0.8) 100% - ); + background: linear-gradient( to right, rgba(255, 255, 255, 0.8) 0%, rgba(230, 239, 255, 0.8) 100%); } .flex-container.left-right { @@ -69,12 +57,13 @@ width: 359px; } -.list-entry-left > img { +.list-entry-left>img { width: 100%; object-fit: contain; object-position: top; height: 100%; } + .list-entry-middle { flex: 1; } @@ -124,4 +113,9 @@ .exp-title-sim-info { padding-left: 20px; font-style: italic; +} + +.entity-thumbnail { + max-width: 230px; + max-height: 160px; } \ No newline at end of file diff --git a/src/components/experiment-list/experiment-list-element.js b/src/components/experiment-list/experiment-list-element.js index 907d5af..5dd6543 100644 --- a/src/components/experiment-list/experiment-list-element.js +++ b/src/components/experiment-list/experiment-list-element.js @@ -53,10 +53,8 @@ export default class ExperimentListElement extends React.Component { } getAvailabilityInfo() { - const clusterAvailability = ExperimentServerService.instance.getClusterAvailability(); - let status; - if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) { + if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) { status = 'Available'; } else if (!this.props.availableServers || this.props.availableServers.length === 0) { @@ -66,17 +64,14 @@ export default class ExperimentListElement extends React.Component { status = 'Restricted'; } - let cluster = `Cluster availability: ${clusterAvailability.free} / ${clusterAvailability.total}`; let backends = `Backends: ${this.props.availableServers.length}`; - return `${status}\n${cluster}\n${backends}`; + return `${status}\n${backends}`; } getServerStatusClass() { - const clusterAvailability = ExperimentServerService.instance.getClusterAvailability(); - let status = ''; - if (clusterAvailability && clusterAvailability.free > CLUSTER_THRESHOLDS.AVAILABLE) { + if (this.props.availableServers && this.props.availableServers.length > CLUSTER_THRESHOLDS.AVAILABLE) { status = 'server-status-available'; } else if (!this.props.availableServers || this.props.availableServers.length === 0) { @@ -171,12 +166,13 @@ export default class ExperimentListElement extends React.Component { <div className='btn-group' role='group' > {this.canLaunchExperiment && exp.configuration.experimentFile && exp.configuration.bibiConfSrc ? - <button onClick={() => { - ExperimentExecutionService.instance.startNewExperiment(exp, false); - }} - disabled={this.isLaunchDisabled()} - className='btn btn-default' - title={this.launchButtonTitle} > + <button + onClick={() => { + ExperimentExecutionService.instance.startNewExperiment(exp, false); + }} + disabled={this.isLaunchDisabled()} + className='btn btn-default' + title={this.launchButtonTitle} > <i className='fa fa-plus'></i> Launch </button> : null} diff --git a/src/components/user-menu/user-menu.js b/src/components/user-menu/user-menu.js index 5182fe1..d9cda55 100644 --- a/src/components/user-menu/user-menu.js +++ b/src/components/user-menu/user-menu.js @@ -17,21 +17,18 @@ export default class UserMenu extends React.Component { }; } - componentDidMount() { - this._userRequest = NrpUserService.instance - .getCurrentUser() - .then((currentUser) => { - this._userRequest = null; - this.setState(() => ({ + async componentDidMount() { + NrpUserService.instance.getCurrentUser().then((currentUser) => { + if (!this.cancelGetCurrentUser) { + this.setState({ user: currentUser - })); - }); + }); + } + }); } componentWillUnmount() { - if (this._userRequest) { - this._userRequest.cancel(); - } + this.cancelGetCurrentUser = true; } onClickLogout() { diff --git a/src/mocks/handlers.js b/src/mocks/handlers.js index 3ceb2fb..aeb98a0 100644 --- a/src/mocks/handlers.js +++ b/src/mocks/handlers.js @@ -5,6 +5,9 @@ import endpoints from '../services/proxy/data/endpoints'; import MockExperiments from './mock_experiments.json'; import MockAvailableServers from './mock_available-servers.json'; import MockSimulationResources from './mock_simulation-resources.json'; +import MockServerConfig from './mock_server-config.json'; +import MockUser from './mock_user.json'; +import MockSimulations from './mock_simulations.json'; import ImageAI from '../assets/images/Artificial_Intelligence_2.jpg'; @@ -13,14 +16,10 @@ const experiments = MockExperiments; export const handlers = [ rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.experiments.url}`, (req, res, ctx) => { - return res( - ctx.json(experiments) - ); + return res(ctx.json(experiments)); }), rest.get(`${config.api.proxy.url}${endpoints.proxy.availableServers.url}`, (req, res, ctx) => { - return res( - ctx.json(availableServers) - ); + return res(ctx.json(availableServers)); }), rest.get(`${config.api.proxy.url}${endpoints.proxy.storage.url}/:experimentName/:thumbnailFilename`, (req, res, ctx) => { @@ -35,5 +34,14 @@ export const handlers = [ else { throw new Error('Simulation resource error'); } + }), + rest.get('http://:serverIP/simulation/', (req, res, ctx) => { + return res(ctx.json(MockSimulations[0])); + }), + rest.get(`${config.api.proxy.url}${endpoints.proxy.server.url}/:serverID`, (req, res, ctx) => { + return res(ctx.json(MockServerConfig)); + }), + rest.get(`${config.api.proxy.url}${endpoints.proxy.identity.me.url}`, (req, res, ctx) => { + return res(ctx.json(MockUser)); }) ]; \ No newline at end of file diff --git a/src/mocks/mock_available-servers.json b/src/mocks/mock_available-servers.json index 070ca4f..eb2935a 100644 --- a/src/mocks/mock_available-servers.json +++ b/src/mocks/mock_available-servers.json @@ -25,6 +25,6 @@ "websocket": "ws://1.2.3.4:8080/rosbridge" }, "serverJobLocation": "1.2.3.4", - "id": "1.2.3.4:8080" + "id": "1.2.3.4-port-8080" } ] \ No newline at end of file diff --git a/src/mocks/mock_server-config.json b/src/mocks/mock_server-config.json new file mode 100644 index 0000000..e19865e --- /dev/null +++ b/src/mocks/mock_server-config.json @@ -0,0 +1,14 @@ +{ + "internalIp": "http://localhost:8080", + "gzweb": { + "assets": "http://localhost:8080/assets", + "nrp-services": "http://localhost:8080", + "videoStreaming": "http://localhost:8080/webstream/", + "websocket": "ws://localhost:8080/gzbridge" + }, + "rosbridge": { + "websocket": "ws://localhost:8080/rosbridge" + }, + "serverJobLocation": "local", + "id": "localhost" +} \ No newline at end of file diff --git a/src/mocks/mock_user.json b/src/mocks/mock_user.json new file mode 100644 index 0000000..8225c18 --- /dev/null +++ b/src/mocks/mock_user.json @@ -0,0 +1,4 @@ +{ + "id": "nrpuser", + "displayName": "nrpuser" +} \ No newline at end of file diff --git a/src/services/experiments/execution/__tests__/experiment-execution-service.test.js b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js new file mode 100644 index 0000000..d49c61d --- /dev/null +++ b/src/services/experiments/execution/__tests__/experiment-execution-service.test.js @@ -0,0 +1,166 @@ +/** + * @jest-environment jsdom +*/ +import '@testing-library/jest-dom'; +import 'jest-fetch-mock'; + +import MockExperiments from '../../../../mocks/mock_experiments.json'; +import MockAvailableServers from '../../../../mocks/mock_available-servers.json'; +import MockServerConfig from '../../../../mocks/mock_server-config.json'; +import MockSimulations from '../../../../mocks/mock_simulations.json'; + +import ExperimentExecutionService from '../../../../services/experiments/execution/experiment-execution-service'; +import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service'; +import SimulationService from '../../../../services/experiments/execution/simulation-service'; + +//jest.setTimeout(10000); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('makes sure that invoking the constructor fails with the right message', () => { + expect(() => { + new ExperimentExecutionService(); + }).toThrow(Error); + expect(() => { + new ExperimentExecutionService(); + }).toThrowError(Error('Use ExperimentExecutionService.instance')); +}); + +test('the service instance always refers to the same object', () => { + const instance1 = ExperimentExecutionService.instance; + const instance2 = ExperimentExecutionService.instance; + expect(instance1).toBe(instance2); +}); + +test('should emit an event on starting an experiment', async () => { + jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => { + return Promise.resolve(); + }); + let experiment = MockExperiments[0]; + + let confirmStartingExperiment = (startingExperiment) => { + expect(startingExperiment).toEqual(experiment); + }; + ExperimentExecutionService.instance.addListener( + ExperimentExecutionService.EVENTS.START_EXPERIMENT, + confirmStartingExperiment + ); + await ExperimentExecutionService.instance.startNewExperiment(experiment); + ExperimentExecutionService.instance.removeListener( + ExperimentExecutionService.EVENTS.START_EXPERIMENT, + confirmStartingExperiment + ); +}); + +test('should go through the list of available servers when trying to start an experiment', (done) => { + jest.spyOn(console, 'error').mockImplementation(); + jest.spyOn(ServerResourcesService.instance, 'getServerConfig'); + jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation( + // only the last server in the list will return a successful launch + (expID, isPrivate, numBrainProc, serverID) => { + if (serverID !== MockAvailableServers[MockAvailableServers.length - 1].id) { + return Promise.reject({ + error: { + data: 'test rejection for launch on server ' + serverID + } + }); + } + + return Promise.resolve(); + } + ); + + let experiment = MockExperiments[0]; + ExperimentExecutionService.instance.startNewExperiment(experiment).then(() => { + MockAvailableServers.forEach(server => { + expect(ServerResourcesService.instance.getServerConfig).toHaveBeenCalledWith(server.id); + }); + expect(console.error).toHaveBeenCalled(); + done(); + }); +}); + +test('starting an experiment should abort early if a fatal error occurs', (done) => { + jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation( + () => { + return Promise.reject({ + isFatal: true + }); + } + ); + + let experiment = MockExperiments[0]; + ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => { + expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR); + done(); + }); +}); + +test('starting an experiment should fail if no server is ready', (done) => { + jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation( + () => { + return Promise.reject({}); + } + ); + + let experiment = MockExperiments[0]; + ExperimentExecutionService.instance.startNewExperiment(experiment).catch(error => { + expect(error).toEqual(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT); + done(); + }); +}); + +test('respects settings for specific dev server to launch and single brain process mode', async () => { + jest.spyOn(ExperimentExecutionService.instance, 'launchExperimentOnServer').mockImplementation(() => { + return Promise.resolve(); + }); + + let mockExperiment = { + id: 'test-experiment-id', + devServer: 'test-dev-server-url' + }; + await ExperimentExecutionService.instance.startNewExperiment(mockExperiment, true); + expect(ExperimentExecutionService.instance.launchExperimentOnServer).toHaveBeenCalledWith( + mockExperiment.id, + undefined, + 1, + mockExperiment.devServer, + expect.any(Object), + undefined, + undefined, + undefined, + expect.any(Function) + ); +}); + +test('can launch an experiment given a specific server + configuration', async () => { + jest.spyOn(ExperimentExecutionService.instance, 'httpRequestPOST').mockImplementation(); + jest.spyOn(SimulationService.instance, 'registerForRosStatusInformation').mockImplementation(); + jest.spyOn(SimulationService.instance, 'simulationReady').mockImplementation(() => { + return Promise.resolve(MockSimulations[0]); + }); + jest.spyOn(SimulationService.instance, 'initConfigFiles').mockImplementation(() => { + return Promise.resolve(); + }); + + let experimentID = 'test-experiment-id'; + let privateExperiment = true; + let brainProcesses = 2; + let serverID = 'test-server-id'; + let serverConfiguration = MockServerConfig; + let reservation = {}; + let playbackRecording = {}; + let profiler = {}; + let progressCallback = jest.fn(); + let callParams = [experimentID, privateExperiment, brainProcesses, serverID, serverConfiguration, reservation, + playbackRecording, profiler, progressCallback]; + + let result = await ExperimentExecutionService.instance.launchExperimentOnServer(...callParams); + expect(ExperimentExecutionService.instance.httpRequestPOST) + .toHaveBeenLastCalledWith(serverConfiguration.gzweb['nrp-services'] + '/simulation', expect.any(String)); + expect(progressCallback).toHaveBeenCalled(); + expect(result).toBe('esv-private/experiment-view/' + serverID + '/' + experimentID + '/' + + privateExperiment + '/' + MockSimulations[0].simulationID); +}); diff --git a/src/services/experiments/execution/__tests__/server-resources-service.test.js b/src/services/experiments/execution/__tests__/server-resources-service.test.js new file mode 100644 index 0000000..2345da7 --- /dev/null +++ b/src/services/experiments/execution/__tests__/server-resources-service.test.js @@ -0,0 +1,82 @@ +/** + * @jest-environment jsdom +*/ +import '@testing-library/jest-dom'; +import 'jest-fetch-mock'; + +import MockServerconfig from '../../../../mocks/mock_server-config.json'; + +import ServerResourcesService from '../../../../services/experiments/execution/server-resources-service'; +import ErrorHandlerService from '../../../error-handler-service'; + +jest.setTimeout(10000); + +let onWindowBeforeUnloadCb = undefined; +beforeEach(() => { + jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => { + if (event === 'beforeunload') { + onWindowBeforeUnloadCb = cb; + } + }); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +test('makes sure that invoking the constructor fails with the right message', () => { + expect(() => { + new ServerResourcesService(); + }).toThrow(Error); + expect(() => { + new ServerResourcesService(); + }).toThrowError(Error('Use ServerResourcesService.instance')); +}); + +test('the service instance always refers to the same object', () => { + const instance1 = ServerResourcesService.instance; + const instance2 = ServerResourcesService.instance; + expect(instance1).toBe(instance2); +}); + +test('does automatic poll updates for server availability', (done) => { + jest.spyOn(ServerResourcesService.instance, 'getServerAvailability'); + + // check that getExperiments is periodically called after poll interval + let numCallsServerAvailabilityT0 = ServerResourcesService.instance.getServerAvailability.mock.calls.length; + setTimeout(() => { + let numCallsServerAvailabilityT1 = ServerResourcesService.instance.getServerAvailability.mock.calls.length; + expect(numCallsServerAvailabilityT1 > numCallsServerAvailabilityT0).toBe(true); + + // stop updates and check that no more calls occur after poll interval + ServerResourcesService.instance.stopUpdates(); + setTimeout(() => { + let numCallsServerAvailabilityT2 = ServerResourcesService.instance.getServerAvailability.mock.calls.length; + expect(numCallsServerAvailabilityT2 === numCallsServerAvailabilityT1).toBe(true); + done(); + }, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY); + }, ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY); +}); + +test('can get a server config', async () => { + // regular call with proper json + let config = await ServerResourcesService.instance.getServerConfig('test-server-id'); + expect(config).toEqual(MockServerconfig); + + // rejected promise on GET + jest.spyOn(ServerResourcesService.instance, 'httpRequestGET').mockImplementation(() => { + return Promise.reject(); + }); + jest.spyOn(ErrorHandlerService.instance, 'displayServerHTTPError').mockImplementation(); + config = await ServerResourcesService.instance.getServerConfig('test-server-id'); + expect(ErrorHandlerService.instance.displayServerHTTPError).toHaveBeenCalled(); +}); + +test('should stop polling updates when window is unloaded', async () => { + let service = ServerResourcesService.instance; + expect(onWindowBeforeUnloadCb).toBeDefined(); + + jest.spyOn(service, 'stopUpdates'); + onWindowBeforeUnloadCb({}); + expect(service.stopUpdates).toHaveBeenCalled(); +}); diff --git a/src/services/experiments/execution/experiment-execution-service.js b/src/services/experiments/execution/experiment-execution-service.js index 2ec73f5..193982e 100644 --- a/src/services/experiments/execution/experiment-execution-service.js +++ b/src/services/experiments/execution/experiment-execution-service.js @@ -1,6 +1,6 @@ import _ from 'lodash'; -import NrpAnalyticsService from '../../nrp-analytics-service.js'; +//import NrpAnalyticsService from '../../nrp-analytics-service.js'; import ServerResourcesService from './server-resources-service.js'; import SimulationService from './simulation-service.js'; import { HttpService } from '../../http-service.js'; @@ -39,22 +39,23 @@ class ExperimentExecutionService extends HttpService { * @param {object} playbackRecording - a recording of a previous execution * @param {*} profiler - a profiler option */ - startNewExperiment( + async startNewExperiment( experiment, launchSingleMode, reservation, playbackRecording, profiler ) { - NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' }); - NrpAnalyticsService.instance.tickDurationEvent('Server-initialization'); + //TODO: implement NrpAnalyticsService functionality + //NrpAnalyticsService.instance.eventTrack('Start', { category: 'Experiment' }); + //NrpAnalyticsService.instance.tickDurationEvent('Server-initialization'); ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, experiment); let fatalErrorOccurred = false; let serversToTry = experiment.devServer ? [experiment.devServer] - : ServerResourcesService.instance.getServerAvailability(true).map(s => s.id); + : (await ServerResourcesService.instance.getServerAvailability(true)).map(s => s.id); let brainProcesses = launchSingleMode ? 1 : experiment.configuration.brainProcesses; @@ -64,20 +65,23 @@ class ExperimentExecutionService extends HttpService { }; let launchOnNextServer = async () => { - let nextServer = serversToTry.splice(0, 1); - if (fatalErrorOccurred || !nextServer.length) { - //no more servers to retry, we have failed to start experiment - return Promise.reject(fatalErrorOccurred); + if (!serversToTry.length) { + //TODO: GUI feedback + return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_NO_SERVERS_LEFT); + } + if (fatalErrorOccurred) { + //TODO: GUI feedback + return Promise.reject(ExperimentExecutionService.ERRORS.LAUNCH_FATAL_ERROR); } - let server = nextServer[0]; - let serverConfig = await ServerResourcesService.instance.getServerConfig(server); + let serverID = serversToTry.splice(0, 1)[0]; + let serverConfig = await ServerResourcesService.instance.getServerConfig(serverID); return await this.launchExperimentOnServer( experiment.id, experiment.private, brainProcesses, - server, + serverID, serverConfig, reservation, playbackRecording, @@ -85,6 +89,7 @@ class ExperimentExecutionService extends HttpService { progressCallback ).catch((failure) => { if (failure.error && failure.error.data) { + //TODO: proper ErrorHandlerService callback console.error('Failed to start simulation: ' + JSON.stringify(failure.error.data)); } fatalErrorOccurred = fatalErrorOccurred || failure.isFatal; @@ -101,7 +106,7 @@ class ExperimentExecutionService extends HttpService { * @param {string} experimentID - ID of the experiment to launch * @param {boolean} privateExperiment - whether the experiment is private or not * @param {number} brainProcesses - number of brain processes to start with - * @param {string} server - server ID + * @param {string} serverID - server ID * @param {object} serverConfiguration - configuration of server * @param {object} reservation - server reservation * @param {object} playbackRecording - recording @@ -112,7 +117,7 @@ class ExperimentExecutionService extends HttpService { experimentID, privateExperiment, brainProcesses, - server, + serverID, serverConfiguration, reservation, playbackRecording, @@ -158,7 +163,7 @@ class ExperimentExecutionService extends HttpService { .then((simulation) => { SimulationService.instance.initConfigFiles(serverURL, simulation.simulationID) .then(() => { - let simulationURL = 'esv-private/experiment-view/' + server + '/' + experimentID + '/' + + let simulationURL = 'esv-private/experiment-view/' + serverID + '/' + experimentID + '/' + privateExperiment + '/' + simulation.simulationID; resolve(simulationURL); ExperimentExecutionService.instance.emit(ExperimentExecutionService.EVENTS.START_EXPERIMENT, undefined); @@ -205,18 +210,34 @@ class ExperimentExecutionService extends HttpService { if (!data || !data.state) { return Promise.reject(); } - switch (data.state) { - case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED + + // CREATED --(initialize)--> PAUSED --(stop)--> STOPPED + if (data.state === EXPERIMENT_STATE.CREATED) { return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then( _.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED) ); - case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED - case EXPERIMENT_STATE.PAUSED: //PAUSED --(stop)--> STOPPED - case EXPERIMENT_STATE.HALTED: //HALTED --(stop)--> FAILED + } + // STARTED/PAUSED/HALTED --(stop)--> STOPPED + else if (data.state === EXPERIMENT_STATE.STARTED || + data.state === EXPERIMENT_STATE.PAUSED || + data.state === EXPERIMENT_STATE.HALTED) { return updateSimulationState(EXPERIMENT_STATE.STOPPED); - default: - return Promise.reject(); } + + return Promise.reject(); + + /*switch (data.state) { + case EXPERIMENT_STATE.CREATED: //CREATED --(initialize)--> PAUSED --(stop)--> STOPPED + return updateSimulationState(EXPERIMENT_STATE.INITIALIZED).then( + _.partial(updateSimulationState, EXPERIMENT_STATE.STOPPED) + ); + case EXPERIMENT_STATE.STARTED: //STARTED --(stop)--> STOPPED + case EXPERIMENT_STATE.PAUSED: //PAUSED --(stop)--> STOPPED + case EXPERIMENT_STATE.HALTED: //HALTED --(stop)--> FAILED + return updateSimulationState(EXPERIMENT_STATE.STOPPED); + default: + return Promise.reject(); + }*/ }); /*eslint-enable camelcase*/ }) @@ -231,4 +252,9 @@ ExperimentExecutionService.EVENTS = Object.freeze({ STOP_EXPERIMENT: 'STOP_EXPERIMENT' }); +ExperimentExecutionService.ERRORS = Object.freeze({ + LAUNCH_FATAL_ERROR: 'failed to launch experiment, encountered a fatal error', + LAUNCH_NO_SERVERS_LEFT: 'failed to launch experiment, no available server could successfully start it' +}); + export default ExperimentExecutionService; diff --git a/src/services/experiments/execution/server-resources-service.js b/src/services/experiments/execution/server-resources-service.js index 57c2abf..708a46b 100644 --- a/src/services/experiments/execution/server-resources-service.js +++ b/src/services/experiments/execution/server-resources-service.js @@ -1,23 +1,14 @@ -import _ from 'lodash'; -import { Subject, timer } from 'rxjs'; -import { switchMap, filter, map, multicast } from 'rxjs/operators'; - import ErrorHandlerService from '../../error-handler-service.js'; import { HttpService } from '../../http-service.js'; import endpoints from '../../proxy/data/endpoints.json'; import config from '../../../config.json'; const proxyServerURL = `${config.api.proxy.url}${endpoints.proxy.server.url}`; -const slurmMonitorURL = `${config.api.slurmmonitor.url}/api/v1/partitions/interactive`; const availableServersURL = `${config.api.proxy.url}${endpoints.proxy.availableServers.url}`; let _instance = null; const SINGLETON_ENFORCER = Symbol(); -const INTERVAL_POLL_SLURM_MONITOR = 5000; -const INTERVAL_POLL_SERVER_AVAILABILITY = 3000; -let clusterAvailability = { free: 'N/A', total: 'N/A' }; - /** * Service handling server resources for simulating experiments. */ @@ -31,9 +22,10 @@ class ServerResourcesService extends HttpService { this.availableServers = []; this.startUpdates(); - window.onbeforeunload = () => { + window.addEventListener('beforeunload', (event) => { this.stopUpdates(); - }; + event.returnValue = ''; + }); } static get instance() { @@ -48,17 +40,11 @@ class ServerResourcesService extends HttpService { * Start polling updates. */ startUpdates() { - this.clusterAvailabilityObservable = this._createSlurmMonitorObservable(); - this.clusterAvailabilitySubscription = this.clusterAvailabilityObservable.subscribe( - availability => (clusterAvailability = availability) - ); - - this.getServerAvailability(true); this.intervalGetServerAvailability = setInterval( () => { this.getServerAvailability(true); }, - INTERVAL_POLL_SERVER_AVAILABILITY + ServerResourcesService.CONSTANTS.INTERVAL_POLL_SERVER_AVAILABILITY ); } @@ -66,30 +52,17 @@ class ServerResourcesService extends HttpService { * Stop polling updates. */ stopUpdates() { - this.clusterAvailabilitySubscription && this.clusterAvailabilitySubscription.unsubscribe(); this.intervalGetServerAvailability && clearInterval(this.intervalGetServerAvailability); } - /** - * Get available cluster server info. - * @returns {object} cluster availability info - */ - getClusterAvailability() { - return clusterAvailability; - } - /** * Return a list of available servers for starting simulations. * @param {boolean} forceUpdate force an update * @returns {Array} A list of available servers. */ - getServerAvailability(forceUpdate = false) { + async getServerAvailability(forceUpdate = false) { if (!this.availableServers || forceUpdate) { - let update = async () => { - let response = await this.httpRequestGET(availableServersURL); - this.availableServers = await response.json(); - }; - update(); + this.availableServers = await (await this.httpRequestGET(availableServersURL)).json(); this.emit(ServerResourcesService.EVENTS.UPDATE_SERVER_AVAILABILITY, this.availableServers); } @@ -108,32 +81,14 @@ class ServerResourcesService extends HttpService { }) .catch(ErrorHandlerService.instance.displayServerHTTPError); } - - _createSlurmMonitorObservable() { - return timer(0, INTERVAL_POLL_SLURM_MONITOR) - .pipe(switchMap(() => { - try { - return this.httpRequestGET(slurmMonitorURL); - } - catch (error) { - _.once(error => { - if (error.status === -1) { - error = Object.assign(error, { - data: 'Could not probe vizualization cluster' - }); - } - ErrorHandlerService.instance.displayServerHTTPError(error); - }); - } - })) - .pipe(filter(e => e)) - .pipe(map(({ free, nodes }) => ({ free, total: nodes[3] }))) - .pipe(multicast(new Subject())).refCount(); - } } ServerResourcesService.EVENTS = Object.freeze({ UPDATE_SERVER_AVAILABILITY: 'UPDATE_SERVER_AVAILABILITY' }); +ServerResourcesService.CONSTANTS = Object.freeze({ + INTERVAL_POLL_SERVER_AVAILABILITY: 3000 +}); + export default ServerResourcesService; diff --git a/src/services/experiments/storage/__tests__/experiment-storage-service.test.js b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js index 2176a82..c87014c 100644 --- a/src/services/experiments/storage/__tests__/experiment-storage-service.test.js +++ b/src/services/experiments/storage/__tests__/experiment-storage-service.test.js @@ -15,6 +15,15 @@ const experimentsUrl = `${config.api.proxy.url}${proxyEndpoint.storage.experimen jest.setTimeout(3 * ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS); +let onWindowBeforeUnloadCb = undefined; +beforeEach(() => { + jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => { + if (event === 'beforeunload') { + onWindowBeforeUnloadCb = cb; + } + }); +}); + afterEach(() => { jest.restoreAllMocks(); }); @@ -82,6 +91,15 @@ test('does automatic poll updates of experiment list which can be stopped', (don }, ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS); }); +test('should stop polling updates when window is unloaded', async () => { + let service = ExperimentStorageService.instance; + expect(onWindowBeforeUnloadCb).toBeDefined(); + + jest.spyOn(service, 'stopUpdates'); + onWindowBeforeUnloadCb({}); + expect(service.stopUpdates).toHaveBeenCalled(); +}); + test('gets a thumbnail image for experiments', async () => { let experiment = MockExperiments[0]; const imageBlob = await ExperimentStorageService.instance.getThumbnail(experiment.name, diff --git a/src/services/experiments/storage/experiment-storage-service.js b/src/services/experiments/storage/experiment-storage-service.js index c3daae8..08199b9 100644 --- a/src/services/experiments/storage/experiment-storage-service.js +++ b/src/services/experiments/storage/experiment-storage-service.js @@ -7,8 +7,6 @@ const storageExperimentsURL = `${config.api.proxy.url}${endpoints.proxy.storage. let _instance = null; const SINGLETON_ENFORCER = Symbol(); -const INTERVAL_POLL_EXPERIMENTS = 3000; - /** * Service that fetches the template experiments list from the proxy given * that the user has authenticated successfully. @@ -44,7 +42,7 @@ class ExperimentStorageService extends HttpService { () => { this.getExperiments(true); }, - INTERVAL_POLL_EXPERIMENTS + ExperimentStorageService.CONSTANTS.INTERVAL_POLL_EXPERIMENTS ); } @@ -126,7 +124,7 @@ ExperimentStorageService.EVENTS = Object.freeze({ }); ExperimentStorageService.CONSTANTS = Object.freeze({ - INTERVAL_POLL_EXPERIMENTS: INTERVAL_POLL_EXPERIMENTS + INTERVAL_POLL_EXPERIMENTS: 3000 }); export default ExperimentStorageService; diff --git a/src/services/nrp-analytics-service.js b/src/services/nrp-analytics-service.js index f00751e..2b3c3af 100644 --- a/src/services/nrp-analytics-service.js +++ b/src/services/nrp-analytics-service.js @@ -30,10 +30,10 @@ class NrpAnalyticsService { options.value = _.toInteger(options.value); } return NrpUserService.instance.getCurrentUser().then((user) => { - var extendedOptions = _.extend(options, { + /*var extendedOptions = _.extend(options, { label: user.displayName }); - //$analytics.eventTrack(actionName, extendedOptions); + $analytics.eventTrack(actionName, extendedOptions);*/ console.error('implement $analytics.eventTrack(actionName, extendedOptions)'); }); } diff --git a/src/services/proxy/nrp-user-service.js b/src/services/proxy/nrp-user-service.js index e528d34..a81f2e7 100644 --- a/src/services/proxy/nrp-user-service.js +++ b/src/services/proxy/nrp-user-service.js @@ -40,8 +40,7 @@ class NrpUserService extends HttpService { * @returns {promise} Request for the user */ async getUser(userID) { - let response = await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID); - return response.json(); + return await (await this.httpRequestGET(this.IDENTITY_BASE_URL + '/' + userID)).json(); } /** @@ -62,8 +61,7 @@ class NrpUserService extends HttpService { */ async getCurrentUser() { if (!this.currentUser) { - let response = await this.httpRequestGET(this.IDENTITY_ME_URL); - this.currentUser = response.json(); + this.currentUser = await (await this.httpRequestGET(this.IDENTITY_ME_URL)).json(); } return this.currentUser; -- GitLab