//! F49 step 2 — F45 buffered-recovery-replay live verification. //! //! Subscribe buffered, force `Session::recover_connection` mid-flight, //! assert the replay branch issued `RegisterReference` (NOT //! `AdviseSupervisory`) by observing that the subscription continues //! to receive `0x33` DataUpdate frames after the recovery completes. //! //! Mirrors the .NET reference's `MxNativeSession.ReAdviseSubscription` //! (`MxNativeSession.cs:538-569`) which branches on //! `subscription.IsBuffered` to pick the right replay op. #![allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] #[cfg(all(windows, feature = "live-windows-com"))] mod live { use std::sync::Arc; use std::time::{Duration, Instant}; use mxaccess::{BufferedOptions, RecoveryPolicy, Session, SessionOptions}; use mxaccess_galaxy::SqlTagResolver; use mxaccess_nmx::NmxClient; use mxaccess_rpc::ntlm::NtlmClientContext; fn ntlm_from_test_env() -> NtlmClientContext { let user = std::env::var("MX_TEST_USER").expect("MX_TEST_USER"); let password = std::env::var("MX_TEST_PASSWORD").expect("MX_TEST_PASSWORD"); let domain = std::env::var("MX_TEST_DOMAIN").unwrap_or_default(); let hostname = std::env::var("COMPUTERNAME").unwrap_or_default(); NtlmClientContext::new(&user, &password, &domain, Some(&hostname)) } /// Drain the broadcast until at least `target` raw NMX subscription /// messages arrive or the deadline passes. Returns the count. async fn drain_until( rx: &mut tokio::sync::broadcast::Receiver< Arc, >, target: usize, deadline: Instant, label: &str, ) -> usize { let mut received = 0; while received < target && Instant::now() < deadline { match tokio::time::timeout(Duration::from_secs(5), rx.recv()).await { Ok(Ok(msg)) => { eprintln!( "[{label} {received}] cmd=0x{:02x} record_count={}", msg.command, msg.record_count ); received += 1; } Ok(Err(_)) => break, Err(_) => eprintln!("5s gap on {label} broadcast"), } } received } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[ignore] async fn buffered_recovery_replays_register_reference() { if std::env::var_os("MX_LIVE").is_none() { eprintln!("MX_LIVE not set — skipping live test"); return; } let tag = std::env::var("MX_TEST_TAG") .unwrap_or_else(|_| "TestMachine_001.TestChangingInt".to_string()); let _ = tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), ) .with_test_writer() .try_init(); let galaxy_db = std::env::var("MX_GALAXY_DB").expect("MX_GALAXY_DB"); let resolver = Arc::new( SqlTagResolver::from_ado_string(&galaxy_db).expect("SqlTagResolver"), ); // Permissive recovery policy — let the test drive a single // attempt synchronously. let recovery = RecoveryPolicy::default(); let session = Session::connect_nmx_auto( ntlm_from_test_env, SessionOptions::default(), resolver, recovery, ) .await .expect("connect_nmx_auto"); eprintln!("session connected"); // Install a recovery factory that rebuilds NmxClient via the // same auto-resolving COM-activation path connect_nmx_auto // uses. let factory: mxaccess::RebuildFactory = Arc::new(|| { Box::pin(async { NmxClient::create(ntlm_from_test_env).await }) }); session.set_recovery_factory(factory).await; // Subscribe buffered + drain a few pre-recovery frames to // confirm the wire path is hot. let mut callbacks_rx = session.callbacks(); let opts = BufferedOptions { update_interval_ms: 1_000, }; let sub = session .subscribe_buffered(&tag, opts) .await .expect("subscribe_buffered"); eprintln!( "buffered subscribed, correlation_id = {:02x?}", sub.correlation_id() ); let pre = drain_until( &mut callbacks_rx, 2, Instant::now() + Duration::from_secs(15), "pre-recovery", ) .await; assert!(pre >= 1, "pre-recovery: subscription wire path is dead"); eprintln!("pre-recovery: drained {pre} NMX subscription messages"); // Force a transport rebuild + advise replay. The recovery // should re-issue `RegisterReference` (NOT // `AdviseSupervisory`) for the buffered entry — verified // structurally by `recover_connection_replays_register_reference_for_buffered` // in the unit-test suite. Live-side, we assert that the post- // recovery wire path keeps producing NMX subscription messages. eprintln!("triggering recover_connection"); session .recover_connection(RecoveryPolicy::default()) .await .expect("recover_connection"); eprintln!("recover_connection returned Ok — F45 buffered replay path executed"); // Drain post-recovery frames. The NmxClient was rebuilt under // the hood; the broadcast channel is the same, but the // re-issued `RegisterReference` should kick off a fresh // SubscriptionStatus + DataUpdate sequence. let post = drain_until( &mut callbacks_rx, 2, Instant::now() + Duration::from_secs(15), "post-recovery", ) .await; assert!( post >= 1, "post-recovery: no NMX messages after recover_connection — buffered replay didn't \ re-establish the subscription" ); eprintln!("post-recovery: drained {post} NMX subscription messages"); session.unsubscribe(sub).await.expect("unsubscribe"); session.shutdown_nmx().await.expect("shutdown"); } } #[cfg(not(all(windows, feature = "live-windows-com")))] mod live { #[test] #[ignore] fn buffered_recovery_replays_register_reference() { eprintln!("test skipped: requires Windows + live-windows-com feature"); } }