Skip to content

Commit 7a6b153

Browse files
committed
Add tests for action cancellation
Signed-off-by: Michael X. Grey <[email protected]>
1 parent 1693a83 commit 7a6b153

File tree

4 files changed

+353
-27
lines changed

4 files changed

+353
-27
lines changed

rclrs/src/action.rs

Lines changed: 209 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ impl CancelResponseCode {
9393
pub fn is_accepted(&self) -> bool {
9494
matches!(self, Self::Accept)
9595
}
96+
97+
/// Check if the cancellation was rejected.
98+
pub fn is_rejected(&self) -> bool {
99+
matches!(self, Self::Reject)
100+
}
96101
}
97102

98103
impl From<i8> for CancelResponseCode {
@@ -135,6 +140,11 @@ impl CancelResponse {
135140
pub fn is_accepted(&self) -> bool {
136141
self.code.is_accepted()
137142
}
143+
144+
/// Check whether the request was rejected.
145+
pub fn is_rejected(&self) -> bool {
146+
self.code.is_rejected()
147+
}
138148
}
139149

140150
/// This is returned by [`ActionClientState::cancel_all_goals`] and
@@ -153,6 +163,11 @@ impl MultiCancelResponse {
153163
pub fn is_accepted(&self) -> bool {
154164
self.code.is_accepted()
155165
}
166+
167+
/// Check whether the request was rejected.
168+
pub fn is_rejected(&self) -> bool {
169+
self.code.is_rejected()
170+
}
156171
}
157172

158173
/// Values defined by `action_msgs/msg/GoalStatus`
@@ -180,6 +195,13 @@ pub enum GoalStatusCode {
180195
Aborted = 6,
181196
}
182197

198+
impl GoalStatusCode {
199+
/// Check if the status belongs to one of the terminated modes
200+
pub fn is_terminated(&self) -> bool {
201+
matches!(self, Self::Succeeded | Self::Cancelled | Self::Aborted)
202+
}
203+
}
204+
183205
impl From<i8> for GoalStatusCode {
184206
fn from(value: i8) -> Self {
185207
if 0 <= value && value <= 6 {
@@ -253,7 +275,7 @@ mod tests {
253275
let action_name = format!("test_action_success_{}_action", line!());
254276
let _action_server = node
255277
.create_action_server(&action_name, |handle| {
256-
fibonacci_action(handle, Duration::from_micros(10))
278+
fibonacci_action(handle, TestActionSettings::default())
257279
})
258280
.unwrap();
259281

@@ -315,10 +337,10 @@ mod tests {
315337
let node = executor
316338
.create_node(&format!("test_action_cancel_{}", line!()))
317339
.unwrap();
318-
let action_name = format!("test_action_cancel_{}_slow_action", line!());
340+
let action_name = format!("test_action_cancel_{}_action", line!());
319341
let _action_server = node
320342
.create_action_server(&action_name, |handle| {
321-
fibonacci_action(handle, Duration::from_secs(1))
343+
fibonacci_action(handle, TestActionSettings::slow())
322344
})
323345
.unwrap();
324346

@@ -339,9 +361,121 @@ mod tests {
339361
executor.spin(SpinOptions::default().until_promise_resolved(promise));
340362
}
341363

364+
#[test]
365+
fn test_action_cancel_rejection() {
366+
let mut executor = Context::default().create_basic_executor();
367+
368+
let node = executor
369+
.create_node(&format!("test_action_cancel_refusal_{}", line!()))
370+
.unwrap();
371+
let action_name = format!("test_action_cancel_refusal_{}_action", line!());
372+
let _action_server = node
373+
.create_action_server(&action_name, |handle| {
374+
// This action server will intentionally reject 3 cancellation requests
375+
fibonacci_action(handle, TestActionSettings::slow().cancel_refusal(3))
376+
})
377+
.unwrap();
378+
379+
let client = node
380+
.create_action_client::<Fibonacci>(&action_name)
381+
.unwrap();
382+
383+
let request = client.request_goal(Fibonacci_Goal { order: 10 });
384+
385+
let promise = executor.commands().run(async move {
386+
let goal_client = request.await.unwrap();
387+
388+
// The first three cancellation requests should be rejected
389+
for _ in 0..3 {
390+
let cancellation = goal_client.cancellation.cancel().await;
391+
assert!(cancellation.is_rejected());
392+
}
393+
394+
// The next cancellation request should be accepted
395+
let cancellation = goal_client.cancellation.cancel().await;
396+
assert!(cancellation.is_accepted());
397+
398+
// The next one should also be accepted or we get notified that the
399+
// goal no longer exists.
400+
let late_cancellation = goal_client.cancellation.cancel().await;
401+
assert!(matches!(
402+
late_cancellation.code,
403+
CancelResponseCode::Accept | CancelResponseCode::GoalTerminated
404+
));
405+
406+
let (status, _) = goal_client.result.await;
407+
assert_eq!(status, GoalStatusCode::Cancelled);
408+
409+
// After we have received the response, we can be confident that the
410+
// action server will report back that the goal was terminated.
411+
let very_late_cancellation = goal_client.cancellation.cancel().await;
412+
assert!(matches!(
413+
very_late_cancellation.code,
414+
CancelResponseCode::GoalTerminated
415+
));
416+
});
417+
418+
executor.spin(SpinOptions::default().until_promise_resolved(promise));
419+
}
420+
421+
#[test]
422+
fn test_action_slow_cancel() {
423+
let mut executor = Context::default().create_basic_executor();
424+
425+
let node = executor
426+
.create_node(&format!("test_action_slow_cancel_{}", line!()))
427+
.unwrap();
428+
let action_name = format!("test_action_slow_cancel_{}_action", line!());
429+
let _action_server = node
430+
.create_action_server(&action_name, |handle| {
431+
// This action server will intentionally reject 3 cancellation requests
432+
fibonacci_action(
433+
handle,
434+
TestActionSettings::slow()
435+
.cancel_refusal(3)
436+
.continue_after_cancelling(),
437+
)
438+
})
439+
.unwrap();
440+
441+
let client = node
442+
.create_action_client::<Fibonacci>(&action_name)
443+
.unwrap();
444+
445+
let request = client.request_goal(Fibonacci_Goal { order: 10 });
446+
447+
let promise = executor.commands().run(async move {
448+
let goal_client = request.await.unwrap();
449+
450+
// The first three cancellation requests should be rejected
451+
for _ in 0..3 {
452+
let cancellation = goal_client.cancellation.cancel().await;
453+
assert!(cancellation.is_rejected());
454+
}
455+
456+
// The next cancellation request should be accepted
457+
let cancellation = goal_client.cancellation.cancel().await;
458+
assert!(cancellation.is_accepted());
459+
460+
// The next one should also be accepted or we get notified that the
461+
// goal no longer exists.
462+
let late_cancellation = goal_client.cancellation.cancel().await;
463+
assert!(late_cancellation.is_accepted());
464+
465+
let very_late_cancellation = goal_client.cancellation.cancel().await;
466+
assert!(very_late_cancellation.is_accepted());
467+
});
468+
469+
executor.spin(SpinOptions::default().until_promise_resolved(promise));
470+
}
471+
342472
async fn fibonacci_action(
343473
handle: RequestedGoal<Fibonacci>,
344-
period: Duration,
474+
TestActionSettings {
475+
period,
476+
cancel_refusal_limit,
477+
continue_after_cancelling,
478+
}: TestActionSettings,
345479
) -> TerminatedGoal {
346480
let goal_order = handle.goal().order;
347481
if goal_order < 0 {
@@ -376,7 +510,8 @@ mod tests {
376510
});
377511

378512
let mut sequence = Vec::new();
379-
loop {
513+
let mut cancel_requests = 0;
514+
let cancelling = loop {
380515
match executing.unless_cancel_requested(receiver.recv()).await {
381516
Ok(Some(next)) => {
382517
// We have a new item in the sequence
@@ -391,11 +526,78 @@ mod tests {
391526
return executing.succeeded_with(result);
392527
}
393528
Err(_) => {
394-
// The action has been cancelled
529+
// The user has asked for the action to be cancelled
530+
cancel_requests += 1;
531+
if cancel_requests > cancel_refusal_limit {
532+
let cancelling = executing.begin_cancelling();
533+
if !continue_after_cancelling {
534+
result.sequence = sequence;
535+
return cancelling.cancelled_with(result);
536+
}
537+
538+
break cancelling;
539+
}
540+
541+
// We have not yet reached the number of cancel requests that
542+
// we intend to reject. Reject this cancellation and wait for
543+
// the next one.
544+
executing.reject_cancellation();
545+
}
546+
}
547+
};
548+
549+
// We will continue to iterate to the finish even though we are in the
550+
// cancelling mode. We only do this as a way of running tests on a
551+
// prolonged action cancelling state.
552+
loop {
553+
match receiver.recv().await {
554+
Some(next) => {
555+
sequence.push(next);
556+
cancelling.publish_feedback(Fibonacci_Feedback {
557+
sequence: sequence.clone(),
558+
});
559+
}
560+
None => {
561+
// The sequence has finished
395562
result.sequence = sequence;
396-
return executing.begin_cancelling().cancelled_with(result);
563+
return cancelling.succeeded_with(result);
397564
}
398565
}
399566
}
400567
}
568+
569+
struct TestActionSettings {
570+
period: Duration,
571+
cancel_refusal_limit: usize,
572+
continue_after_cancelling: bool,
573+
}
574+
575+
impl Default for TestActionSettings {
576+
fn default() -> Self {
577+
TestActionSettings {
578+
period: Duration::from_micros(10),
579+
cancel_refusal_limit: 0,
580+
continue_after_cancelling: false,
581+
}
582+
}
583+
}
584+
585+
impl TestActionSettings {
586+
fn slow() -> Self {
587+
TestActionSettings {
588+
period: Duration::from_secs(1),
589+
..Default::default()
590+
}
591+
}
592+
593+
fn cancel_refusal(mut self, limit: usize) -> Self {
594+
self.cancel_refusal_limit = limit;
595+
self
596+
}
597+
598+
fn continue_after_cancelling(mut self) -> Self {
599+
self.continue_after_cancelling = true;
600+
self
601+
}
602+
}
401603
}

rclrs/src/action/action_goal_receiver.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
ActionServer, ActionServerOptions, ActionServerState, Node, RclrsError, RequestedGoal,
2+
ActionServer, ActionServerState, IntoActionServerOptions, Node, RclrsError, RequestedGoal,
33
TerminatedGoal,
44
};
55
use rosidl_runtime_rs::Action;
@@ -59,7 +59,7 @@ impl<A: Action> ActionGoalReceiver<A> {
5959

6060
pub(crate) fn new<'a>(
6161
node: &Node,
62-
options: impl Into<ActionServerOptions<'a>>,
62+
options: impl IntoActionServerOptions<'a>,
6363
) -> Result<Self, RclrsError> {
6464
let (sender, receiver) = unbounded_channel();
6565
let server = ActionServerState::new_for_receiver(node, options, sender)?;

0 commit comments

Comments
 (0)