1use crate::io::{HttpClient, HttpRequest, HttpResponse};
62use crate::tile_source::{
63 DecodedImage, RevalidationHint, TileData, TileDecoder, TileError, TileFreshness, TileResponse,
64 TileSource, TileSourceDiagnostics, TileSourceFailureDiagnostics,
65};
66use rustial_math::TileId;
67use std::collections::HashMap;
68use std::sync::Mutex;
69use std::time::{Duration, SystemTime};
70
71fn parse_cache_control_max_age(value: &str) -> Option<u64> {
72 for directive in value.split(',') {
73 let directive = directive.trim();
74 if let Some(rest) = directive.strip_prefix("max-age=") {
75 if let Ok(seconds) = rest.trim_matches('"').parse::<u64>() {
76 return Some(seconds);
77 }
78 }
79 }
80 None
81}
82
83fn parse_age_seconds(response: &HttpResponse) -> u64 {
84 response
85 .header("age")
86 .and_then(|value| value.parse::<u64>().ok())
87 .unwrap_or(0)
88}
89
90fn parse_http_freshness(response: &HttpResponse) -> TileFreshness {
91 let now = SystemTime::now();
92 let age = parse_age_seconds(response);
93
94 let expires_at = response
95 .header("cache-control")
96 .and_then(parse_cache_control_max_age)
97 .map(|max_age| max_age.saturating_sub(age))
98 .map(Duration::from_secs)
99 .and_then(|ttl| now.checked_add(ttl))
100 .or_else(|| {
101 response
102 .header("expires")
103 .and_then(|value| httpdate::parse_http_date(value).ok())
104 });
105
106 TileFreshness {
107 expires_at,
108 etag: response.header("etag").map(ToOwned::to_owned),
109 last_modified: response.header("last-modified").map(ToOwned::to_owned),
110 }
111}
112
113fn is_timeout_error(error: &str) -> bool {
114 error.to_ascii_lowercase().contains("timeout")
115}
116
117pub struct HttpTileSource {
129 url_template: String,
131
132 client: Box<dyn HttpClient>,
134
135 decoder: Option<Box<dyn TileDecoder>>,
140
141 default_headers: Vec<(String, String)>,
145
146 pending: Mutex<HashMap<String, TileId>>,
153 failure_diagnostics: Mutex<TileSourceFailureDiagnostics>,
155}
156
157impl std::fmt::Debug for HttpTileSource {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 let pending_count = self.pending.lock().map(|p| p.len()).unwrap_or(0);
160 f.debug_struct("HttpTileSource")
161 .field("url_template", &self.url_template)
162 .field("has_decoder", &self.decoder.is_some())
163 .field("default_headers", &self.default_headers.len())
164 .field("pending", &pending_count)
165 .finish()
166 }
167}
168
169impl HttpTileSource {
174 pub fn new(url_template: impl Into<String>, client: Box<dyn HttpClient>) -> Self {
180 Self {
181 url_template: url_template.into(),
182 client,
183 decoder: None,
184 default_headers: Vec::new(),
185 pending: Mutex::new(HashMap::new()),
186 failure_diagnostics: Mutex::new(TileSourceFailureDiagnostics::default()),
187 }
188 }
189
190 pub fn with_decoder(
193 url_template: impl Into<String>,
194 client: Box<dyn HttpClient>,
195 decoder: Box<dyn TileDecoder>,
196 ) -> Self {
197 Self {
198 url_template: url_template.into(),
199 client,
200 decoder: Some(decoder),
201 default_headers: Vec::new(),
202 pending: Mutex::new(HashMap::new()),
203 failure_diagnostics: Mutex::new(TileSourceFailureDiagnostics::default()),
204 }
205 }
206
207 pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
222 self.default_headers.push((name.into(), value.into()));
223 self
224 }
225
226 pub fn tile_url(&self, id: &TileId) -> String {
233 self.url_template
234 .replace("{z}", &id.zoom.to_string())
235 .replace("{x}", &id.x.to_string())
236 .replace("{y}", &id.y.to_string())
237 }
238
239 #[inline]
241 pub fn url_template(&self) -> &str {
242 &self.url_template
243 }
244
245 pub fn pending_count(&self) -> usize {
247 self.pending.lock().map(|p| p.len()).unwrap_or(0)
248 }
249}
250
251impl TileSource for HttpTileSource {
256 fn request(&self, id: TileId) {
257 let url = self.tile_url(&id);
258
259 if let Ok(mut pending) = self.pending.lock() {
262 pending.insert(url.clone(), id);
263 }
264
265 let mut req = HttpRequest::get(&url);
267 for (name, value) in &self.default_headers {
268 req = req.with_header(name.clone(), value.clone());
269 }
270
271 self.client.send(req);
272 }
273
274 fn request_revalidate(&self, id: TileId, hint: RevalidationHint) {
275 let url = self.tile_url(&id);
276
277 if let Ok(mut pending) = self.pending.lock() {
278 pending.insert(url.clone(), id);
279 }
280
281 let mut req = HttpRequest::get(&url);
282 for (name, value) in &self.default_headers {
283 req = req.with_header(name.clone(), value.clone());
284 }
285
286 if let Some(etag) = &hint.etag {
288 req = req.with_header("If-None-Match", etag.clone());
289 }
290 if let Some(last_modified) = &hint.last_modified {
291 req = req.with_header("If-Modified-Since", last_modified.clone());
292 }
293
294 self.client.send(req);
295 }
296
297 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
298 let responses = self.client.poll();
299 if responses.is_empty() {
300 return Vec::new();
301 }
302
303 let mut pending = match self.pending.lock() {
304 Ok(p) => p,
305 Err(_) => return Vec::new(),
306 };
307
308 let mut results = Vec::with_capacity(responses.len());
309
310 for (url, response) in responses {
311 let tile_id = match pending.remove(&url) {
312 Some(id) => id,
313 None => {
314 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
315 diagnostics.ignored_completed_responses += 1;
316 }
317 continue;
318 }
319 };
320
321 match response {
322 Ok(resp) if resp.status == 304 => {
323 let freshness = parse_http_freshness(&resp);
327 results.push((tile_id, Ok(TileResponse::not_modified(freshness))));
328 }
329 Ok(resp) if resp.is_success() => {
330 let freshness = parse_http_freshness(&resp);
331 let tile_result = if let Some(ref decoder) = self.decoder {
332 decoder
333 .decode(&resp.body)
334 .map(TileData::Raster)
335 .map(|data| TileResponse {
336 data,
337 freshness,
338 not_modified: false,
339 })
340 } else {
341 Ok(TileResponse {
342 data: TileData::Raster(DecodedImage {
343 width: 256,
344 height: 256,
345 data: std::sync::Arc::new(resp.body),
346 }),
347 freshness,
348 not_modified: false,
349 })
350 };
351 if tile_result.is_err() {
352 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
353 diagnostics.decode_failures += 1;
354 }
355 }
356 results.push((tile_id, tile_result));
357 }
358 Ok(resp) if resp.status == 404 => {
359 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
360 diagnostics.not_found_failures += 1;
361 }
362 results.push((tile_id, Err(TileError::NotFound(tile_id))));
363 }
364 Ok(resp) => {
365 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
366 diagnostics.http_status_failures += 1;
367 }
368 results.push((
369 tile_id,
370 Err(TileError::Network(format!("HTTP {}", resp.status))),
371 ));
372 }
373 Err(err) => {
374 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
375 diagnostics.transport_failures += 1;
376 if is_timeout_error(&err) {
377 diagnostics.timeout_failures += 1;
378 }
379 }
380 results.push((tile_id, Err(TileError::Network(err))));
381 }
382 }
383 }
384
385 results
386 }
387
388 fn cancel(&self, id: TileId) {
389 if let Ok(mut pending) = self.pending.lock() {
394 let url = self.tile_url(&id);
395 if pending.remove(&url).is_some() {
396 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
397 diagnostics.forced_cancellations += 1;
398 }
399 }
400 }
401 }
402
403 fn diagnostics(&self) -> Option<TileSourceDiagnostics> {
404 let failure_diagnostics = self
405 .failure_diagnostics
406 .lock()
407 .map(|diagnostics| diagnostics.clone())
408 .unwrap_or_default();
409
410 Some(TileSourceDiagnostics {
411 queued_requests: 0,
412 in_flight_requests: self.pending_count(),
413 known_requests: self.pending_count(),
414 cancelled_in_flight_requests: 0,
415 max_concurrent_requests: 0,
416 pending_decode_tasks: 0,
417 failure_diagnostics,
418 })
419 }
420}
421
422#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::io::HttpResponse;
430 use std::sync::{Arc, Mutex as StdMutex};
431
432 struct FailingDecoder;
433
434 impl TileDecoder for FailingDecoder {
435 fn decode(&self, _bytes: &[u8]) -> Result<DecodedImage, TileError> {
436 Err(TileError::Decode("bad image".into()))
437 }
438 }
439
440 struct MockHttpClientInner {
446 sent: StdMutex<Vec<HttpRequest>>,
447 responses: StdMutex<Vec<(String, Result<HttpResponse, String>)>>,
448 }
449
450 #[derive(Clone)]
457 struct MockHttpClient {
458 inner: Arc<MockHttpClientInner>,
459 }
460
461 impl MockHttpClient {
462 fn new() -> Self {
463 Self {
464 inner: Arc::new(MockHttpClientInner {
465 sent: StdMutex::new(Vec::new()),
466 responses: StdMutex::new(Vec::new()),
467 }),
468 }
469 }
470
471 fn queue_response(&self, url: &str, status: u16, body: Vec<u8>) {
473 self.inner.responses.lock().unwrap().push((
474 url.to_string(),
475 Ok(HttpResponse {
476 status,
477 body,
478 headers: vec![],
479 }),
480 ));
481 }
482
483 fn queue_response_with_headers(
485 &self,
486 url: &str,
487 status: u16,
488 body: Vec<u8>,
489 headers: Vec<(String, String)>,
490 ) {
491 self.inner.responses.lock().unwrap().push((
492 url.to_string(),
493 Ok(HttpResponse {
494 status,
495 body,
496 headers,
497 }),
498 ));
499 }
500
501 fn queue_error(&self, url: &str, error: &str) {
503 self.inner
504 .responses
505 .lock()
506 .unwrap()
507 .push((url.to_string(), Err(error.to_string())));
508 }
509
510 fn sent_urls(&self) -> Vec<String> {
512 self.inner
513 .sent
514 .lock()
515 .unwrap()
516 .iter()
517 .map(|r| r.url.clone())
518 .collect()
519 }
520
521 fn last_sent_headers(&self) -> Vec<(String, String)> {
523 self.inner
524 .sent
525 .lock()
526 .unwrap()
527 .last()
528 .map(|r| r.headers.clone())
529 .unwrap_or_default()
530 }
531 }
532
533 impl HttpClient for MockHttpClient {
534 fn send(&self, request: HttpRequest) {
535 self.inner.sent.lock().unwrap().push(request);
536 }
537
538 fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
539 std::mem::take(&mut *self.inner.responses.lock().unwrap())
540 }
541 }
542
543 struct MockDecoder;
545
546 impl TileDecoder for MockDecoder {
547 fn decode(&self, _bytes: &[u8]) -> Result<DecodedImage, TileError> {
548 Ok(DecodedImage {
549 width: 2,
550 height: 2,
551 data: std::sync::Arc::new(vec![255u8; 2 * 2 * 4]),
552 })
553 }
554 }
555
556 const TEMPLATE: &str = "https://tiles.example.com/{z}/{x}/{y}.png";
561
562 fn tile(z: u8, x: u32, y: u32) -> TileId {
563 TileId::new(z, x, y)
564 }
565
566 #[test]
571 fn tile_url_substitution() {
572 let client = MockHttpClient::new();
573 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
574 let url = source.tile_url(&tile(10, 512, 340));
575 assert_eq!(url, "https://tiles.example.com/10/512/340.png");
576 }
577
578 #[test]
579 fn tile_url_zoom_zero() {
580 let client = MockHttpClient::new();
581 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
582 let url = source.tile_url(&tile(0, 0, 0));
583 assert_eq!(url, "https://tiles.example.com/0/0/0.png");
584 }
585
586 #[test]
587 fn url_template_accessor() {
588 let client = MockHttpClient::new();
589 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
590 assert_eq!(source.url_template(), TEMPLATE);
591 }
592
593 #[test]
598 fn request_sends_http_get() {
599 let client = MockHttpClient::new();
600 let handle = client.clone();
601 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
602
603 source.request(tile(5, 10, 20));
604
605 let urls = handle.sent_urls();
606 assert_eq!(urls.len(), 1);
607 assert_eq!(urls[0], "https://tiles.example.com/5/10/20.png");
608 }
609
610 #[test]
611 fn poll_returns_empty_when_no_responses() {
612 let client = MockHttpClient::new();
613 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
614 let results = source.poll();
615 assert!(results.is_empty());
616 }
617
618 #[test]
619 fn successful_fetch_without_decoder() {
620 let client = MockHttpClient::new();
621 let url = "https://tiles.example.com/0/0/0.png";
622 let body = vec![0u8; 256 * 256 * 4];
623 client.queue_response(url, 200, body.clone());
624
625 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
626 source.request(tile(0, 0, 0));
627
628 let results = source.poll();
629 assert_eq!(results.len(), 1);
630 let (id, result) = &results[0];
631 assert_eq!(*id, tile(0, 0, 0));
632 let data = result.as_ref().expect("should succeed");
633 match &data.data {
634 TileData::Raster(img) => {
635 assert_eq!(img.width, 256);
636 assert_eq!(img.height, 256);
637 assert_eq!(img.data.len(), 256 * 256 * 4);
638 }
639 TileData::Vector(_) | TileData::RawVector(_) => panic!("expected raster tile data"),
640 }
641 }
642
643 #[test]
644 fn successful_fetch_with_decoder() {
645 let client = MockHttpClient::new();
646 let url = "https://tiles.example.com/0/0/0.png";
647 client.queue_response(url, 200, vec![1, 2, 3]);
648
649 let source =
650 HttpTileSource::with_decoder(TEMPLATE, Box::new(client), Box::new(MockDecoder));
651 source.request(tile(0, 0, 0));
652
653 let results = source.poll();
654 assert_eq!(results.len(), 1);
655 let (_, result) = &results[0];
656 match &result.as_ref().unwrap().data {
657 TileData::Raster(img) => {
658 assert_eq!(img.width, 2);
659 assert_eq!(img.height, 2);
660 }
661 TileData::Vector(_) | TileData::RawVector(_) => panic!("expected raster tile data"),
662 }
663 }
664
665 #[test]
666 fn cache_control_max_age_populates_freshness() {
667 let client = MockHttpClient::new();
668 let url = "https://tiles.example.com/0/0/0.png";
669 client.inner.responses.lock().unwrap().push((
670 url.to_string(),
671 Ok(HttpResponse {
672 status: 200,
673 body: vec![0u8; 256 * 256 * 4],
674 headers: vec![("Cache-Control".into(), "public, max-age=60".into())],
675 }),
676 ));
677
678 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
679 source.request(tile(0, 0, 0));
680
681 let results = source.poll();
682 let freshness = &results[0].1.as_ref().unwrap().freshness;
683 assert!(freshness.expires_at.is_some());
684 assert!(!freshness.is_expired());
685 }
686
687 #[test]
688 fn expires_header_populates_freshness() {
689 let client = MockHttpClient::new();
690 let url = "https://tiles.example.com/0/0/0.png";
691 client.inner.responses.lock().unwrap().push((
692 url.to_string(),
693 Ok(HttpResponse {
694 status: 200,
695 body: vec![0u8; 256 * 256 * 4],
696 headers: vec![("Expires".into(), "Wed, 21 Oct 2099 07:28:00 GMT".into())],
697 }),
698 ));
699
700 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
701 source.request(tile(0, 0, 0));
702
703 let results = source.poll();
704 let freshness = &results[0].1.as_ref().unwrap().freshness;
705 assert!(freshness.expires_at.is_some());
706 }
707
708 #[test]
713 fn http_404_returns_not_found() {
714 let client = MockHttpClient::new();
715 let url = "https://tiles.example.com/0/0/0.png";
716 client.queue_response(url, 404, vec![]);
717
718 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
719 source.request(tile(0, 0, 0));
720
721 let results = source.poll();
722 assert_eq!(results.len(), 1);
723 assert!(matches!(results[0].1, Err(TileError::NotFound(_))));
724 }
725
726 #[test]
727 fn http_500_returns_network_error() {
728 let client = MockHttpClient::new();
729 let url = "https://tiles.example.com/0/0/0.png";
730 client.queue_response(url, 500, vec![]);
731
732 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
733 source.request(tile(0, 0, 0));
734
735 let results = source.poll();
736 assert_eq!(results.len(), 1);
737 let (_, result) = &results[0];
738 match result {
739 Err(TileError::Network(msg)) => assert!(msg.contains("500")),
740 other => panic!("expected Network error, got {other:?}"),
741 }
742 }
743
744 #[test]
745 fn http_2xx_range_is_success() {
746 let client = MockHttpClient::new();
747 let url = "https://tiles.example.com/0/0/0.png";
748 client.queue_response(url, 204, vec![0u8; 256 * 256 * 4]);
749
750 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
751 source.request(tile(0, 0, 0));
752
753 let results = source.poll();
754 assert_eq!(results.len(), 1);
755 assert!(results[0].1.is_ok(), "2xx should be treated as success");
756 }
757
758 #[test]
759 fn transport_error_returns_network_error() {
760 let client = MockHttpClient::new();
761 let url = "https://tiles.example.com/0/0/0.png";
762 client.queue_error(url, "connection refused");
763
764 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
765 source.request(tile(0, 0, 0));
766
767 let results = source.poll();
768 assert_eq!(results.len(), 1);
769 match &results[0].1 {
770 Err(TileError::Network(msg)) => assert!(msg.contains("connection refused")),
771 other => panic!("expected Network error, got {other:?}"),
772 }
773 }
774
775 #[test]
780 fn default_headers_are_sent() {
781 let client = MockHttpClient::new();
782 let handle = client.clone();
783
784 let source = HttpTileSource::new(TEMPLATE, Box::new(client))
785 .with_header("Authorization", "Bearer test-key")
786 .with_header("User-Agent", "rustial/0.1");
787
788 source.request(tile(0, 0, 0));
789
790 let headers = handle.last_sent_headers();
791 assert_eq!(headers.len(), 2);
792 assert_eq!(headers[0].0, "Authorization");
793 assert_eq!(headers[0].1, "Bearer test-key");
794 assert_eq!(headers[1].0, "User-Agent");
795 assert_eq!(headers[1].1, "rustial/0.1");
796 }
797
798 #[test]
803 fn cancel_removes_pending_entry() {
804 let client = MockHttpClient::new();
805 let url = "https://tiles.example.com/0/0/0.png";
806 client.queue_response(url, 200, vec![0u8; 256 * 256 * 4]);
807
808 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
809 source.request(tile(0, 0, 0));
810 assert_eq!(source.pending_count(), 1);
811
812 source.cancel(tile(0, 0, 0));
813 assert_eq!(source.pending_count(), 0);
814
815 let results = source.poll();
816 assert!(
817 results.is_empty(),
818 "cancelled tile should not appear in poll results"
819 );
820 }
821
822 #[test]
827 fn pending_count_tracks_inflight() {
828 let client = MockHttpClient::new();
829 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
830
831 assert_eq!(source.pending_count(), 0);
832 source.request(tile(0, 0, 0));
833 assert_eq!(source.pending_count(), 1);
834 source.request(tile(1, 0, 0));
835 assert_eq!(source.pending_count(), 2);
836 }
837
838 #[test]
843 fn unknown_url_response_is_ignored() {
844 let client = MockHttpClient::new();
845 client.queue_response("https://other.example.com/tile.png", 200, vec![]);
846
847 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
848 let results = source.poll();
849 assert!(results.is_empty());
850 }
851
852 #[test]
857 fn debug_impl() {
858 let client = MockHttpClient::new();
859 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
860 let dbg = format!("{source:?}");
861 assert!(dbg.contains("HttpTileSource"));
862 assert!(dbg.contains("url_template"));
863 }
864
865 #[test]
870 fn request_revalidate_sends_if_none_match_header() {
871 let client = MockHttpClient::new();
872 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
873 let id = TileId::new(1, 0, 0);
874
875 source.request_revalidate(
876 id,
877 RevalidationHint {
878 etag: Some("abc123".into()),
879 last_modified: None,
880 },
881 );
882
883 let headers = client.last_sent_headers();
884 let etag_header = headers.iter().find(|(k, _)| k == "If-None-Match");
885 assert!(etag_header.is_some(), "should include If-None-Match header");
886 assert_eq!(etag_header.unwrap().1, "abc123");
887 }
888
889 #[test]
890 fn request_revalidate_sends_if_modified_since_header() {
891 let client = MockHttpClient::new();
892 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
893 let id = TileId::new(1, 0, 0);
894
895 source.request_revalidate(
896 id,
897 RevalidationHint {
898 etag: None,
899 last_modified: Some("Wed, 01 Jan 2025 00:00:00 GMT".into()),
900 },
901 );
902
903 let headers = client.last_sent_headers();
904 let lm_header = headers.iter().find(|(k, _)| k == "If-Modified-Since");
905 assert!(
906 lm_header.is_some(),
907 "should include If-Modified-Since header"
908 );
909 assert_eq!(lm_header.unwrap().1, "Wed, 01 Jan 2025 00:00:00 GMT");
910 }
911
912 #[test]
913 fn poll_returns_not_modified_for_304_response() {
914 let client = MockHttpClient::new();
915 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
916 let id = TileId::new(1, 0, 0);
917 let url = source.tile_url(&id);
918
919 source.request(id);
920
921 client.queue_response_with_headers(
922 &url,
923 304,
924 vec![],
925 vec![
926 ("cache-control".into(), "max-age=600".into()),
927 ("etag".into(), "new-etag".into()),
928 ],
929 );
930
931 let results = source.poll();
932 assert_eq!(results.len(), 1);
933 let (result_id, result) = &results[0];
934 assert_eq!(*result_id, id);
935 let response = result.as_ref().expect("should be Ok");
936 assert!(response.not_modified, "should be a not-modified response");
937 assert_eq!(response.freshness.etag.as_deref(), Some("new-etag"));
938 assert!(response.freshness.expires_at.is_some());
939 }
940
941 #[test]
942 fn diagnostics_count_categorized_failures() {
943 let client = MockHttpClient::new();
944 let source = HttpTileSource::with_decoder(
945 TEMPLATE,
946 Box::new(client.clone()),
947 Box::new(FailingDecoder),
948 );
949
950 let decode_tile = tile(3, 0, 0);
951 let timeout_tile = tile(3, 0, 1);
952 let status_tile = tile(3, 0, 2);
953 let not_found_tile = tile(3, 0, 3);
954 let cancelled_tile = tile(3, 0, 4);
955
956 for tile_id in [
957 decode_tile,
958 timeout_tile,
959 status_tile,
960 not_found_tile,
961 cancelled_tile,
962 ] {
963 source.request(tile_id);
964 }
965
966 client.queue_response(&source.tile_url(&decode_tile), 200, vec![1, 2, 3]);
967 client.queue_error(&source.tile_url(&timeout_tile), "request timeout");
968 client.queue_response(&source.tile_url(&status_tile), 500, Vec::new());
969 client.queue_response(&source.tile_url(¬_found_tile), 404, Vec::new());
970
971 source.cancel(cancelled_tile);
972 client.queue_response(&source.tile_url(&cancelled_tile), 200, vec![0; 4]);
973
974 let _ = source.poll();
975 let diagnostics = source.diagnostics().expect("http source diagnostics");
976
977 assert_eq!(diagnostics.failure_diagnostics.decode_failures, 1);
978 assert_eq!(diagnostics.failure_diagnostics.transport_failures, 1);
979 assert_eq!(diagnostics.failure_diagnostics.timeout_failures, 1);
980 assert_eq!(diagnostics.failure_diagnostics.http_status_failures, 1);
981 assert_eq!(diagnostics.failure_diagnostics.not_found_failures, 1);
982 assert_eq!(diagnostics.failure_diagnostics.forced_cancellations, 1);
983 assert_eq!(
984 diagnostics.failure_diagnostics.ignored_completed_responses,
985 1
986 );
987 }
988}