Skip to content

Commit 94a1728

Browse files
authored
Add tests to improve coverage (#1363)
### Summary Enhanced unit test coverage for core library modules and simplified CI configuration. ### Details * **Test Coverage Improvements**: * Added comprehensive unit tests for `utils.js`, `time_source.js`, `native_loader.js`, `timer.js`, `message_validation.js`, `message_serialization.js`, `client.js`, `event_handler.js`, and `logging.js`. * Achieved >80% code coverage for these modules. * Fixed compatibility issues for ROS 2 Humble (added conditional checks and mocks `stubOptional` for newer API features like `setOnResetCallback`). * **CI Configuration**: * Updated `.github/workflows/linux-x64-build-and-test.yml` to remove the redundant `finish` job and `parallel` Coveralls upload, streamlining the process for single-job coverage reporting. Fix: #1362
1 parent e01fbf7 commit 94a1728

12 files changed

+1763
-18
lines changed

.github/workflows/linux-x64-build-and-test.yml

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,8 @@ jobs:
7676
npm run test-idl
7777
npm run clean
7878
79-
- name: Coveralls Parallel
79+
- name: Coveralls
8080
if: ${{ matrix.ros_distribution == 'rolling' && matrix['node-version'] == '24.X' }}
8181
uses: coverallsapp/github-action@v2
8282
with:
8383
github-token: ${{ secrets.GITHUB_TOKEN }}
84-
flag-name: run-${{ inputs.trigger_type }}-${{ matrix.node-version }}-${{ matrix.architecture }}
85-
parallel: true
86-
87-
finish:
88-
needs: build
89-
if: always()
90-
runs-on: ubuntu-latest
91-
steps:
92-
- name: Coveralls Finished
93-
uses: coverallsapp/github-action@v2
94-
with:
95-
github-token: ${{ secrets.GITHUB_TOKEN }}
96-
parallel-finished: true

lib/native_loader.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,14 @@ function loadNativeAddon() {
175175
}
176176
}
177177

178-
module.exports = loadNativeAddon();
178+
const addon = loadNativeAddon();
179+
180+
// Export internal functions for testing purposes
181+
if (process.env.NODE_ENV === 'test') {
182+
addon.TestHelpers = {
183+
customFallbackLoader,
184+
loadNativeAddon,
185+
};
186+
}
187+
188+
module.exports = addon;

lib/time_source.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class TimeSource {
102102
* @return {undefined}
103103
*/
104104
attachNode(node) {
105-
if ((!node) instanceof rclnodejs.ShadowNode) {
105+
if (!(node instanceof rclnodejs.ShadowNode)) {
106106
throw new TypeValidationError('node', node, 'Node', {
107107
entityType: 'time source',
108108
});

test/test-client.js

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
'use strict';
2+
3+
const assert = require('assert');
4+
const sinon = require('sinon');
5+
const rclnodejsBinding = require('../lib/native_loader.js');
6+
const Client = require('../lib/client.js');
7+
const DistroUtils = require('../lib/distro.js');
8+
9+
describe('Client coverage testing', function () {
10+
let sandbox;
11+
let mockHandle = { _handle: 'mock-handle' };
12+
let mockNodeHandle = { _handle: 'mock-node-handle' };
13+
14+
const MockTypeClass = {
15+
type: () => ({
16+
interfaceName: 'AddTwoInts',
17+
pkgName: 'example_interfaces',
18+
}),
19+
Request: class MockRequest {
20+
constructor(data) {
21+
Object.assign(this, data);
22+
}
23+
serialize() {
24+
return new Uint8Array([0]);
25+
}
26+
},
27+
Response: class MockResponse {
28+
constructor(data) {
29+
Object.assign(this, data);
30+
}
31+
toPlainObject() {
32+
return { sum: 3 };
33+
}
34+
},
35+
};
36+
37+
beforeEach(function () {
38+
sandbox = sinon.createSandbox();
39+
40+
// Stub native bindings
41+
sandbox.stub(rclnodejsBinding, 'sendRequest').returns(12345); // Sequence number
42+
sandbox.stub(rclnodejsBinding, 'serviceServerIsAvailable').returns(true);
43+
sandbox
44+
.stub(rclnodejsBinding, 'getClientServiceName')
45+
.returns('test_service');
46+
sandbox.stub(rclnodejsBinding, 'createClient').returns(mockHandle);
47+
sandbox.stub(rclnodejsBinding, 'getNodeLoggerName').returns('node_logger');
48+
49+
if (rclnodejsBinding.configureClientIntrospection) {
50+
sandbox
51+
.stub(rclnodejsBinding, 'configureClientIntrospection')
52+
.returns(undefined);
53+
}
54+
});
55+
56+
afterEach(function () {
57+
sandbox.restore();
58+
});
59+
60+
it('constructor sets properties correctly', function () {
61+
const client = new Client(
62+
mockHandle,
63+
mockNodeHandle,
64+
'test_service',
65+
MockTypeClass,
66+
{
67+
validateRequests: true,
68+
validationOptions: { strict: false },
69+
}
70+
);
71+
72+
assert.strictEqual(client.willValidateRequest, true);
73+
assert.strictEqual(client._serviceName, 'test_service');
74+
});
75+
76+
it('createClient static method', function () {
77+
const client = Client.createClient(
78+
mockNodeHandle,
79+
'service',
80+
MockTypeClass,
81+
{ qos: {} }
82+
);
83+
assert.ok(client instanceof Client);
84+
assert.ok(rclnodejsBinding.createClient.called);
85+
});
86+
87+
it('willValidateRequest setter', function () {
88+
const client = new Client(
89+
mockHandle,
90+
mockNodeHandle,
91+
's',
92+
MockTypeClass,
93+
{}
94+
);
95+
client.willValidateRequest = true;
96+
assert.strictEqual(client.willValidateRequest, true);
97+
});
98+
99+
it('setValidation updates options', function () {
100+
const client = new Client(
101+
mockHandle,
102+
mockNodeHandle,
103+
's',
104+
MockTypeClass,
105+
{}
106+
);
107+
client.setValidation({ strict: false });
108+
assert.strictEqual(client._validationOptions.strict, false);
109+
});
110+
111+
it('sendRequest throws on invalid callback', function () {
112+
const client = new Client(
113+
mockHandle,
114+
mockNodeHandle,
115+
's',
116+
MockTypeClass,
117+
{}
118+
);
119+
assert.throws(() => {
120+
client.sendRequest({}, null);
121+
}, /callback/);
122+
});
123+
124+
it('sendRequest sends and stores callback', function () {
125+
const client = new Client(
126+
mockHandle,
127+
mockNodeHandle,
128+
's',
129+
MockTypeClass,
130+
{}
131+
);
132+
const spyCallback = sinon.spy();
133+
134+
client.sendRequest({ a: 1 }, spyCallback);
135+
136+
assert.ok(rclnodejsBinding.sendRequest.called);
137+
assert.ok(client._sequenceNumberToCallbackMap.has(12345));
138+
});
139+
140+
it('processResponse calls callback', function () {
141+
const client = new Client(
142+
mockHandle,
143+
mockNodeHandle,
144+
's',
145+
MockTypeClass,
146+
{}
147+
);
148+
const spyCallback = sinon.spy();
149+
150+
// Simulate sending
151+
client.sendRequest({ a: 1 }, spyCallback);
152+
153+
// Simulate receiving
154+
const mockResponseWrapper = new MockTypeClass.Response();
155+
client.processResponse(12345, mockResponseWrapper);
156+
157+
assert.ok(spyCallback.calledOnce);
158+
assert.deepStrictEqual(spyCallback.firstCall.args[0], { sum: 3 });
159+
assert.strictEqual(client._sequenceNumberToCallbackMap.has(12345), false);
160+
});
161+
162+
it('processResponse fails gracefully for unknown sequence', function () {
163+
const client = new Client(
164+
mockHandle,
165+
mockNodeHandle,
166+
's',
167+
MockTypeClass,
168+
{}
169+
);
170+
// Should verify it logs debug info, but hard to capture 'debug' module.
171+
// We assume it runs without error.
172+
client.processResponse(99999, {});
173+
});
174+
175+
it('sendRequestAsync resolves on success', async function () {
176+
const client = new Client(
177+
mockHandle,
178+
mockNodeHandle,
179+
's',
180+
MockTypeClass,
181+
{}
182+
);
183+
const promise = client.sendRequestAsync({ a: 1 });
184+
185+
// Simulate response arrival manually since we don't have the event loop of rclnodejs running
186+
// We need to trigger the callback registered in the map
187+
const callback = client._sequenceNumberToCallbackMap.get(12345);
188+
assert.ok(callback);
189+
190+
callback(new MockTypeClass.Response());
191+
192+
const result = await promise;
193+
assert.deepStrictEqual(result, new MockTypeClass.Response());
194+
});
195+
196+
it('sendRequestAsync handles timeout', async function () {
197+
const client = new Client(
198+
mockHandle,
199+
mockNodeHandle,
200+
's',
201+
MockTypeClass,
202+
{}
203+
);
204+
205+
// We need to avoid waiting actual time.
206+
// mocking AbortSignal.timeout is hard, usually implemented by node.
207+
// We'll use a short timeout.
208+
209+
try {
210+
await client.sendRequestAsync({ a: 1 }, { timeout: 10 });
211+
assert.fail('Should have timed out');
212+
} catch (err) {
213+
assert.ok(
214+
err.name === 'TimeoutError' ||
215+
err.code === 'ETIMEDOUT' ||
216+
err.message.includes('Timeout'),
217+
'Error was: ' + err
218+
);
219+
}
220+
});
221+
222+
it('sendRequestAsync handles manual abort', async function () {
223+
if (typeof AbortController === 'undefined') this.skip();
224+
225+
const client = new Client(
226+
mockHandle,
227+
mockNodeHandle,
228+
's',
229+
MockTypeClass,
230+
{}
231+
);
232+
const ac = new AbortController();
233+
234+
const promise = client.sendRequestAsync({ a: 1 }, { signal: ac.signal });
235+
ac.abort();
236+
237+
try {
238+
await promise;
239+
assert.fail('Should have aborted');
240+
} catch (err) {
241+
// Name might vary depending on polyfill or native
242+
assert.ok(err.name === 'AbortError', 'Error name ' + err.name);
243+
}
244+
});
245+
246+
it('waitForService waits and returns true', async function () {
247+
const client = new Client(
248+
mockHandle,
249+
mockNodeHandle,
250+
's',
251+
MockTypeClass,
252+
{}
253+
);
254+
rclnodejsBinding.serviceServerIsAvailable.returns(true);
255+
256+
const available = await client.waitForService(100);
257+
assert.strictEqual(available, true);
258+
});
259+
260+
it('waitForService handles timeout', async function () {
261+
const client = new Client(
262+
mockHandle,
263+
mockNodeHandle,
264+
's',
265+
MockTypeClass,
266+
{}
267+
);
268+
rclnodejsBinding.serviceServerIsAvailable.returns(false);
269+
270+
const start = Date.now();
271+
const available = await client.waitForService(50);
272+
const duration = Date.now() - start;
273+
274+
assert.strictEqual(available, false);
275+
assert.ok(duration >= 40); // Allows some slack
276+
});
277+
278+
it('loggerName getter', function () {
279+
const client = new Client(
280+
mockHandle,
281+
mockNodeHandle,
282+
's',
283+
MockTypeClass,
284+
{}
285+
);
286+
assert.strictEqual(client.loggerName, 'node_logger');
287+
});
288+
289+
it('configureIntrospection warns on old distro', function () {
290+
const client = new Client(
291+
mockHandle,
292+
mockNodeHandle,
293+
's',
294+
MockTypeClass,
295+
{}
296+
);
297+
const clockStub = { handle: 'clock' };
298+
const qos = {};
299+
300+
// Mock DistroUtils.getDistroId to return humble (enum numeric) vs old
301+
// We stub getDistroId on the class
302+
sandbox.stub(DistroUtils, 'getDistroId').callsFake((name) => {
303+
if (name === 'humble') return 2;
304+
return 1; // Current distro is 1 (older than 2)
305+
});
306+
307+
const consoleSpy = sandbox.spy(console, 'warn');
308+
309+
client.configureIntrospection(clockStub, qos, 'on');
310+
311+
assert.ok(consoleSpy.calledWithMatch(/not supported/));
312+
if (rclnodejsBinding.configureClientIntrospection) {
313+
assert.strictEqual(
314+
rclnodejsBinding.configureClientIntrospection.called,
315+
false
316+
);
317+
}
318+
});
319+
320+
it('configureIntrospection calls binding on new distro', function () {
321+
if (!rclnodejsBinding.configureClientIntrospection) {
322+
this.skip();
323+
}
324+
325+
const client = new Client(
326+
mockHandle,
327+
mockNodeHandle,
328+
's',
329+
MockTypeClass,
330+
{}
331+
);
332+
const clockStub = { handle: 'clock' };
333+
const qos = {};
334+
335+
sandbox.stub(DistroUtils, 'getDistroId').callsFake((name) => {
336+
if (name === 'humble') return 1;
337+
return 2; // Current distro is 2 (newer than 1)
338+
});
339+
340+
client.configureIntrospection(clockStub, qos, 'on');
341+
342+
assert.strictEqual(
343+
rclnodejsBinding.configureClientIntrospection.called,
344+
true
345+
);
346+
});
347+
});

0 commit comments

Comments
 (0)