Skip to content

Commit ba6f9a5

Browse files
authored
Fix action server goal expiration and correct broken test assertions (#1420)
## fix: action server goal expiration never removed expired goals ### Summary Fixed two bugs in the action server goal expiration mechanism that caused expired goals to never be removed from memory, and a crash when they were. Also fixed 9 tautological test assertions that masked both bugs, and added deserialized goal info validation to the multi-goal expiration test. ### Bug 1 — Wrong buffer read in `_executeExpiredGoals` (`lib/action/server.js`) `_executeExpiredGoals()` deserialized goal info from `result.data[i].refObject`, which are JavaScript wrapper objects each with their own empty `_refObject` buffer. The native `rcl_action_expire_goals()` writes into `result._refArray.buffer`, a completely separate memory block. The wrappers never see the native data, so the deserialized UUID was always all-zeros and `_goalHandles.delete(uuid)` never matched — goals were silently never expired. **Fix:** Changed `goal.refObject` → `result._refArray[i]` to read directly from the native-written buffer. ### Bug 2 — Crash on zero-capacity buffer (`lib/node.js`) After Bug 1 is fixed, expired goals are actually removed from `_goalHandles`. On the next spin cycle, `isGoalExpired` may still fire with `_goalHandles.size == 0`. Passing a zero-capacity buffer to `rcl_action_expire_goals` violates an RCL precondition and crashes: > `expired_goals, expired_goals_capacity, and num_expired inconsistent` **Fix:** Added `if (numGoals > 0)` guard to skip the native call when there are no goal handles. ### Bug 3 — Tautological test assertions (`test/test-action-server.js`) 9 instances of `assert.ok(value, expected)` where `assert.ok`'s second argument is a failure message, not a comparison. These always passed regardless of actual values. **Fix:** Changed all 9 to `assert.strictEqual(value, expected)`. ### Enhanced test validation (`test/test-action-server.js`) Added goal info validation to "Test expire goals multi" that wraps `_executeExpiredGoals` to intercept and verify the deserialized data from the native buffer: - **Count:** exactly 3 expired goals reported - **Non-zero UUIDs:** each deserialized UUID is not all-zeros (catches the original bug) - **Uniqueness:** all 3 UUIDs are distinct (no buffer aliasing) - **UUID match:** expired UUIDs are byte-identical to the accepted goal UUIDs - **Non-zero stamps:** each `GoalInfo.stamp` has a non-zero acceptance time (validates full struct deserialization) Also increased expiration delays from 1000ms to 3000ms for `resultTimeout: 1` (1 second) to provide adequate margin. ### Files changed | File | Changes | |------|---------| | `lib/action/server.js` | Read from `result._refArray[i]` instead of `result.data[i].refObject` | | `lib/node.js` | Guard `actionExpireGoals` call with `if (numGoals > 0)` | | `test/test-action-server.js` | Fix 9 `assert.ok` → `assert.strictEqual`; add deserialized goal info validation; increase expire delays | Fix: #1419
1 parent 7b89b67 commit ba6f9a5

3 files changed

Lines changed: 70 additions & 24 deletions

File tree

lib/action/server.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,10 +437,8 @@ class ActionServer extends Entity {
437437

438438
_executeExpiredGoals(result, count) {
439439
for (let i = 0; i < count; i++) {
440-
const goal = result.data[i];
441-
442440
const goalInfo = new ActionInterfaces.GoalInfo();
443-
goalInfo.deserialize(goal.refObject);
441+
goalInfo.deserialize(result._refArray[i]);
444442

445443
let uuid = ActionUuid.fromBytes(goalInfo.goal_id.uuid).toString();
446444
this._goalHandles.delete(uuid);

lib/node.js

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -425,17 +425,20 @@ class Node extends rclnodejs.ShadowNode {
425425
}
426426

427427
if (properties.isGoalExpired) {
428-
let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
429-
let message = new GoalInfoArray(actionServer._goalHandles.size);
430-
let count = rclnodejs.actionExpireGoals(
431-
actionServer.handle,
432-
actionServer._goalHandles.size,
433-
message._refArray.buffer
434-
);
435-
if (count > 0) {
436-
actionServer.processGoalExpired(message, count);
428+
let numGoals = actionServer._goalHandles.size;
429+
if (numGoals > 0) {
430+
let GoalInfoArray = ActionInterfaces.GoalInfo.ArrayType;
431+
let message = new GoalInfoArray(numGoals);
432+
let count = rclnodejs.actionExpireGoals(
433+
actionServer.handle,
434+
numGoals,
435+
message._refArray.buffer
436+
);
437+
if (count > 0) {
438+
actionServer.processGoalExpired(message, count);
439+
}
440+
GoalInfoArray.freeArray(message);
437441
}
438-
GoalInfoArray.freeArray(message);
439442
}
440443
}
441444

test/test-action-server.js

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ describe('rclnodejs action server', function () {
446446

447447
let result = await handle.getResult();
448448
assert.ok(result);
449-
assert.ok(handle.status, GoalStatus.STATUS_SUCCEEDED);
449+
assert.strictEqual(handle.status, GoalStatus.STATUS_SUCCEEDED);
450450
assert.ok(deepEqual(result.sequence, Int32Array.from(testSequence)));
451451

452452
server.destroy();
@@ -480,7 +480,7 @@ describe('rclnodejs action server', function () {
480480

481481
let result = await handle.getResult();
482482
assert.ok(result);
483-
assert.ok(handle.status, GoalStatus.STATUS_ABORTED);
483+
assert.strictEqual(handle.status, GoalStatus.STATUS_ABORTED);
484484
assert.ok(deepEqual(result.sequence, Int32Array.from(testSequence)));
485485

486486
server.destroy();
@@ -515,7 +515,7 @@ describe('rclnodejs action server', function () {
515515
let result = await handle.getResult();
516516
assert.ok(result);
517517
// Goal status should default to aborted
518-
assert.ok(handle.status, GoalStatus.STATUS_ABORTED);
518+
assert.strictEqual(handle.status, GoalStatus.STATUS_ABORTED);
519519
assert.ok(deepEqual(result.sequence, Int32Array.from(testSequence)));
520520

521521
server.destroy();
@@ -543,7 +543,7 @@ describe('rclnodejs action server', function () {
543543
let result = await handle.getResult();
544544
assert.ok(result);
545545
// Goal status should default to aborted
546-
assert.ok(handle.status, GoalStatus.STATUS_ABORTED);
546+
assert.strictEqual(handle.status, GoalStatus.STATUS_ABORTED);
547547
assert.ok(deepEqual(result.sequence, Int32Array.from([])));
548548

549549
server.destroy();
@@ -567,7 +567,7 @@ describe('rclnodejs action server', function () {
567567
await client.sendGoal(goal);
568568

569569
await assertUtils.createDelay(500);
570-
assert.ok(server._goalHandles.size, 1);
570+
assert.strictEqual(server._goalHandles.size, 1);
571571

572572
server.destroy();
573573
});
@@ -590,10 +590,10 @@ describe('rclnodejs action server', function () {
590590
await client.sendGoal(goal);
591591

592592
await assertUtils.createDelay(500);
593-
assert.ok(server._goalHandles.size, 1);
593+
assert.strictEqual(server._goalHandles.size, 1);
594594

595-
await assertUtils.createDelay(1000);
596-
assert.ok(server._goalHandles.size, 0);
595+
await assertUtils.createDelay(3000);
596+
assert.strictEqual(server._goalHandles.size, 0);
597597

598598
server.destroy();
599599
});
@@ -610,6 +610,24 @@ describe('rclnodejs action server', function () {
610610
{ resultTimeout: 1 }
611611
);
612612

613+
// Wrap _executeExpiredGoals to capture the goalInfo deserialized from the
614+
// native buffer, so we can verify they match the accepted goals.
615+
const expiredGoalInfos = [];
616+
const origExecuteExpiredGoals = server._executeExpiredGoals.bind(server);
617+
server._executeExpiredGoals = function (result, count) {
618+
const ActionInterfaces = require('../lib/action/interfaces.js');
619+
const ActionUuid = require('../lib/action/uuid.js');
620+
for (let i = 0; i < count; i++) {
621+
const goalInfo = new ActionInterfaces.GoalInfo();
622+
goalInfo.deserialize(result._refArray[i]);
623+
expiredGoalInfos.push({
624+
uuid: ActionUuid.fromBytes(goalInfo.goal_id.uuid).toString(),
625+
stamp: goalInfo.stamp,
626+
});
627+
}
628+
origExecuteExpiredGoals(result, count);
629+
};
630+
613631
let goal = new Fibonacci.Goal();
614632

615633
await client.waitForServer(1000);
@@ -620,10 +638,37 @@ describe('rclnodejs action server', function () {
620638
]);
621639

622640
await assertUtils.createDelay(500);
623-
assert.ok(server._goalHandles.size, 3);
641+
assert.strictEqual(server._goalHandles.size, 3);
642+
643+
// Snapshot the accepted goal UUIDs before expiration
644+
const acceptedUuids = Array.from(server._goalHandles.keys()).sort();
645+
646+
await assertUtils.createDelay(3000);
647+
assert.strictEqual(server._goalHandles.size, 0);
648+
649+
// Verify the expired goal info deserialized from the native buffer
650+
assert.strictEqual(expiredGoalInfos.length, 3);
651+
652+
const expiredUuids = expiredGoalInfos.map((g) => g.uuid).sort();
624653

625-
await assertUtils.createDelay(1000);
626-
assert.ok(server._goalHandles.size, 0);
654+
// Each UUID must be non-zero (the original bug produced all-zero UUIDs)
655+
for (const info of expiredGoalInfos) {
656+
assert.notStrictEqual(info.uuid, Array(16).fill(0).join(','));
657+
}
658+
659+
// All 3 UUIDs must be unique (3 distinct goals)
660+
assert.strictEqual(new Set(expiredUuids).size, 3);
661+
662+
// The expired UUIDs must match the originally accepted ones
663+
assert.deepStrictEqual(expiredUuids, acceptedUuids);
664+
665+
// Each expired goal must have a non-zero acceptance stamp
666+
for (const info of expiredGoalInfos) {
667+
assert.ok(
668+
info.stamp.sec > 0 || info.stamp.nanosec > 0,
669+
`Expected non-zero stamp, got sec=${info.stamp.sec} nanosec=${info.stamp.nanosec}`
670+
);
671+
}
627672

628673
server.destroy();
629674
});

0 commit comments

Comments
 (0)