@@ -180,30 +180,63 @@ pub fn test_latency(client: &Client) -> f64 {
180180 let req_builder = client. get ( url) ;
181181
182182 let start = Instant :: now ( ) ;
183- let response = req_builder. send ( ) . expect ( "failed to get response" ) ;
183+ let response = match req_builder. send ( ) {
184+ Ok ( res) => res,
185+ Err ( e) => {
186+ log:: error!( "Failed to get response for latency test: {}" , e) ;
187+ return 0.0 ;
188+ }
189+ } ;
184190 let _status_code = response. status ( ) ;
185191 let duration = start. elapsed ( ) . as_secs_f64 ( ) * 1_000.0 ;
186192
187- let re = Regex :: new ( r"cfRequestDuration;dur=([\d.]+)" ) . unwrap ( ) ;
188- let cf_req_duration: f64 = re
189- . captures (
190- response
191- . headers ( )
192- . get ( "Server-Timing" )
193- . expect ( "No Server-Timing in response header" )
194- . to_str ( )
195- . unwrap ( ) ,
196- )
197- . unwrap ( )
198- . get ( 1 )
199- . unwrap ( )
200- . as_str ( )
201- . parse ( )
202- . unwrap ( ) ;
193+ // Try to extract cfRequestDuration from Server-Timing header
194+ let cf_req_duration = match response. headers ( ) . get ( "Server-Timing" ) {
195+ Some ( header_value) => match header_value. to_str ( ) {
196+ Ok ( header_str) => {
197+ let re = match Regex :: new ( r"cfRequestDuration;dur=([\d.]+)" ) {
198+ Ok ( re) => re,
199+ Err ( e) => {
200+ log:: error!( "Failed to compile regex: {}" , e) ;
201+ return duration; // Return full duration if we can't parse server timing
202+ }
203+ } ;
204+
205+ match re. captures ( header_str) {
206+ Some ( captures) => match captures. get ( 1 ) {
207+ Some ( dur_match) => match dur_match. as_str ( ) . parse :: < f64 > ( ) {
208+ Ok ( parsed) => parsed,
209+ Err ( e) => {
210+ log:: error!( "Failed to parse cfRequestDuration: {}" , e) ;
211+ return duration;
212+ }
213+ } ,
214+ None => {
215+ log:: debug!( "No cfRequestDuration found in Server-Timing header" ) ;
216+ return duration;
217+ }
218+ } ,
219+ None => {
220+ log:: debug!( "Server-Timing header doesn't match expected format" ) ;
221+ return duration;
222+ }
223+ }
224+ }
225+ Err ( e) => {
226+ log:: error!( "Failed to convert Server-Timing header to string: {}" , e) ;
227+ return duration;
228+ }
229+ } ,
230+ None => {
231+ log:: debug!( "No Server-Timing header in response" ) ;
232+ return duration;
233+ }
234+ } ;
235+
203236 let mut req_latency = duration - cf_req_duration;
204237 if req_latency < 0.0 {
205- // TODO investigate negative latency values
206- req_latency = 0.0
238+ log :: warn! ( "Negative latency calculated: {req_latency}ms, using 0.0ms instead" ) ;
239+ req_latency = 0.0 ;
207240 }
208241 req_latency
209242}
@@ -261,14 +294,19 @@ pub fn test_upload(client: &Client, payload_size_bytes: usize, output_format: Ou
261294 let url = & format ! ( "{BASE_URL}/{UPLOAD_URL}" ) ;
262295 let payload: Vec < u8 > = vec ! [ 1 ; payload_size_bytes] ;
263296 let req_builder = client. post ( url) . body ( payload) ;
264- let ( status_code, mbits, duration) = {
265- let start = Instant :: now ( ) ;
266- let response = req_builder. send ( ) . expect ( "failed to get response" ) ;
267- let status_code = response. status ( ) ;
268- let duration = start. elapsed ( ) ;
269- let mbits = ( payload_size_bytes as f64 * 8.0 / 1_000_000.0 ) / duration. as_secs_f64 ( ) ;
270- ( status_code, mbits, duration)
297+
298+ let start = Instant :: now ( ) ;
299+ let response = match req_builder. send ( ) {
300+ Ok ( res) => res,
301+ Err ( e) => {
302+ log:: error!( "Failed to send upload request: {}" , e) ;
303+ return 0.0 ;
304+ }
271305 } ;
306+ let status_code = response. status ( ) ;
307+ let duration = start. elapsed ( ) ;
308+ let mbits = ( payload_size_bytes as f64 * 8.0 / 1_000_000.0 ) / duration. as_secs_f64 ( ) ;
309+
272310 if output_format == OutputFormat :: StdOut {
273311 print_current_speed ( mbits, duration, status_code, payload_size_bytes) ;
274312 }
@@ -282,15 +320,20 @@ pub fn test_download(
282320) -> f64 {
283321 let url = & format ! ( "{BASE_URL}/{DOWNLOAD_URL}{payload_size_bytes}" ) ;
284322 let req_builder = client. get ( url) ;
285- let ( status_code , mbits , duration ) = {
286- let start = Instant :: now ( ) ;
287- let response = req_builder. send ( ) . expect ( "failed to get response" ) ;
288- let status_code = response . status ( ) ;
289- let _res_bytes = response . bytes ( ) ;
290- let duration = start . elapsed ( ) ;
291- let mbits = ( payload_size_bytes as f64 * 8.0 / 1_000_000.0 ) / duration . as_secs_f64 ( ) ;
292- ( status_code , mbits , duration )
323+
324+ let start = Instant :: now ( ) ;
325+ let response = match req_builder. send ( ) {
326+ Ok ( res ) => res ,
327+ Err ( e ) => {
328+ log :: error! ( "Failed to send download request: {}" , e ) ;
329+ return 0.0 ;
330+ }
293331 } ;
332+ let status_code = response. status ( ) ;
333+ let _res_bytes = response. bytes ( ) ;
334+ let duration = start. elapsed ( ) ;
335+ let mbits = ( payload_size_bytes as f64 * 8.0 / 1_000_000.0 ) / duration. as_secs_f64 ( ) ;
336+
294337 if output_format == OutputFormat :: StdOut {
295338 print_current_speed ( mbits, duration, status_code, payload_size_bytes) ;
296339 }
@@ -493,4 +536,88 @@ mod tests {
493536 let result = fetch_metadata ( & client) ;
494537 assert ! ( result. is_err( ) ) ;
495538 }
539+
540+ #[ test]
541+ fn test_test_latency_with_mock_client ( ) {
542+ // This test verifies that test_latency handles errors gracefully
543+ // We can't easily mock a failing client in this test setup,
544+ // but we can verify the function doesn't panic
545+ use std:: time:: Duration ;
546+
547+ let client = reqwest:: blocking:: Client :: builder ( )
548+ . timeout ( Duration :: from_millis ( 1 ) )
549+ . build ( )
550+ . unwrap ( ) ;
551+
552+ // This should either return a value or timeout gracefully
553+ let result = test_latency ( & client) ;
554+ // The function should return some value (could be 0.0 if it fails)
555+ assert ! ( result >= 0.0 ) ;
556+ }
557+
558+ #[ test]
559+ fn test_test_upload_with_mock_client ( ) {
560+ // Test that test_upload handles errors gracefully
561+ use std:: time:: Duration ;
562+
563+ let client = reqwest:: blocking:: Client :: builder ( )
564+ . timeout ( Duration :: from_millis ( 1 ) )
565+ . build ( )
566+ . unwrap ( ) ;
567+
568+ // This should either return a value or handle timeout gracefully
569+ let result = test_upload ( & client, 1000 , OutputFormat :: None ) ;
570+ // The function should return some value (could be 0.0 if it fails)
571+ assert ! ( result >= 0.0 ) ;
572+ }
573+
574+ #[ test]
575+ fn test_test_download_with_mock_client ( ) {
576+ // Test that test_download handles errors gracefully
577+ use std:: time:: Duration ;
578+
579+ let client = reqwest:: blocking:: Client :: builder ( )
580+ . timeout ( Duration :: from_millis ( 1 ) )
581+ . build ( )
582+ . unwrap ( ) ;
583+
584+ // This should either return a value or handle timeout gracefully
585+ let result = test_download ( & client, 1000 , OutputFormat :: None ) ;
586+ // The function should return some value (could be 0.0 if it fails)
587+ assert ! ( result >= 0.0 ) ;
588+ }
589+
590+ #[ test]
591+ fn test_server_timing_header_parsing ( ) {
592+ // Test the Server-Timing header parsing logic
593+ // We'll test the regex and parsing separately since we can't easily mock responses
594+ use regex:: Regex ;
595+
596+ let re = Regex :: new ( r"cfRequestDuration;dur=([\d.]+)" ) . unwrap ( ) ;
597+
598+ // Test valid Server-Timing header
599+ let valid_header = "cfRequestDuration;dur=12.34" ;
600+ let captures = re. captures ( valid_header) . unwrap ( ) ;
601+ let dur_match = captures. get ( 1 ) . unwrap ( ) ;
602+ let parsed = dur_match. as_str ( ) . parse :: < f64 > ( ) . unwrap ( ) ;
603+ assert_eq ! ( parsed, 12.34 ) ;
604+
605+ // Test header with multiple values
606+ let multi_header = "cfRequestDuration;dur=56.78, other;dur=99.99" ;
607+ let captures = re. captures ( multi_header) . unwrap ( ) ;
608+ let dur_match = captures. get ( 1 ) . unwrap ( ) ;
609+ let parsed = dur_match. as_str ( ) . parse :: < f64 > ( ) . unwrap ( ) ;
610+ assert_eq ! ( parsed, 56.78 ) ;
611+
612+ // Test header without cfRequestDuration
613+ let no_cf_header = "other;dur=99.99" ;
614+ let captures = re. captures ( no_cf_header) ;
615+ assert ! ( captures. is_none( ) ) ;
616+
617+ // Test malformed duration - use a value that can't be parsed
618+ let malformed_header = "cfRequestDuration;dur=not-a-number" ;
619+ let captures = re. captures ( malformed_header) ;
620+ // This should not match the regex at all since it contains no digits
621+ assert ! ( captures. is_none( ) ) ;
622+ }
496623}
0 commit comments