field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | README | LICENSE

runtime_lifecycle.rs (4293B)


      1 use std::{sync::Arc, time::Duration};
      2 
      3 use tera_ffi::TeraAppError;
      4 
      5 mod support;
      6 
      7 #[tokio::test]
      8 async fn host_release_ordering_retains_close_and_finishes_within_deadline() {
      9     let (_root, runtime) = support::runtime().await;
     10     let host = Arc::new(runtime);
     11     let closing_owner = Arc::clone(&host);
     12     let close = tokio::spawn(async move { closing_owner.shutdown().await });
     13     drop(host);
     14 
     15     let result = tokio::time::timeout(Duration::from_secs(1), close)
     16         .await
     17         .expect("mobile shutdown exceeded its host deadline")
     18         .expect("shutdown task panicked")
     19         .expect("shutdown failed");
     20     assert_eq!(result.state, "closed");
     21     assert!(!result.already_closed);
     22 }
     23 
     24 #[tokio::test]
     25 async fn concurrent_host_references_converge_and_repeated_close_is_idempotent() {
     26     let (_root, runtime) = support::runtime().await;
     27     let runtime = Arc::new(runtime);
     28     let first = Arc::clone(&runtime);
     29     let second = Arc::clone(&runtime);
     30     let (first, second) = tokio::join!(first.shutdown(), second.shutdown());
     31 
     32     for outcome in [&first, &second] {
     33         assert!(
     34             outcome.is_ok()
     35                 || matches!(
     36                     outcome,
     37                     Err(TeraAppError::Failure { report })
     38                         if report.code == "client_close_in_progress"
     39                 )
     40         );
     41     }
     42     let repeated = runtime.shutdown().await.expect("repeated close");
     43     assert!(repeated.already_closed);
     44 }
     45 
     46 struct CallbackGate {
     47     entered: Arc<tokio::sync::Notify>,
     48     released: Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>,
     49 }
     50 
     51 impl tera_ffi::TeraRuntimeObserver for CallbackGate {
     52     fn on_change(&self, _: tera_ffi::FfiRuntimeChangeRecord) {
     53         self.entered.notify_one();
     54         let (released, wake) = &*self.released;
     55         drop(
     56             wake.wait_while(released.lock().unwrap(), |released| !*released)
     57                 .unwrap(),
     58         );
     59     }
     60 }
     61 
     62 struct ReleaseCallback(Arc<(std::sync::Mutex<bool>, std::sync::Condvar)>);
     63 
     64 impl Drop for ReleaseCallback {
     65     fn drop(&mut self) {
     66         *self.0.0.lock().unwrap() = true;
     67         self.0.1.notify_all();
     68     }
     69 }
     70 
     71 #[tokio::test]
     72 async fn ffi_close_retains_a_slow_native_callback_across_cancelled_and_repeated_waits() {
     73     use std::{
     74         future::{Future, poll_fn},
     75         task::Poll,
     76     };
     77     let (_root, runtime) = support::runtime().await;
     78     let entered = Arc::new(tokio::sync::Notify::new());
     79     let released = Arc::new((std::sync::Mutex::new(false), std::sync::Condvar::new()));
     80     let release = ReleaseCallback(Arc::clone(&released));
     81     let observer = runtime
     82         .subscribe_changes(Box::new(CallbackGate {
     83             entered: Arc::clone(&entered),
     84             released,
     85         }))
     86         .unwrap();
     87     tokio::time::timeout(Duration::from_secs(5), entered.notified())
     88         .await
     89         .unwrap();
     90     let mut close = Box::pin(runtime.shutdown());
     91     tokio::time::timeout(Duration::from_secs(5), async {
     92         loop {
     93             // Poll the actual exported close until storage has settled. Native
     94             // callback ownership must still keep its result pending.
     95             let outcome = poll_fn(|context| Poll::Ready(close.as_mut().poll(context))).await;
     96             assert!(
     97                 outcome.is_pending(),
     98                 "FFI close returned before its native callback"
     99             );
    100             if runtime.info().sdk_closed {
    101                 break;
    102             }
    103             tokio::task::yield_now().await;
    104         }
    105     })
    106     .await
    107     .unwrap();
    108     assert!(!observer.is_active());
    109     assert_eq!(
    110         runtime
    111             .sdk_storage_status()
    112             .await
    113             .unwrap_err()
    114             .report()
    115             .code,
    116         "client_closed"
    117     );
    118     drop(close);
    119     let mut repeat = Box::pin(runtime.shutdown());
    120     let outcome = poll_fn(|context| Poll::Ready(repeat.as_mut().poll(context))).await;
    121     assert!(
    122         outcome.is_pending(),
    123         "Cancelled close must retain its callback drain"
    124     );
    125     drop(release);
    126     let receipt = tokio::time::timeout(Duration::from_secs(5), repeat)
    127         .await
    128         .unwrap()
    129         .unwrap();
    130     assert_eq!(receipt.state, "closed");
    131     assert!(receipt.already_closed);
    132     assert!(runtime.shutdown().await.unwrap().already_closed);
    133 }