@@ -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
98103impl 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+
183205impl 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}
0 commit comments