|
| 1 | +//! Regression test for #427 / #428 (F-TEST-4): the connect-timeout |
| 2 | +//! call in `app.rs::run` must construct `tokio::time::timeout(...)` |
| 3 | +//! *inside* the runtime context. |
| 4 | +//! |
| 5 | +//! `tokio::time::timeout(...)` registers with the timer driver at |
| 6 | +//! **construction**, not at first poll. If constructed outside a |
| 7 | +//! runtime (e.g. as the argument to `runtime.block_on(...)` directly), |
| 8 | +//! it panics with `"there is no reactor running, must be called from |
| 9 | +//! the context of a Tokio 1.x runtime"`. Pre-fix, every `pitboss-tui` |
| 10 | +//! launch hit this on the connect-control path, making v0.13.0 |
| 11 | +//! unusable. |
| 12 | +//! |
| 13 | +//! The fix wraps the timeout in `async { ... .await }` so the timeout |
| 14 | +//! is constructed inside the future being polled by `block_on`. This |
| 15 | +//! test mirrors the exact pattern from `app.rs:90-102`. A regression |
| 16 | +//! that unwraps the async block as a "simplification" would panic in |
| 17 | +//! the body of this test. |
| 18 | +
|
| 19 | +use std::path::PathBuf; |
| 20 | +use std::time::Duration; |
| 21 | + |
| 22 | +#[test] |
| 23 | +fn timeout_constructed_inside_async_block_does_not_panic() { |
| 24 | + let runtime = tokio::runtime::Builder::new_multi_thread() |
| 25 | + .enable_all() |
| 26 | + .build() |
| 27 | + .unwrap(); |
| 28 | + |
| 29 | + // Exact production pattern from `pitboss_tui::app::run`. The future |
| 30 | + // passed to `block_on` constructs `tokio::time::timeout` AFTER the |
| 31 | + // runtime context is entered. `open_run_session` returns a |
| 32 | + // `RunStreamSession` directly (not a `Result`), so the outer |
| 33 | + // `Result` is purely the timeout's elapsed-vs-completed flag. |
| 34 | + let result: Result<pitboss_cli::stream::RunStreamSession, tokio::time::error::Elapsed> = |
| 35 | + runtime.block_on(async { |
| 36 | + tokio::time::timeout( |
| 37 | + Duration::from_millis(50), |
| 38 | + pitboss_cli::stream::open_run_session( |
| 39 | + PathBuf::from("/tmp/pitboss-tui-tests-nonexistent-run-dir"), |
| 40 | + Some(PathBuf::from( |
| 41 | + "/tmp/pitboss-tui-tests-nonexistent-control.sock", |
| 42 | + )), |
| 43 | + pitboss_cli::stream::LiveStreamMode::LiveOnly, |
| 44 | + ), |
| 45 | + ) |
| 46 | + .await |
| 47 | + }); |
| 48 | + |
| 49 | + // The outcome doesn't matter for this regression test — what we're |
| 50 | + // proving is that `tokio::time::timeout` construction inside the |
| 51 | + // async block doesn't panic with "no reactor running". A missing |
| 52 | + // run dir / socket either completes fast (Ok) or elapses (Err) |
| 53 | + // within the 50ms budget; both are acceptable. |
| 54 | + let _ = result; |
| 55 | +} |
| 56 | + |
| 57 | +#[test] |
| 58 | +fn naive_block_on_pattern_panics_when_constructed_outside_runtime() { |
| 59 | + // Negative coverage: prove the original v0.13.0 pattern panics. If |
| 60 | + // someone "simplifies" the wrapper away again, this test pins what |
| 61 | + // the failure mode actually was — a panic in the runtime layer. |
| 62 | + // |
| 63 | + // We catch the panic with `std::panic::catch_unwind` so this test |
| 64 | + // reports the regression rather than crashing the harness. The |
| 65 | + // closure constructs `tokio::time::timeout` *outside* an `async` |
| 66 | + // block — the same shape that broke v0.13.0. |
| 67 | + let runtime = tokio::runtime::Builder::new_multi_thread() |
| 68 | + .enable_all() |
| 69 | + .build() |
| 70 | + .unwrap(); |
| 71 | + |
| 72 | + // `catch_unwind` requires `UnwindSafe`; both the runtime and the |
| 73 | + // pure tokio future satisfy this for the construction-time panic |
| 74 | + // we're catching. |
| 75 | + let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
| 76 | + runtime.block_on(tokio::time::timeout( |
| 77 | + Duration::from_millis(50), |
| 78 | + pitboss_cli::stream::open_run_session( |
| 79 | + PathBuf::from("/tmp/pitboss-tui-tests-nonexistent-run-dir"), |
| 80 | + Some(PathBuf::from( |
| 81 | + "/tmp/pitboss-tui-tests-nonexistent-control.sock", |
| 82 | + )), |
| 83 | + pitboss_cli::stream::LiveStreamMode::LiveOnly, |
| 84 | + ), |
| 85 | + )) |
| 86 | + })) |
| 87 | + .is_err(); |
| 88 | + |
| 89 | + assert!( |
| 90 | + panicked, |
| 91 | + "tokio::time::timeout constructed outside async block should panic \ |
| 92 | + with 'no reactor running' — if this assertion fails, either tokio's \ |
| 93 | + behavior changed or the test is no longer exercising the pre-fix \ |
| 94 | + shape that v0.13.0 hit." |
| 95 | + ); |
| 96 | +} |
0 commit comments