@@ -2,13 +2,15 @@ use anyhow::{anyhow, Context, Result};
22use common:: { run_rathole_client, PING , PONG } ;
33use rand:: Rng ;
44use rand:: RngCore ;
5+ use std:: future:: Future ;
56use std:: net:: { IpAddr , Ipv4Addr , Ipv6Addr , SocketAddr } ;
6- use std:: path:: Path ;
7+ use std:: ops:: AsyncFnOnce ;
8+ use std:: path:: { Path , PathBuf } ;
79use std:: time:: Duration ;
810use tokio:: {
911 io:: { AsyncBufReadExt , AsyncReadExt , AsyncWriteExt , BufReader } ,
1012 net:: { TcpStream , UdpSocket } ,
11- sync:: broadcast,
13+ sync:: { broadcast, oneshot } ,
1214 task:: { JoinHandle , JoinSet } ,
1315 time,
1416} ;
@@ -32,8 +34,9 @@ const PINGPONG_SERVER_SOCKET_EXPOSED: &str = "/tmp/rathole_integration_test/ping
3234
3335const HITTER_NUM : usize = 4 ;
3436const TASK_SHUTDOWN_TIMEOUT : Duration = Duration :: from_secs ( 5 ) ;
37+ const INTEGRATION_SCENARIO_TIMEOUT : Duration = Duration :: from_secs ( 60 ) ;
3538#[ cfg( any( feature = "native-tls" , feature = "rustls" ) ) ]
36- const TLS_TEST_TIMEOUT : Duration = Duration :: from_secs ( 60 ) ;
39+ const TLS_SETUP_TIMEOUT : Duration = Duration :: from_secs ( 60 ) ;
3740
3841const PP2_SIG : [ u8 ; 12 ] = [
3942 0x0D , 0x0A , 0x0D , 0x0A , 0x00 , 0x0D , 0x0A , 0x51 , 0x55 , 0x49 , 0x54 , 0x0A ,
@@ -208,44 +211,71 @@ async fn socket_stream() -> Result<()> {
208211
209212#[ instrument]
210213async fn test ( config_path : impl AsRef < Path > + std:: fmt:: Debug , t : Type ) -> Result < ( ) > {
211- test_with_timeout ( config_path, t, None ) . await
212- }
213-
214- async fn test_with_timeout (
215- config_path : impl AsRef < Path > ,
216- t : Type ,
217- timeout : Option < Duration > ,
218- ) -> Result < ( ) > {
219214 if cfg ! ( not( all( feature = "client" , feature = "server" ) ) ) {
220- // Skip the test if the client or the server is not enabled
221215 return Ok ( ( ) ) ;
222216 }
223217
224- let mut processes = TestProcesses :: new ( config_path. as_ref ( ) . to_path_buf ( ) ) ;
225- let scenario = run_scenario ( & mut processes, t) ;
226- let scenario_result = match timeout {
227- Some ( timeout) => match time:: timeout ( timeout, scenario) . await {
228- Ok ( result) => result,
229- Err ( _) => Err ( anyhow ! (
230- "integration test timed out after {} seconds" ,
231- timeout. as_secs( )
232- ) ) ,
233- } ,
234- None => scenario. await ,
235- } ;
218+ run_managed_scenario (
219+ config_path. as_ref ( ) . to_path_buf ( ) ,
220+ "traffic scenario" ,
221+ INTEGRATION_SCENARIO_TIMEOUT ,
222+ async move |processes| run_scenario ( processes, t) . await ,
223+ )
224+ . await
225+ }
226+
227+ async fn run_managed_scenario (
228+ config_path : PathBuf ,
229+ phase : & ' static str ,
230+ timeout : Duration ,
231+ scenario : impl AsyncFnOnce ( & mut TestProcesses ) -> Result < ( ) > ,
232+ ) -> Result < ( ) > {
233+ let mut processes = TestProcesses :: new ( config_path. clone ( ) ) ;
234+ let scenario_result =
235+ run_with_timeout ( & config_path, phase, timeout, scenario ( & mut processes) ) . await ;
236236 let cleanup_result = processes. shutdown ( ) . await ;
237237
238- finish_scenario ( scenario_result, cleanup_result)
238+ finish_with_cleanup (
239+ scenario_result,
240+ cleanup_result,
241+ "integration-test process cleanup" ,
242+ )
243+ }
244+
245+ async fn run_with_timeout < T > (
246+ config_path : & Path ,
247+ phase : & str ,
248+ timeout : Duration ,
249+ operation : impl Future < Output = Result < T > > ,
250+ ) -> Result < T > {
251+ match time:: timeout ( timeout, operation) . await {
252+ Ok ( result) => result. with_context ( || {
253+ format ! (
254+ "integration test `{}` failed during {phase}" ,
255+ config_path. display( )
256+ )
257+ } ) ,
258+ Err ( _) => Err ( anyhow ! (
259+ "integration test `{}` timed out during {phase} after {timeout:?}" ,
260+ config_path. display( )
261+ ) ) ,
262+ }
239263}
240264
241- fn finish_scenario ( scenario_result : Result < ( ) > , cleanup_result : Result < ( ) > ) -> Result < ( ) > {
265+ fn finish_with_cleanup (
266+ scenario_result : Result < ( ) > ,
267+ cleanup_result : Result < ( ) > ,
268+ cleanup_name : & str ,
269+ ) -> Result < ( ) > {
242270 match ( scenario_result, cleanup_result) {
243271 ( Ok ( ( ) ) , Ok ( ( ) ) ) => Ok ( ( ) ) ,
244- ( Ok ( ( ) ) , Err ( cleanup_error) ) => Err ( cleanup_error) ,
272+ ( Ok ( ( ) ) , Err ( cleanup_error) ) => {
273+ Err ( cleanup_error) . with_context ( || format ! ( "{cleanup_name} failed" ) )
274+ }
245275 ( Err ( scenario_error) , Ok ( ( ) ) ) => Err ( scenario_error) ,
246- ( Err ( scenario_error) , Err ( cleanup_error) ) => Err ( scenario_error . context ( format ! (
247- "integration-test process cleanup also failed: {cleanup_error:#}"
248- ) ) ) ,
276+ ( Err ( scenario_error) , Err ( cleanup_error) ) => {
277+ Err ( scenario_error . context ( format ! ( "{cleanup_name} also failed: {cleanup_error:#}") ) )
278+ }
249279 }
250280}
251281
@@ -445,20 +475,38 @@ async fn stop_task(
445475 Err ( _) => {
446476 let task = task. take ( ) . expect ( "running task should exist" ) ;
447477 task. abort ( ) ;
448- let _ = task. await ;
449478 Err ( anyhow ! ( "{name} did not stop within {shutdown_timeout:?}" ) )
450479 }
451480 }
452481}
453482
483+ struct NotifyOnDrop ( Option < oneshot:: Sender < ( ) > > ) ;
484+
485+ impl Drop for NotifyOnDrop {
486+ fn drop ( & mut self ) {
487+ if let Some ( tx) = self . 0 . take ( ) {
488+ let _ = tx. send ( ( ) ) ;
489+ }
490+ }
491+ }
492+
454493#[ tokio:: test]
455494async fn stop_task_aborts_an_unresponsive_task ( ) {
456495 let ( shutdown_tx, mut shutdown_rx) = broadcast:: channel ( 1 ) ;
496+ let ( started_tx, started_rx) = oneshot:: channel ( ) ;
497+ let ( dropped_tx, dropped_rx) = oneshot:: channel ( ) ;
457498 let mut task = Some ( tokio:: spawn ( async move {
499+ let _notify_on_drop = NotifyOnDrop ( Some ( dropped_tx) ) ;
500+ let _ = started_tx. send ( ( ) ) ;
458501 let _ = shutdown_rx. recv ( ) . await ;
459502 std:: future:: pending :: < Result < ( ) > > ( ) . await
460503 } ) ) ;
461504
505+ time:: timeout ( Duration :: from_secs ( 1 ) , started_rx)
506+ . await
507+ . expect ( "test task should start promptly" )
508+ . expect ( "test task should report startup" ) ;
509+
462510 let result = stop_task (
463511 "test task" ,
464512 & shutdown_tx,
@@ -469,6 +517,10 @@ async fn stop_task_aborts_an_unresponsive_task() {
469517
470518 assert ! ( result. is_err( ) ) ;
471519 assert ! ( task. is_none( ) ) ;
520+ time:: timeout ( Duration :: from_secs ( 1 ) , dropped_rx)
521+ . await
522+ . expect ( "aborted task should be dropped promptly" )
523+ . expect ( "drop notification sender should survive until task cleanup" ) ;
472524}
473525
474526#[ tokio:: test]
@@ -499,11 +551,102 @@ async fn cancelling_stop_keeps_the_task_managed() {
499551 assert ! ( task. is_none( ) ) ;
500552}
501553
554+ #[ tokio:: test]
555+ async fn scenario_timeout_reports_phase_and_cleans_up_tasks ( ) {
556+ use std:: sync:: {
557+ atomic:: { AtomicUsize , Ordering } ,
558+ Arc ,
559+ } ;
560+
561+ let shutdown_count = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
562+ let scenario_shutdown_count = Arc :: clone ( & shutdown_count) ;
563+ let result = run_managed_scenario (
564+ PathBuf :: from ( "tests/stalled-traffic.toml" ) ,
565+ "stalled traffic scenario" ,
566+ Duration :: from_millis ( 10 ) ,
567+ async move |processes| {
568+ let mut client_shutdown_rx = processes. client_shutdown_tx . subscribe ( ) ;
569+ let client_shutdown_count = Arc :: clone ( & scenario_shutdown_count) ;
570+ processes. client = Some ( tokio:: spawn ( async move {
571+ // shutdown() sends twice through a one-slot channel. Production
572+ // shutdown receivers finish on either a value or a lag error.
573+ assert ! ( matches!(
574+ client_shutdown_rx. recv( ) . await ,
575+ Ok ( true ) | Err ( broadcast:: error:: RecvError :: Lagged ( _) )
576+ ) ) ;
577+ client_shutdown_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
578+ Ok ( ( ) )
579+ } ) ) ;
580+
581+ let mut server_shutdown_rx = processes. server_shutdown_tx . subscribe ( ) ;
582+ let server_shutdown_count = Arc :: clone ( & scenario_shutdown_count) ;
583+ processes. server = Some ( tokio:: spawn ( async move {
584+ // See the matching client task above.
585+ assert ! ( matches!(
586+ server_shutdown_rx. recv( ) . await ,
587+ Ok ( true ) | Err ( broadcast:: error:: RecvError :: Lagged ( _) )
588+ ) ) ;
589+ server_shutdown_count. fetch_add ( 1 , Ordering :: SeqCst ) ;
590+ Ok ( ( ) )
591+ } ) ) ;
592+
593+ std:: future:: pending :: < Result < ( ) > > ( ) . await
594+ } ,
595+ )
596+ . await ;
597+
598+ let error = format ! (
599+ "{:#}" ,
600+ result. expect_err( "stalled scenario should time out" )
601+ ) ;
602+ assert ! ( error. contains( "tests/stalled-traffic.toml" ) ) ;
603+ assert ! ( error. contains( "stalled traffic scenario" ) ) ;
604+ assert ! ( error. contains( "10ms" ) ) ;
605+ assert ! ( !error. contains( "cleanup" ) ) ;
606+ assert_eq ! ( shutdown_count. load( Ordering :: SeqCst ) , 2 ) ;
607+ }
608+
502609#[ cfg( any( feature = "native-tls" , feature = "rustls" ) ) ]
503610async fn test_tls ( config_template : impl AsRef < Path > , t : Type ) -> Result < ( ) > {
504- let config = common:: tls:: TlsTestConfig :: from_template ( config_template) ?;
505- test_with_timeout ( config. path ( ) , t, Some ( TLS_TEST_TIMEOUT ) ) . await ?;
506- config. close ( )
611+ if cfg ! ( not( all( feature = "client" , feature = "server" ) ) ) {
612+ return Ok ( ( ) ) ;
613+ }
614+
615+ let config = setup_tls_test_config ( config_template) . await ?;
616+ let scenario_result = test ( config. path ( ) , t) . await ;
617+ let cleanup_result = config. close ( ) ;
618+ finish_with_cleanup ( scenario_result, cleanup_result, "TLS artifact cleanup" )
619+ }
620+
621+ #[ cfg( any( feature = "native-tls" , feature = "rustls" ) ) ]
622+ async fn setup_tls_test_config (
623+ config_template : impl AsRef < Path > ,
624+ ) -> Result < common:: tls:: TlsTestConfig > {
625+ let template_path = config_template. as_ref ( ) . to_path_buf ( ) ;
626+ let generation_path = template_path. clone ( ) ;
627+ run_with_timeout (
628+ & template_path,
629+ "TLS artifact setup" ,
630+ TLS_SETUP_TIMEOUT ,
631+ generate_tls_test_config ( generation_path) ,
632+ )
633+ . await
634+ }
635+
636+ #[ cfg( any( feature = "native-tls" , feature = "rustls" ) ) ]
637+ async fn generate_tls_test_config ( template_path : PathBuf ) -> Result < common:: tls:: TlsTestConfig > {
638+ let ( result_tx, result_rx) = oneshot:: channel ( ) ;
639+ std:: thread:: Builder :: new ( )
640+ . name ( "rathole-tls-test-setup" . to_owned ( ) )
641+ . spawn ( move || {
642+ let result = common:: tls:: TlsTestConfig :: from_template ( template_path) ;
643+ let _ = result_tx. send ( result) ;
644+ } )
645+ . context ( "failed to spawn TLS artifact setup thread" ) ?;
646+
647+ result_rx
648+ . await
649+ . context ( "TLS artifact setup thread panicked" ) ?
507650}
508651
509652async fn echo_hitter ( addr : & ' static str , t : Type ) -> Result < ( ) > {
@@ -642,27 +785,29 @@ async fn test_proxy_protocol(config_path: &'static str) -> Result<()> {
642785 return Ok ( ( ) ) ;
643786 }
644787
645- let mut processes = TestProcesses :: new ( config_path. into ( ) ) ;
646- let scenario_result = async {
647- info ! ( "start the client" ) ;
648- processes. start_client ( ) ;
649- time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
650- processes. check_client_running ( ) . await ?;
651-
652- info ! ( "start the server" ) ;
653- processes. start_server ( ) ;
654- time:: sleep ( Duration :: from_millis ( 2500 ) ) . await ;
655- processes. check_running ( ) . await ?;
656-
657- info ! ( "echo" ) ;
658- tcp_echo_hitter_expect_proxy_protocol ( ECHO_SERVER_ADDR_EXPOSED ) . await ?;
659-
660- info ! ( "pingpong" ) ;
661- tcp_pingpong_hitter ( PINGPONG_SERVER_ADDR_EXPOSED ) . await
662- }
663- . await ;
664- let cleanup_result = processes. shutdown ( ) . await ;
665- finish_scenario ( scenario_result, cleanup_result)
788+ run_managed_scenario (
789+ config_path. into ( ) ,
790+ "proxy-protocol traffic scenario" ,
791+ INTEGRATION_SCENARIO_TIMEOUT ,
792+ async |processes| {
793+ info ! ( "start the client" ) ;
794+ processes. start_client ( ) ;
795+ time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
796+ processes. check_client_running ( ) . await ?;
797+
798+ info ! ( "start the server" ) ;
799+ processes. start_server ( ) ;
800+ time:: sleep ( Duration :: from_millis ( 2500 ) ) . await ;
801+ processes. check_running ( ) . await ?;
802+
803+ info ! ( "echo" ) ;
804+ tcp_echo_hitter_expect_proxy_protocol ( ECHO_SERVER_ADDR_EXPOSED ) . await ?;
805+
806+ info ! ( "pingpong" ) ;
807+ tcp_pingpong_hitter ( PINGPONG_SERVER_ADDR_EXPOSED ) . await
808+ } ,
809+ )
810+ . await
666811}
667812
668813async fn read_proxy_protocol_header (
0 commit comments