Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v5

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/node.js.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [22.12.0]
node-version: [22.14.0]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- run: npm run dockerInstall
Expand All @@ -19,7 +19,7 @@ jobs:
- run: npm pack
- run: mkdir /home/runner/work/nelson-cloud/nelson-cloud/artifacts
- run: cp /home/runner/work/nelson-cloud/nelson-cloud/nelson-cloud-*.tgz /home/runner/work/nelson-cloud/nelson-cloud/artifacts
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: nelson-cloud-github-action-artifacts
path: artifacts/
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
22.12.0
22.14.0
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 1.4.4 (2025-11-22)

- Update packages dependencies

- unit tests reworked

- Tested with Nelson 1.10.0

- Requires Nodejs 22.14.0

## 1.4.4 (2024-12-14)

- Update packages dependencies
Expand Down
1 change: 0 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ $ git commit
The commit message should describe what changed and why.

1. The first line should:

- contain a short description of the change
- be 50 characters or less
- be entirely in lowercase with the exception of proper nouns, acronyms, and
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Nelson Cloud brings the power of Nelson numerical computation software to your w

Before installation, ensure you have:

- Node.js 22.12.0 or higher
- Node.js 22.14.0 or higher
- Docker installed and running
- A modern web browser

Expand Down
250 changes: 86 additions & 164 deletions lib/nelsonCloud.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,177 +18,99 @@
//=============================================================================
'use strict';
//=============================================================================
const io = require('socket.io-client');
const EventEmitter = require('events');
const {
__test__: { child_on, child_emit },
} = require('./nelsonSocketIO');
//=============================================================================
const defaultTimeout = 4;
const longerTimeout = 8;
class MockSocket extends EventEmitter {
constructor(session = undefined) {
super();
this.session = session;
}
}
//=============================================================================
var socket;
//=============================================================================
jest.setTimeout(30000);
//=============================================================================
beforeEach(function (done) {
setTimeout(() => {
socket = io.connect('http://localhost:9090', {
serveClient: false,
'reconnection delay': 0,
'reopen delay': 0,
'force new connection': true,
});
socket.on('connect', function () {
done();
});
socket.on('disconnect', function () {
done();
describe('child_on', () => {
function createHarness(session) {
const socket = new MockSocket(session);
const child = { send: jest.fn() };
const ios = { emit: jest.fn() };
child_on(socket, child, ios);
return { socket, child, ios };
}

test.each([
['command', 'disp(42);', { msgtype: 'command', data: 'disp(42);' }],
['clc', undefined, { msgtype: 'clc' }],
['stop', undefined, { msgtype: 'stop' }],
['available', undefined, { msgtype: 'available' }],
['unavailable', undefined, { msgtype: 'unavailable' }],
['disconnect', undefined, { msgtype: 'quit' }],
])(
'forwards %s events to the child process',
(eventName, payload, expected) => {
const { socket, child } = createHarness();
if (payload === undefined) {
socket.emit(eventName);
} else {
socket.emit(eventName, payload);
}
expect(child.send).toHaveBeenCalledTimes(1);
expect(child.send).toHaveBeenCalledWith(expected);
},
);

it('emits reply events through io with inferred sender name when missing', () => {
const { socket, ios } = createHarness({ name: 'session-1' });
const payload = { text: 'Hello' };
socket.emit('reply', payload);
expect(ios.emit).toHaveBeenCalledWith('reply', {
text: 'Hello',
from: 'session-1',
});
}, longerTimeout);
});
//=============================================================================
afterEach(function (done) {
setTimeout(() => {
if (socket.connected) {
socket.disconnect();
} else {
}
done();
}, longerTimeout);
});
//=============================================================================
describe('Nelson SocketIO tests', function () {
//=============================================================================
it('should receive connect and send available.', function (done) {
setTimeout(() => {
socket.on('available', function (data) {
done();
});
}, longerTimeout);
expect(payload).toEqual({ text: 'Hello' });
});
//=============================================================================
it('should receive unavailable during computation.', function (done) {
setTimeout(() => {
socket.on('available', function (data) {
socket.emit('command', 'A = ones(1000);');
socket.on('unavailable', function (data) {
done();
});
});
}, defaultTimeout);

it('preserves explicit sender information on reply events', () => {
const { socket, ios } = createHarness({ name: 'session-1' });
const payload = { text: 'Hello', from: 'user-provided' };
socket.emit('reply', payload);
expect(ios.emit).toHaveBeenCalledWith('reply', payload);
});
//=============================================================================
it('should receive available after computation.', function (done) {
setTimeout(() => {
socket.on('available', function (data) {
socket.emit('command', 'A = ones(1000);');
socket.on('unavailable', function () {
socket.on('available', function () {
done();
});
});
});
}, defaultTimeout);

it('broadcasts variables retrieved from nelson', () => {
const { socket, ios } = createHarness();
const variablePayload = { name: 'A', value: 42 };
socket.emit('send_variable', variablePayload);
expect(ios.emit).toHaveBeenCalledWith('send_variable', variablePayload);
});
//=============================================================================
it('should receive prompt', function (done) {
setTimeout(() => {
socket.on('prompt', function (data) {
const expectedResult = '>> ';
expect(data).toEqual(expectedResult);
done();
});
}, defaultTimeout);

it('broadcasts quit instructions to all listeners', () => {
const { socket, ios } = createHarness();
socket.emit('quit');
expect(ios.emit).toHaveBeenCalledWith('quit');
});
//=============================================================================
it('should send an command to nelson and get result (variable exists).', function (done) {
setTimeout(() => {
socket.on('available', function (data) {
socket.emit('command', 'A = 1 + 1;');
socket.on('available', function () {
socket.emit('command', "siogetvariable('A')");
socket.on('send_variable', function (data) {
try {
const expectedResult =
'{"name":"A","exists":true,"type":"double","dims":[1,1],"value":2}';
expect(data).toEqual(expectedResult);
done();
} catch (e) {
done(e);
}
});
});
});
}, defaultTimeout);
});
//=============================================================================
it('should send an command to nelson and get result (variable does not exist).', function (done) {
setTimeout(() => {
socket.on('available', function () {
socket.emit('command', "siogetvariable('A')");
socket.on('send_variable', function (data) {
try {
const expectedResult = '{"name":"A","exists":false,"value":[]}';
expect(data).toEqual(expectedResult);
done();
} catch (e) {
done(e);
}
});
});
}, defaultTimeout);
});
//=============================================================================
it('should send clc command to browser.', function (done) {
setTimeout(() => {
socket.on('available', function () {
socket.emit('command', 'clc');
socket.on('clc', function () {
done();
});
});
}, defaultTimeout);
});
//=============================================================================
it('should stop nelson loop in progress.', function (done) {
setTimeout(() => {
socket.on('available', function () {
socket.emit('command', 'while (true), A = 1, end');
socket.on('unavailable', function () {
socket.emit('stop');
socket.on('available', function () {
done();
});
});
});
}, defaultTimeout);
});
//=============================================================================
it('should return help url to open by web browser.', function (done) {
setTimeout(() => {
socket.on('available', function () {
socket.emit('command', 'doc sin');
socket.on('help', function (data) {
try {
const expectedResult =
'https://nelson-9.gitbook.io/nelson/en/trigonometric_functions/sin.html';
expect(data).toEqual(expectedResult);
done();
} catch (e) {
done(e);
}
});
});
}, defaultTimeout);
});
//=============================================================================
it('should receive quit message at exit.', function (done) {
setTimeout(() => {
socket.on('available', function () {
socket.emit('command', 'exit');
socket.on('quit', function () {
done();
});
});
}, defaultTimeout);
});
//=============================================================================
describe('child_emit', () => {
test.each([
[{ msgtype: 'command_received' }, (msg) => ['command_received']],
[{ msgtype: 'prompt', output: '>> ' }, (msg) => ['prompt', msg.output]],
[{ msgtype: 'reply', output: 'payload' }, (msg) => ['reply', msg]],
[{ msgtype: 'clc' }, (msg) => ['clc']],
[{ msgtype: 'stop' }, (msg) => ['stop']],
[{ msgtype: 'available' }, (msg) => ['available']],
[{ msgtype: 'unavailable' }, (msg) => ['unavailable']],
[{ msgtype: 'quit' }, (msg) => ['quit']],
[
{ msgtype: 'custom', output: 'value' },
(msg) => [msg.msgtype, msg.output],
],
])('emits %p through the provided socket', (message, expectedFactory) => {
const socket = { emit: jest.fn() };
child_emit(socket, message);
expect(socket.emit).toHaveBeenCalledTimes(1);
expect(socket.emit).toHaveBeenCalledWith(...expectedFactory(message));
});
//=============================================================================
});
//=============================================================================
Loading
Loading