1use crate::io::{HttpClient, HttpRequest, HttpResponse};
62use crate::tile_source::{
63 DecodedImage, TileData, TileDecoder, TileError, TileFreshness, TileResponse, TileSource,
64 RevalidationHint, 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
160 .pending
161 .lock()
162 .map(|p| p.len())
163 .unwrap_or(0);
164 f.debug_struct("HttpTileSource")
165 .field("url_template", &self.url_template)
166 .field("has_decoder", &self.decoder.is_some())
167 .field("default_headers", &self.default_headers.len())
168 .field("pending", &pending_count)
169 .finish()
170 }
171}
172
173impl HttpTileSource {
178 pub fn new(url_template: impl Into<String>, client: Box<dyn HttpClient>) -> Self {
184 Self {
185 url_template: url_template.into(),
186 client,
187 decoder: None,
188 default_headers: Vec::new(),
189 pending: Mutex::new(HashMap::new()),
190 failure_diagnostics: Mutex::new(TileSourceFailureDiagnostics::default()),
191 }
192 }
193
194 pub fn with_decoder(
197 url_template: impl Into<String>,
198 client: Box<dyn HttpClient>,
199 decoder: Box<dyn TileDecoder>,
200
201 ) -> Self {
202 Self {
203 url_template: url_template.into(),
204 client,
205 decoder: Some(decoder),
206 default_headers: Vec::new(),
207 pending: Mutex::new(HashMap::new()),
208 failure_diagnostics: Mutex::new(TileSourceFailureDiagnostics::default()),
209 }
210 }
211
212 pub fn with_header(
227 mut self,
228 name: impl Into<String>,
229 value: impl Into<String>,
230 ) -> Self {
231 self.default_headers.push((name.into(), value.into()));
232 self
233 }
234
235 pub fn tile_url(&self, id: &TileId) -> String {
242 self.url_template
243 .replace("{z}", &id.zoom.to_string())
244 .replace("{x}", &id.x.to_string())
245 .replace("{y}", &id.y.to_string())
246 }
247
248 #[inline]
250 pub fn url_template(&self) -> &str {
251 &self.url_template
252 }
253
254 pub fn pending_count(&self) -> usize {
256 self.pending.lock().map(|p| p.len()).unwrap_or(0)
257 }
258}
259
260impl TileSource for HttpTileSource {
265 fn request(&self, id: TileId) {
266 let url = self.tile_url(&id);
267
268 if let Ok(mut pending) = self.pending.lock() {
271 pending.insert(url.clone(), id);
272 }
273
274 let mut req = HttpRequest::get(&url);
276 for (name, value) in &self.default_headers {
277 req = req.with_header(name.clone(), value.clone());
278 }
279
280 self.client.send(req);
281 }
282
283 fn request_revalidate(&self, id: TileId, hint: RevalidationHint) {
284 let url = self.tile_url(&id);
285
286 if let Ok(mut pending) = self.pending.lock() {
287 pending.insert(url.clone(), id);
288 }
289
290 let mut req = HttpRequest::get(&url);
291 for (name, value) in &self.default_headers {
292 req = req.with_header(name.clone(), value.clone());
293 }
294
295 if let Some(etag) = &hint.etag {
297 req = req.with_header("If-None-Match", etag.clone());
298 }
299 if let Some(last_modified) = &hint.last_modified {
300 req = req.with_header("If-Modified-Since", last_modified.clone());
301 }
302
303 self.client.send(req);
304 }
305
306 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
307 let responses = self.client.poll();
308 if responses.is_empty() {
309 return Vec::new();
310 }
311
312 let mut pending = match self.pending.lock() {
313 Ok(p) => p,
314 Err(_) => return Vec::new(),
315 };
316
317 let mut results = Vec::with_capacity(responses.len());
318
319 for (url, response) in responses {
320 let tile_id = match pending.remove(&url) {
321 Some(id) => id,
322 None => {
323 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
324 diagnostics.ignored_completed_responses += 1;
325 }
326 continue;
327 }
328 };
329
330 match response {
331 Ok(resp) if resp.status == 304 => {
332 let freshness = parse_http_freshness(&resp);
336 results.push((tile_id, Ok(TileResponse::not_modified(freshness))));
337 }
338 Ok(resp) if resp.is_success() => {
339 let freshness = parse_http_freshness(&resp);
340 let tile_result = if let Some(ref decoder) = self.decoder {
341 decoder
342 .decode(&resp.body)
343 .map(TileData::Raster)
344 .map(|data| TileResponse {
345 data,
346 freshness,
347 not_modified: false,
348 })
349 } else {
350 Ok(TileResponse {
351 data: TileData::Raster(DecodedImage {
352 width: 256,
353 height: 256,
354 data: std::sync::Arc::new(resp.body),
355 }),
356 freshness,
357 not_modified: false,
358 })
359 };
360 if tile_result.is_err() {
361 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
362 diagnostics.decode_failures += 1;
363 }
364 }
365 results.push((tile_id, tile_result));
366 }
367 Ok(resp) if resp.status == 404 => {
368 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
369 diagnostics.not_found_failures += 1;
370 }
371 results.push((tile_id, Err(TileError::NotFound(tile_id))));
372 }
373 Ok(resp) => {
374 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
375 diagnostics.http_status_failures += 1;
376 }
377 results.push((
378 tile_id,
379 Err(TileError::Network(format!("HTTP {}", resp.status))),
380 ));
381 }
382 Err(err) => {
383 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
384 diagnostics.transport_failures += 1;
385 if is_timeout_error(&err) {
386 diagnostics.timeout_failures += 1;
387 }
388 }
389 results.push((tile_id, Err(TileError::Network(err))));
390 }
391 }
392 }
393
394 results
395 }
396
397 fn cancel(&self, id: TileId) {
398 if let Ok(mut pending) = self.pending.lock() {
403 let url = self.tile_url(&id);
404 if pending.remove(&url).is_some() {
405 if let Ok(mut diagnostics) = self.failure_diagnostics.lock() {
406 diagnostics.forced_cancellations += 1;
407 }
408 }
409 }
410 }
411
412 fn diagnostics(&self) -> Option<TileSourceDiagnostics> {
413 let failure_diagnostics = self
414 .failure_diagnostics
415 .lock()
416 .map(|diagnostics| diagnostics.clone())
417 .unwrap_or_default();
418
419 Some(TileSourceDiagnostics {
420 queued_requests: 0,
421 in_flight_requests: self.pending_count(),
422 known_requests: self.pending_count(),
423 cancelled_in_flight_requests: 0,
424 max_concurrent_requests: 0,
425 pending_decode_tasks: 0,
426 failure_diagnostics,
427 })
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::io::HttpResponse;
439 use std::sync::{Arc, Mutex as StdMutex};
440
441 struct FailingDecoder;
442
443 impl TileDecoder for FailingDecoder {
444 fn decode(&self, _bytes: &[u8]) -> Result<DecodedImage, TileError> {
445 Err(TileError::Decode("bad image".into()))
446 }
447 }
448
449 struct MockHttpClientInner {
455 sent: StdMutex<Vec<HttpRequest>>,
456 responses: StdMutex<Vec<(String, Result<HttpResponse, String>)>>,
457 }
458
459 #[derive(Clone)]
466 struct MockHttpClient {
467 inner: Arc<MockHttpClientInner>,
468 }
469
470 impl MockHttpClient {
471 fn new() -> Self {
472 Self {
473 inner: Arc::new(MockHttpClientInner {
474 sent: StdMutex::new(Vec::new()),
475 responses: StdMutex::new(Vec::new()),
476 }),
477 }
478 }
479
480 fn queue_response(&self, url: &str, status: u16, body: Vec<u8>) {
482 self.inner.responses.lock().unwrap().push((
483 url.to_string(),
484 Ok(HttpResponse {
485 status,
486 body,
487 headers: vec![],
488 }),
489 ));
490 }
491
492 fn queue_response_with_headers(
494 &self,
495 url: &str,
496 status: u16,
497 body: Vec<u8>,
498 headers: Vec<(String, String)>,
499 ) {
500 self.inner.responses.lock().unwrap().push((
501 url.to_string(),
502 Ok(HttpResponse {
503 status,
504 body,
505 headers,
506 }),
507 ));
508 }
509
510 fn queue_error(&self, url: &str, error: &str) {
512 self.inner
513 .responses
514 .lock()
515 .unwrap()
516 .push((url.to_string(), Err(error.to_string())));
517 }
518
519 fn sent_urls(&self) -> Vec<String> {
521 self.inner
522 .sent
523 .lock()
524 .unwrap()
525 .iter()
526 .map(|r| r.url.clone())
527 .collect()
528 }
529
530 fn last_sent_headers(&self) -> Vec<(String, String)> {
532 self.inner
533 .sent
534 .lock()
535 .unwrap()
536 .last()
537 .map(|r| r.headers.clone())
538 .unwrap_or_default()
539 }
540 }
541
542 impl HttpClient for MockHttpClient {
543 fn send(&self, request: HttpRequest) {
544 self.inner.sent.lock().unwrap().push(request);
545 }
546
547 fn poll(&self) -> Vec<(String, Result<HttpResponse, String>)> {
548 std::mem::take(&mut *self.inner.responses.lock().unwrap())
549 }
550 }
551
552 struct MockDecoder;
554
555 impl TileDecoder for MockDecoder {
556 fn decode(&self, _bytes: &[u8]) -> Result<DecodedImage, TileError> {
557 Ok(DecodedImage {
558 width: 2,
559 height: 2,
560 data: std::sync::Arc::new(vec![255u8; 2 * 2 * 4]),
561 })
562 }
563 }
564
565 const TEMPLATE: &str = "https://tiles.example.com/{z}/{x}/{y}.png";
570
571 fn tile(z: u8, x: u32, y: u32) -> TileId {
572 TileId::new(z, x, y)
573 }
574
575 #[test]
580 fn tile_url_substitution() {
581 let client = MockHttpClient::new();
582 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
583 let url = source.tile_url(&tile(10, 512, 340));
584 assert_eq!(url, "https://tiles.example.com/10/512/340.png");
585 }
586
587 #[test]
588 fn tile_url_zoom_zero() {
589 let client = MockHttpClient::new();
590 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
591 let url = source.tile_url(&tile(0, 0, 0));
592 assert_eq!(url, "https://tiles.example.com/0/0/0.png");
593 }
594
595 #[test]
596 fn url_template_accessor() {
597 let client = MockHttpClient::new();
598 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
599 assert_eq!(source.url_template(), TEMPLATE);
600 }
601
602 #[test]
607 fn request_sends_http_get() {
608 let client = MockHttpClient::new();
609 let handle = client.clone();
610 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
611
612 source.request(tile(5, 10, 20));
613
614 let urls = handle.sent_urls();
615 assert_eq!(urls.len(), 1);
616 assert_eq!(urls[0], "https://tiles.example.com/5/10/20.png");
617 }
618
619 #[test]
620 fn poll_returns_empty_when_no_responses() {
621 let client = MockHttpClient::new();
622 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
623 let results = source.poll();
624 assert!(results.is_empty());
625 }
626
627 #[test]
628 fn successful_fetch_without_decoder() {
629 let client = MockHttpClient::new();
630 let url = "https://tiles.example.com/0/0/0.png";
631 let body = vec![0u8; 256 * 256 * 4];
632 client.queue_response(url, 200, body.clone());
633
634 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
635 source.request(tile(0, 0, 0));
636
637 let results = source.poll();
638 assert_eq!(results.len(), 1);
639 let (id, result) = &results[0];
640 assert_eq!(*id, tile(0, 0, 0));
641 let data = result.as_ref().expect("should succeed");
642 match &data.data {
643 TileData::Raster(img) => {
644 assert_eq!(img.width, 256);
645 assert_eq!(img.height, 256);
646 assert_eq!(img.data.len(), 256 * 256 * 4);
647 }
648 TileData::Vector(_) | TileData::RawVector(_) => panic!("expected raster tile data"),
649 }
650 }
651
652 #[test]
653 fn successful_fetch_with_decoder() {
654 let client = MockHttpClient::new();
655 let url = "https://tiles.example.com/0/0/0.png";
656 client.queue_response(url, 200, vec![1, 2, 3]);
657
658 let source = HttpTileSource::with_decoder(
659 TEMPLATE,
660 Box::new(client),
661 Box::new(MockDecoder),
662 );
663 source.request(tile(0, 0, 0));
664
665 let results = source.poll();
666 assert_eq!(results.len(), 1);
667 let (_, result) = &results[0];
668 match &result.as_ref().unwrap().data {
669 TileData::Raster(img) => {
670 assert_eq!(img.width, 2);
671 assert_eq!(img.height, 2);
672 }
673 TileData::Vector(_) | TileData::RawVector(_) => panic!("expected raster tile data"),
674 }
675 }
676
677 #[test]
678 fn cache_control_max_age_populates_freshness() {
679 let client = MockHttpClient::new();
680 let url = "https://tiles.example.com/0/0/0.png";
681 client.inner.responses.lock().unwrap().push((
682 url.to_string(),
683 Ok(HttpResponse {
684 status: 200,
685 body: vec![0u8; 256 * 256 * 4],
686 headers: vec![("Cache-Control".into(), "public, max-age=60".into())],
687 }),
688 ));
689
690 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
691 source.request(tile(0, 0, 0));
692
693 let results = source.poll();
694 let freshness = &results[0].1.as_ref().unwrap().freshness;
695 assert!(freshness.expires_at.is_some());
696 assert!(!freshness.is_expired());
697 }
698
699 #[test]
700 fn expires_header_populates_freshness() {
701 let client = MockHttpClient::new();
702 let url = "https://tiles.example.com/0/0/0.png";
703 client.inner.responses.lock().unwrap().push((
704 url.to_string(),
705 Ok(HttpResponse {
706 status: 200,
707 body: vec![0u8; 256 * 256 * 4],
708 headers: vec![("Expires".into(), "Wed, 21 Oct 2099 07:28:00 GMT".into())],
709 }),
710 ));
711
712 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
713 source.request(tile(0, 0, 0));
714
715 let results = source.poll();
716 let freshness = &results[0].1.as_ref().unwrap().freshness;
717 assert!(freshness.expires_at.is_some());
718 }
719
720 #[test]
725 fn http_404_returns_not_found() {
726 let client = MockHttpClient::new();
727 let url = "https://tiles.example.com/0/0/0.png";
728 client.queue_response(url, 404, vec![]);
729
730 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
731 source.request(tile(0, 0, 0));
732
733 let results = source.poll();
734 assert_eq!(results.len(), 1);
735 assert!(matches!(results[0].1, Err(TileError::NotFound(_))));
736 }
737
738 #[test]
739 fn http_500_returns_network_error() {
740 let client = MockHttpClient::new();
741 let url = "https://tiles.example.com/0/0/0.png";
742 client.queue_response(url, 500, vec![]);
743
744 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
745 source.request(tile(0, 0, 0));
746
747 let results = source.poll();
748 assert_eq!(results.len(), 1);
749 let (_, result) = &results[0];
750 match result {
751 Err(TileError::Network(msg)) => assert!(msg.contains("500")),
752 other => panic!("expected Network error, got {other:?}"),
753 }
754 }
755
756 #[test]
757 fn http_2xx_range_is_success() {
758 let client = MockHttpClient::new();
759 let url = "https://tiles.example.com/0/0/0.png";
760 client.queue_response(url, 204, vec![0u8; 256 * 256 * 4]);
761
762 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
763 source.request(tile(0, 0, 0));
764
765 let results = source.poll();
766 assert_eq!(results.len(), 1);
767 assert!(results[0].1.is_ok(), "2xx should be treated as success");
768 }
769
770 #[test]
771 fn transport_error_returns_network_error() {
772 let client = MockHttpClient::new();
773 let url = "https://tiles.example.com/0/0/0.png";
774 client.queue_error(url, "connection refused");
775
776 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
777 source.request(tile(0, 0, 0));
778
779 let results = source.poll();
780 assert_eq!(results.len(), 1);
781 match &results[0].1 {
782 Err(TileError::Network(msg)) => assert!(msg.contains("connection refused")),
783 other => panic!("expected Network error, got {other:?}"),
784 }
785 }
786
787 #[test]
792 fn default_headers_are_sent() {
793 let client = MockHttpClient::new();
794 let handle = client.clone();
795
796 let source = HttpTileSource::new(TEMPLATE, Box::new(client))
797 .with_header("Authorization", "Bearer test-key")
798 .with_header("User-Agent", "rustial/0.1");
799
800 source.request(tile(0, 0, 0));
801
802 let headers = handle.last_sent_headers();
803 assert_eq!(headers.len(), 2);
804 assert_eq!(headers[0].0, "Authorization");
805 assert_eq!(headers[0].1, "Bearer test-key");
806 assert_eq!(headers[1].0, "User-Agent");
807 assert_eq!(headers[1].1, "rustial/0.1");
808 }
809
810 #[test]
815 fn cancel_removes_pending_entry() {
816 let client = MockHttpClient::new();
817 let url = "https://tiles.example.com/0/0/0.png";
818 client.queue_response(url, 200, vec![0u8; 256 * 256 * 4]);
819
820 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
821 source.request(tile(0, 0, 0));
822 assert_eq!(source.pending_count(), 1);
823
824 source.cancel(tile(0, 0, 0));
825 assert_eq!(source.pending_count(), 0);
826
827 let results = source.poll();
828 assert!(results.is_empty(), "cancelled tile should not appear in poll results");
829 }
830
831 #[test]
836 fn pending_count_tracks_inflight() {
837 let client = MockHttpClient::new();
838 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
839
840 assert_eq!(source.pending_count(), 0);
841 source.request(tile(0, 0, 0));
842 assert_eq!(source.pending_count(), 1);
843 source.request(tile(1, 0, 0));
844 assert_eq!(source.pending_count(), 2);
845 }
846
847 #[test]
852 fn unknown_url_response_is_ignored() {
853 let client = MockHttpClient::new();
854 client.queue_response("https://other.example.com/tile.png", 200, vec![]);
855
856 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
857 let results = source.poll();
858 assert!(results.is_empty());
859 }
860
861 #[test]
866 fn debug_impl() {
867 let client = MockHttpClient::new();
868 let source = HttpTileSource::new(TEMPLATE, Box::new(client));
869 let dbg = format!("{source:?}");
870 assert!(dbg.contains("HttpTileSource"));
871 assert!(dbg.contains("url_template"));
872 }
873
874 #[test]
879 fn request_revalidate_sends_if_none_match_header() {
880 let client = MockHttpClient::new();
881 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
882 let id = TileId::new(1, 0, 0);
883
884 source.request_revalidate(
885 id,
886 RevalidationHint {
887 etag: Some("abc123".into()),
888 last_modified: None,
889 },
890 );
891
892 let headers = client.last_sent_headers();
893 let etag_header = headers.iter().find(|(k, _)| k == "If-None-Match");
894 assert!(etag_header.is_some(), "should include If-None-Match header");
895 assert_eq!(etag_header.unwrap().1, "abc123");
896 }
897
898 #[test]
899 fn request_revalidate_sends_if_modified_since_header() {
900 let client = MockHttpClient::new();
901 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
902 let id = TileId::new(1, 0, 0);
903
904 source.request_revalidate(
905 id,
906 RevalidationHint {
907 etag: None,
908 last_modified: Some("Wed, 01 Jan 2025 00:00:00 GMT".into()),
909 },
910 );
911
912 let headers = client.last_sent_headers();
913 let lm_header = headers.iter().find(|(k, _)| k == "If-Modified-Since");
914 assert!(lm_header.is_some(), "should include If-Modified-Since header");
915 assert_eq!(lm_header.unwrap().1, "Wed, 01 Jan 2025 00:00:00 GMT");
916 }
917
918 #[test]
919 fn poll_returns_not_modified_for_304_response() {
920 let client = MockHttpClient::new();
921 let source = HttpTileSource::new(TEMPLATE, Box::new(client.clone()));
922 let id = TileId::new(1, 0, 0);
923 let url = source.tile_url(&id);
924
925 source.request(id);
926
927 client.queue_response_with_headers(
928 &url,
929 304,
930 vec![],
931 vec![
932 ("cache-control".into(), "max-age=600".into()),
933 ("etag".into(), "new-etag".into()),
934 ],
935 );
936
937 let results = source.poll();
938 assert_eq!(results.len(), 1);
939 let (result_id, result) = &results[0];
940 assert_eq!(*result_id, id);
941 let response = result.as_ref().expect("should be Ok");
942 assert!(response.not_modified, "should be a not-modified response");
943 assert_eq!(response.freshness.etag.as_deref(), Some("new-etag"));
944 assert!(response.freshness.expires_at.is_some());
945 }
946
947 #[test]
948 fn diagnostics_count_categorized_failures() {
949 let client = MockHttpClient::new();
950 let source = HttpTileSource::with_decoder(
951 TEMPLATE,
952 Box::new(client.clone()),
953 Box::new(FailingDecoder),
954 );
955
956 let decode_tile = tile(3, 0, 0);
957 let timeout_tile = tile(3, 0, 1);
958 let status_tile = tile(3, 0, 2);
959 let not_found_tile = tile(3, 0, 3);
960 let cancelled_tile = tile(3, 0, 4);
961
962 for tile_id in [decode_tile, timeout_tile, status_tile, not_found_tile, cancelled_tile] {
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!(diagnostics.failure_diagnostics.ignored_completed_responses, 1);
984 }
985}