@@ -460,6 +460,15 @@ impl ContextStack {
460460 // The empty context is always at the bottom of the [`ContextStack`]
461461 // and cannot be popped, and the overflow position is invalid, so do
462462 // nothing.
463+ otel_warn ! (
464+ name: "Context.OutOfOrderDrop" ,
465+ position = pos,
466+ message = if pos == ContextStack :: BASE_POS {
467+ "Attempted to pop the base context which is not allowed"
468+ } else {
469+ "Attempted to pop the overflow position which is not allowed"
470+ }
471+ ) ;
463472 return ;
464473 }
465474 let len: u16 = self . stack . len ( ) as u16 ;
@@ -479,6 +488,12 @@ impl ContextStack {
479488 // This is an out of order pop.
480489 if pos >= len {
481490 // This is an invalid id, ignore it.
491+ otel_warn ! (
492+ name: "Context.PopOutOfBounds" ,
493+ position = pos,
494+ stack_length = len,
495+ message = "Attempted to pop beyond the end of the context stack"
496+ ) ;
482497 return ;
483498 }
484499 // Clear out the entry at the given id.
@@ -505,6 +520,8 @@ impl Default for ContextStack {
505520#[ cfg( test) ]
506521mod tests {
507522 use super :: * ;
523+ use std:: time:: Duration ;
524+ use tokio:: time:: sleep;
508525
509526 #[ derive( Debug , PartialEq ) ]
510527 struct ValueA ( u64 ) ;
@@ -653,20 +670,231 @@ mod tests {
653670 for _ in 0 ..16 {
654671 let cx_guard = Context :: current ( ) . with_value ( ValueA ( 1 ) ) . attach ( ) ;
655672 assert_eq ! ( cx_guard. cx_pos, ContextStack :: MAX_POS ) ;
656- assert_eq ! ( Context :: current( ) . get( ) , Some ( & ValueA ( 2 ) ) ) ;
673+ assert_eq ! ( Context :: current( ) . get:: < ValueA > ( ) , Some ( & ValueA ( 2 ) ) ) ;
657674 assert_eq ! ( Context :: current( ) . get( ) , Some ( & ValueB ( stack_max_pos - 2 ) ) ) ;
658675 guards. push ( cx_guard) ;
659676 }
660677 }
661678
679+ /// Tests that a new ContextStack is created with the correct initial capacity.
680+ #[ test]
681+ fn test_initial_capacity ( ) {
682+ let stack = ContextStack :: default ( ) ;
683+ assert_eq ! ( stack. stack. capacity( ) , ContextStack :: INITIAL_CAPACITY ) ;
684+ }
685+
686+ /// Tests that map_current_cx correctly accesses the current context.
687+ #[ test]
688+ fn test_map_current_cx ( ) {
689+ let mut stack = ContextStack :: default ( ) ;
690+ let test_value = ValueA ( 42 ) ;
691+ stack. current_cx = Context :: new ( ) . with_value ( test_value) ;
692+
693+ let result = stack. map_current_cx ( |cx| {
694+ assert_eq ! ( cx. get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
695+ true
696+ } ) ;
697+ assert ! ( result) ;
698+ }
699+
700+ /// Tests popping contexts in non-sequential order.
701+ #[ test]
702+ fn test_pop_id_out_of_order ( ) {
703+ let mut stack = ContextStack :: default ( ) ;
704+
705+ // Push three contexts
706+ let cx1 = Context :: new ( ) . with_value ( ValueA ( 1 ) ) ;
707+ let cx2 = Context :: new ( ) . with_value ( ValueA ( 2 ) ) ;
708+ let cx3 = Context :: new ( ) . with_value ( ValueA ( 3 ) ) ;
709+
710+ let id1 = stack. push ( cx1) ;
711+ let id2 = stack. push ( cx2) ;
712+ let id3 = stack. push ( cx3) ;
713+
714+ // Pop middle context first - should not affect current context
715+ stack. pop_id ( id2) ;
716+ assert_eq ! ( stack. current_cx. get:: <ValueA >( ) , Some ( & ValueA ( 3 ) ) ) ;
717+ assert_eq ! ( stack. stack. len( ) , 3 ) ; // Length unchanged for middle pops
718+
719+ // Pop last context - should restore previous valid context
720+ stack. pop_id ( id3) ;
721+ assert_eq ! ( stack. current_cx. get:: <ValueA >( ) , Some ( & ValueA ( 1 ) ) ) ;
722+ assert_eq ! ( stack. stack. len( ) , 1 ) ;
723+
724+ // Pop first context - should restore to empty state
725+ stack. pop_id ( id1) ;
726+ assert_eq ! ( stack. current_cx. get:: <ValueA >( ) , None ) ;
727+ assert_eq ! ( stack. stack. len( ) , 0 ) ;
728+ }
729+
730+ /// Tests edge cases in context stack operations. IRL these should log
731+ /// warnings, and definitely not panic.
662732 #[ test]
663- fn context_stack_pop_id ( ) {
664- // This is to get full line coverage of the `pop_id` function.
665- // In real life the `Drop`` implementation of `ContextGuard` ensures that
666- // the ids are valid and inside the bounds.
733+ fn test_pop_id_edge_cases ( ) {
667734 let mut stack = ContextStack :: default ( ) ;
735+
736+ // Test popping BASE_POS - should be no-op
668737 stack. pop_id ( ContextStack :: BASE_POS ) ;
738+ assert_eq ! ( stack. stack. len( ) , 0 ) ;
739+
740+ // Test popping MAX_POS - should be no-op
669741 stack. pop_id ( ContextStack :: MAX_POS ) ;
670- stack. pop_id ( 4711 ) ;
742+ assert_eq ! ( stack. stack. len( ) , 0 ) ;
743+
744+ // Test popping invalid position - should be no-op
745+ stack. pop_id ( 1000 ) ;
746+ assert_eq ! ( stack. stack. len( ) , 0 ) ;
747+
748+ // Test popping from empty stack - should be safe
749+ stack. pop_id ( 1 ) ;
750+ assert_eq ! ( stack. stack. len( ) , 0 ) ;
751+ }
752+
753+ /// Tests stack behavior when reaching maximum capacity.
754+ /// Once we push beyond this point, we should end up with a context
755+ /// that points _somewhere_, but mutating it should not affect the current
756+ /// active context.
757+ #[ test]
758+ fn test_push_overflow ( ) {
759+ let mut stack = ContextStack :: default ( ) ;
760+ let max_pos = ContextStack :: MAX_POS as usize ;
761+
762+ // Fill stack up to max position
763+ for i in 0 ..max_pos {
764+ let cx = Context :: new ( ) . with_value ( ValueA ( i as u64 ) ) ;
765+ let id = stack. push ( cx) ;
766+ assert_eq ! ( id, ( i + 1 ) as u16 ) ;
767+ }
768+
769+ // Try to push beyond capacity
770+ let cx = Context :: new ( ) . with_value ( ValueA ( max_pos as u64 ) ) ;
771+ let id = stack. push ( cx) ;
772+ assert_eq ! ( id, ContextStack :: MAX_POS ) ;
773+
774+ // Verify current context remains unchanged after overflow
775+ assert_eq ! (
776+ stack. current_cx. get:: <ValueA >( ) ,
777+ Some ( & ValueA ( ( max_pos - 2 ) as u64 ) )
778+ ) ;
779+ }
780+
781+ /// Tests that:
782+ /// 1. Parent context values are properly propagated to async operations
783+ /// 2. Values added during async operations do not affect parent context
784+ #[ tokio:: test]
785+ async fn test_async_context_propagation ( ) {
786+ // A nested async operation we'll use to test propagation
787+ async fn nested_operation ( ) {
788+ // Verify we can see the parent context's value
789+ assert_eq ! (
790+ Context :: current( ) . get:: <ValueA >( ) ,
791+ Some ( & ValueA ( 42 ) ) ,
792+ "Parent context value should be available in async operation"
793+ ) ;
794+
795+ // Create new context
796+ let cx_with_both = Context :: current ( )
797+ . with_value ( ValueA ( 43 ) ) // override ValueA
798+ . with_value ( ValueB ( 24 ) ) ; // Add new ValueB
799+
800+ // Run nested async operation with both values
801+ async {
802+ // Verify both values are available
803+ assert_eq ! (
804+ Context :: current( ) . get:: <ValueA >( ) ,
805+ Some ( & ValueA ( 43 ) ) ,
806+ "Parent value should still be available after adding new value"
807+ ) ;
808+ assert_eq ! (
809+ Context :: current( ) . get:: <ValueB >( ) ,
810+ Some ( & ValueB ( 24 ) ) ,
811+ "New value should be available in async operation"
812+ ) ;
813+
814+ // Do some async work to simulate real-world scenario
815+ sleep ( Duration :: from_millis ( 10 ) ) . await ;
816+
817+ // Values should still be available after async work
818+ assert_eq ! (
819+ Context :: current( ) . get:: <ValueA >( ) ,
820+ Some ( & ValueA ( 43 ) ) ,
821+ "Parent value should persist across await points"
822+ ) ;
823+ assert_eq ! (
824+ Context :: current( ) . get:: <ValueB >( ) ,
825+ Some ( & ValueB ( 24 ) ) ,
826+ "New value should persist across await points"
827+ ) ;
828+ }
829+ . with_context ( cx_with_both)
830+ . await ;
831+ }
832+
833+ // Set up initial context with ValueA
834+ let parent_cx = Context :: new ( ) . with_value ( ValueA ( 42 ) ) ;
835+
836+ // Create and run async operation with the parent context explicitly propagated
837+ nested_operation ( ) . with_context ( parent_cx. clone ( ) ) . await ;
838+
839+ // After async operation completes:
840+ // 1. Parent context should be unchanged
841+ assert_eq ! (
842+ parent_cx. get:: <ValueA >( ) ,
843+ Some ( & ValueA ( 42 ) ) ,
844+ "Parent context should be unchanged"
845+ ) ;
846+ assert_eq ! (
847+ parent_cx. get:: <ValueB >( ) ,
848+ None ,
849+ "Parent context should not see values added in async operation"
850+ ) ;
851+
852+ // 2. Current context should be back to default
853+ assert_eq ! (
854+ Context :: current( ) . get:: <ValueA >( ) ,
855+ None ,
856+ "Current context should be back to default"
857+ ) ;
858+ assert_eq ! (
859+ Context :: current( ) . get:: <ValueB >( ) ,
860+ None ,
861+ "Current context should not have async operation's values"
862+ ) ;
863+ }
864+
865+ ///
866+ /// Tests that unnatural parent->child relationships in nested async
867+ /// operations behave properly.
868+ ///
869+ #[ tokio:: test]
870+ async fn test_out_of_order_context_detachment_futures ( ) {
871+ // This function returns a future, but doesn't await it
872+ // It will complete before the future that it creates.
873+ async fn create_a_future ( ) -> impl std:: future:: Future < Output = ( ) > {
874+ // Create a future that will do some work, referencing our current
875+ // context, but don't await it.
876+ async {
877+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , Some ( & ValueA ( 42 ) ) ) ;
878+
879+ // Longer work
880+ sleep ( Duration :: from_millis ( 50 ) ) . await ;
881+ }
882+ . with_context ( Context :: current ( ) )
883+ }
884+
885+ // Create our base context
886+ let parent_cx = Context :: new ( ) . with_value ( ValueA ( 42 ) ) ;
887+
888+ // await our nested function, which will create and detach a context
889+ let future = create_a_future ( ) . with_context ( parent_cx) . await ;
890+
891+ // Execute the future. The future that created it is long gone, but this shouldn't
892+ // cause issues.
893+ let _a = future. await ;
894+
895+ // Nothing terrible (e.g., panics!) should happen, and we should definitely not have any
896+ // values attached to our current context that were set in the nested operations.
897+ assert_eq ! ( Context :: current( ) . get:: <ValueA >( ) , None ) ;
898+ assert_eq ! ( Context :: current( ) . get:: <ValueB >( ) , None ) ;
671899 }
672900}
0 commit comments