1use std::ops::Range;
2#[cfg(target_os = "linux")]
3use std::os::fd::AsRawFd;
4use std::path::Path;
5use std::sync::{Arc, LazyLock};
6
7use dio_align::DioAlign;
8use futures::{StreamExt, TryStreamExt};
9use polars_buffer::Buffer;
10use polars_config::FileAdvice;
11use polars_core::prelude::PlHashMap;
12use polars_core::runtime::ASYNC;
13use polars_error::{PolarsResult, feature_gated, polars_bail, polars_err};
14use polars_utils::aliases::InitHashMaps;
15use polars_utils::io::_limit_path_len_io_err;
16use polars_utils::mmap::MMapSemaphore;
17use polars_utils::pl_path::PlRefPath;
18use tokio::sync::Semaphore;
19
20use crate::cloud::concurrency_config::{ConcurrencyStrategy, FetchConfig};
21use crate::cloud::options::CloudOptions;
22#[cfg(feature = "cloud")]
23use crate::cloud::{
24 CloudLocation, ObjectStorePath, PolarsObjectStore, build_object_store, object_path_from_str,
25};
26use crate::metrics::{IOMetrics, OptIOMetrics};
27
28pub mod dio_align;
29#[cfg(target_os = "linux")]
30mod direct_io;
31#[allow(async_fn_in_trait)]
32pub trait ByteSource: Send + Sync {
33 async fn get_size(&self) -> PolarsResult<usize>;
34 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)>;
37 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>>;
40 async fn get_ranges(
42 &self,
43 ranges: &mut [Range<usize>],
44 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>>;
45}
46
47pub struct BufferByteSource(pub Buffer<u8>);
49
50impl BufferByteSource {
51 async fn try_new_mmap_from_path(
52 path: &Path,
53 _cloud_options: Option<&CloudOptions>,
54 ) -> PolarsResult<Self> {
55 let file = Arc::new(
56 tokio::fs::File::open(path)
57 .await
58 .map_err(|err| _limit_path_len_io_err(path, err))?
59 .into_std()
60 .await,
61 );
62
63 Ok(Self(Buffer::from_owner(MMapSemaphore::new_from_file(
64 &file,
65 )?)))
66 }
67}
68
69impl ByteSource for BufferByteSource {
70 async fn get_size(&self) -> PolarsResult<usize> {
71 Ok(self.0.as_ref().len())
72 }
73
74 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
75 let len = self.0.as_ref().len();
76 Ok((self.0.clone().sliced(len.saturating_sub(n)..len), len))
77 }
78
79 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
80 let out = self.0.clone().sliced(range);
81 Ok(out)
82 }
83
84 async fn get_ranges(
85 &self,
86 ranges: &mut [Range<usize>],
87 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
88 Ok(ranges
89 .iter()
90 .map(|x| (x.start, self.0.clone().sliced(x.clone())))
91 .collect())
92 }
93}
94
95pub struct FileByteSource {
97 file: Arc<std::fs::File>,
98 o_direct_align: Option<DioAlign>,
100 concurrency: usize,
102 permits: Arc<Semaphore>,
103 size: u64,
105 io_metrics: OptIOMetrics,
106}
107
108pub fn global_read_permits() -> Arc<Semaphore> {
110 static PERMITS: LazyLock<Arc<Semaphore>> = LazyLock::new(|| {
111 Arc::new(Semaphore::new(
112 polars_config::config().file_read_concurrency().max(1) as usize,
113 ))
114 });
115
116 PERMITS.clone()
117}
118
119#[derive(Clone)]
120pub struct FileReadContext {
121 pub enable_o_direct: bool,
122 pub concurrency: usize,
123 pub permits: Arc<Semaphore>,
124 pub advice: FileAdvice,
126}
127
128impl std::fmt::Debug for FileReadContext {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("FileReadContext")
131 .field("available_permits", &self.permits.available_permits())
132 .field("enable_odirect", &self.enable_o_direct)
133 .field("concurrency", &self.concurrency)
134 .field("advice", &self.advice)
135 .finish()
136 }
137}
138
139#[cfg(target_os = "linux")]
140fn _fadvise(file: &std::fs::File, advice: FileAdvice) {
141 let advice = match advice {
142 FileAdvice::Normal => return,
144 FileAdvice::Sequential => libc::POSIX_FADV_SEQUENTIAL,
145 FileAdvice::Random => libc::POSIX_FADV_RANDOM,
146 FileAdvice::WillNeed => libc::POSIX_FADV_WILLNEED,
147 };
148
149 unsafe {
150 libc::posix_fadvise(file.as_raw_fd(), 0, 0, advice);
151 }
152}
153
154#[cfg(all(unix, not(feature = "nightly")))]
155fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
156 use std::os::unix::fs::FileExt;
157 file.read_exact_at(buf, offset)
158}
159
160#[cfg(all(windows, not(feature = "nightly")))]
161fn pread_exact(file: &std::fs::File, buf: &mut [u8], offset: u64) -> std::io::Result<()> {
162 use std::os::windows::fs::FileExt;
163 let mut filled = 0;
164 while filled < buf.len() {
165 match file.seek_read(&mut buf[filled..], offset + filled as u64)? {
166 0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
167 n => filled += n,
168 }
169 }
170 Ok(())
171}
172
173#[cfg(all(feature = "nightly", unix))]
176fn pread_exact_uninit(
177 file: &std::fs::File,
178 buf: std::io::BorrowedCursor<'_, u8>,
179 offset: u64,
180) -> std::io::Result<()> {
181 use std::os::unix::fs::FileExt;
182 file.read_buf_exact_at(buf, offset)
183}
184
185#[cfg(all(feature = "nightly", windows))]
186fn pread_exact_uninit(
187 file: &std::fs::File,
188 mut buf: std::io::BorrowedCursor<'_, u8>,
189 mut offset: u64,
190) -> std::io::Result<()> {
191 use std::os::windows::fs::FileExt;
192 while buf.capacity() > 0 {
194 let written = buf.written();
195 file.seek_read_buf(buf.reborrow(), offset)?;
196 match buf.written() - written {
197 0 => return Err(std::io::ErrorKind::UnexpectedEof.into()),
198 n => offset += n as u64,
199 }
200 }
201 Ok(())
202}
203
204#[cfg(feature = "nightly")]
210fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
211 let mut v: Vec<u8> = Vec::with_capacity(len);
212
213 let filled = {
214 let mut buf = std::io::BorrowedBuf::from(&mut v.spare_capacity_mut()[..len]);
215 pread_exact_uninit(file, buf.unfilled(), offset)?;
216 buf.len()
217 };
218
219 debug_assert_eq!(filled, len);
220 unsafe { v.set_len(filled) };
223
224 Ok(Buffer::from(v))
225}
226
227#[cfg(not(feature = "nightly"))]
228fn read_buffered(file: &std::fs::File, offset: u64, len: usize) -> PolarsResult<Buffer<u8>> {
229 let mut buf = vec![0u8; len];
230 pread_exact(file, &mut buf, offset)?;
231 Ok(Buffer::from(buf))
232}
233
234#[cfg(target_os = "linux")]
237fn read_at(
238 file: &std::fs::File,
239 align: Option<DioAlign>,
240 offset: u64,
241 len: usize,
242 size: u64,
243) -> PolarsResult<Buffer<u8>> {
244 match align {
245 Some(align) => direct_io::read_aligned(file, align, offset, len, size),
246 None => read_buffered(file, offset, len),
247 }
248}
249
250#[cfg(not(target_os = "linux"))]
251fn read_at(
252 file: &std::fs::File,
253 _align: Option<DioAlign>,
254 offset: u64,
255 len: usize,
256 _size: u64,
257) -> PolarsResult<Buffer<u8>> {
258 read_buffered(file, offset, len)
259}
260
261impl FileByteSource {
262 async fn try_new_from_path(
263 path: PlRefPath,
264 read_context: FileReadContext,
265 io_metrics: Option<Arc<IOMetrics>>,
266 ) -> PolarsResult<Self> {
267 ASYNC
269 .spawn_blocking(move || Self::open_blocking(path, read_context, io_metrics))
270 .await
271 .expect("blocking task panicked")
272 }
273
274 fn open_blocking(
275 path: PlRefPath,
276 read_context: FileReadContext,
277 io_metrics: Option<Arc<IOMetrics>>,
278 ) -> PolarsResult<Self> {
279 let path = path.as_std_path();
280 let enable_o_direct = read_context.enable_o_direct;
281
282 let file = {
283 #[cfg(target_os = "linux")]
284 let f = if enable_o_direct {
285 direct_io::open_o_direct(path).or_else(|e| match e.raw_os_error() {
286 Some(libc::EINVAL) => std::fs::File::open(path),
288 _ => Err(e),
289 })
290 } else {
291 std::fs::File::open(path)
292 };
293 #[cfg(not(target_os = "linux"))]
294 let f = std::fs::File::open(path);
295
296 f.map_err(|e| _limit_path_len_io_err(path, e))?
297 };
298
299 let file = Arc::new(file);
300
301 #[cfg(target_os = "linux")]
302 let o_direct_align =
303 direct_io::probe_or_disable(&file).map_err(|e| _limit_path_len_io_err(path, e))?;
304
305 #[cfg(not(target_os = "linux"))]
306 let o_direct_align: Option<DioAlign> = None;
307
308 #[cfg(target_os = "linux")]
309 if o_direct_align.is_none() {
310 _fadvise(&file, read_context.advice);
312 }
313
314 let concurrency = read_context.concurrency;
315 let permits = read_context.permits;
316
317 let size = file
318 .metadata()
319 .map_err(|e| _limit_path_len_io_err(path, e))?
320 .len();
321
322 if polars_config::config().verbose() {
323 let name = if polars_config::config().verbose_sensitive() {
324 path.display().to_string()
325 } else {
326 "<file>".to_string()
327 };
328 match (enable_o_direct, o_direct_align) {
329 (true, Some(a)) => eprintln!(
330 "[FileByteSource]: {name}: direct IO active, \
331 alignment offset: {}, memory: {}",
332 a.offset, a.memory
333 ),
334 (true, None) => eprintln!(
335 "[FileByteSource]: {name}: direct IO requested but not active, \
336 using buffered reads"
337 ),
338 (false, _) => {},
339 }
340 }
341
342 Ok(FileByteSource {
343 file,
344 o_direct_align,
345 concurrency,
346 permits,
347 size,
348 io_metrics: OptIOMetrics(io_metrics),
349 })
350 }
351
352 pub fn try_new_from_std(
353 file: std::fs::File,
354 read_context: FileReadContext,
355 io_metrics: Option<Arc<IOMetrics>>,
356 ) -> PolarsResult<Self> {
357 let size = file.metadata()?.len();
358
359 #[cfg(target_os = "linux")]
360 let o_direct_align = direct_io::probe_or_disable(&file)?;
361
362 #[cfg(not(target_os = "linux"))]
363 let o_direct_align: Option<DioAlign> = None;
364
365 #[cfg(target_os = "linux")]
366 if o_direct_align.is_none() {
367 _fadvise(&file, read_context.advice);
368 }
369
370 let concurrency = read_context.concurrency;
371 let permits = read_context.permits;
372
373 let enable_o_direct = read_context.enable_o_direct;
375
376 if polars_config::config().verbose() {
377 match (enable_o_direct, o_direct_align) {
378 (true, Some(a)) => eprintln!(
379 "[FileByteSource]: direct IO active, alignment offset: {}, memory: {}",
380 a.offset, a.memory
381 ),
382 (true, None) => eprintln!(
383 "[FileByteSource]: direct IO requested but not active, using buffered reads",
384 ),
385 (false, _) => {},
386 }
387 }
388
389 Ok(Self {
390 file: Arc::new(file),
391 o_direct_align,
392 concurrency,
393 permits,
394 size,
395 io_metrics: OptIOMetrics(io_metrics),
396 })
397 }
398
399 pub fn set_io_metrics(&mut self, io_metrics: Option<Arc<IOMetrics>>) -> &mut Self {
400 self.io_metrics = OptIOMetrics(io_metrics);
401 self
402 }
403
404 pub fn io_metrics(&self) -> &OptIOMetrics {
405 &self.io_metrics
406 }
407}
408
409impl ByteSource for FileByteSource {
410 async fn get_size(&self) -> PolarsResult<usize> {
411 usize::try_from(self.size)
412 .map_err(|_| polars_err!(ComputeError: "file size {} does not fit in usize", self.size))
413 }
414
415 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
416 let size = self.get_size().await?;
417 let bytes = self.get_range(size.saturating_sub(n)..size).await?;
418 Ok((bytes, size))
419 }
420
421 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
422 assert!(range.end as u64 <= self.size);
423
424 let file = self.file.clone();
425 let offset = range.start as u64;
426 let len = range.len();
427 let size = self.size;
428 let o_direct = self.o_direct_align;
429
430 let permit = self.permits.clone().acquire_owned().await.unwrap();
431
432 self.io_metrics()
433 .record_io_read(len as u64, async move {
434 ASYNC
435 .spawn_blocking(move || {
436 let _permit = permit;
437 read_at(&file, o_direct, offset, len, size)
438 })
439 .await
440 })
441 .await
442 .expect("blocking task panicked")
443 }
444
445 async fn get_ranges(
446 &self,
447 ranges: &mut [Range<usize>],
448 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
449 if let [range] = ranges {
450 let mut out = PlHashMap::with_capacity(1);
451 out.insert(range.start, self.get_range(range.clone()).await?);
452 return Ok(out);
453 }
454
455 ranges.sort_unstable_by_key(|r| r.start);
456
457 let mut spans: Vec<Range<usize>> = Vec::with_capacity(ranges.len());
458
459 const MAX_SPAN: usize = 8 << 20;
461 const MAX_GAP: usize = 4096;
463
464 for r in ranges.iter() {
465 match spans.last_mut() {
466 Some(last)
467 if r.start.saturating_sub(last.end) <= MAX_GAP
468 && r.end.saturating_sub(last.start) <= MAX_SPAN =>
469 {
470 last.end = last.end.max(r.end)
471 },
472 _ => spans.push(r.clone()),
473 }
474 }
475
476 let mut fetched: Vec<(Range<usize>, Buffer<u8>)> = futures::stream::iter(spans)
477 .map(|span| async move {
478 let buf = self.get_range(span.clone()).await?;
479 PolarsResult::Ok((span, buf))
480 })
481 .buffer_unordered(self.concurrency)
482 .try_collect()
483 .await?;
484
485 fetched.sort_unstable_by_key(|(s, _)| s.start);
487
488 if let Some(w) = ranges.windows(2).find(|w| w[0].start == w[1].start) {
489 polars_bail!(
490 ComputeError:
491 "duplicate range start {} in read request ({:?} and {:?})",
492 w[0].start, w[0], w[1]
493 );
494 }
495
496 let mut out = PlHashMap::with_capacity(ranges.len());
497 for r in ranges.iter() {
498 if r.is_empty() {
500 out.insert(r.start, Buffer::new());
501 continue;
502 }
503
504 let idx = fetched.partition_point(|(s, _)| s.start <= r.start);
507 let (span, buf) = &fetched[idx - 1];
508 debug_assert!(span.start <= r.start && r.end <= span.end);
509
510 let off = r.start - span.start;
511 out.insert(r.start, buf.clone().sliced(off..off + r.len()));
512 }
513
514 debug_assert_eq!(out.len(), ranges.len());
515 Ok(out)
516 }
517}
518
519#[cfg(feature = "cloud")]
520pub struct ObjectStoreByteSource {
521 store: PolarsObjectStore,
522 path: ObjectStorePath,
523 config: FetchConfig,
524}
525
526#[cfg(feature = "cloud")]
527impl ObjectStoreByteSource {
528 async fn try_new_from_path(
529 path: PlRefPath,
530 cloud_options: Option<&CloudOptions>,
531 io_metrics: Option<Arc<IOMetrics>>,
532 config: FetchConfig,
533 ) -> PolarsResult<Self> {
534 let (CloudLocation { prefix, .. }, mut store) =
535 build_object_store(path, cloud_options, false).await?;
536 let path = object_path_from_str(&prefix)?;
537
538 store.set_io_metrics(io_metrics);
539
540 Ok(Self {
541 store,
542 path,
543 config,
544 })
545 }
546
547 #[allow(unused)]
548 fn chunk_size(&self) -> usize {
549 self.config.chunk_size
550 }
551
552 fn concurrency_strategy(&self) -> ConcurrencyStrategy {
553 self.config.strategy
554 }
555}
556
557#[cfg(feature = "cloud")]
558impl ByteSource for ObjectStoreByteSource {
559 async fn get_size(&self) -> PolarsResult<usize> {
560 Ok(self
561 .store
562 .head(&self.path, self.concurrency_strategy())
563 .await?
564 .size as usize)
565 }
566
567 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
568 self.store.get_suffix(&self.path, n, self.config).await
569 }
570
571 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
572 self.store.get_range(&self.path, range, self.config).await
573 }
574
575 async fn get_ranges(
576 &self,
577 ranges: &mut [Range<usize>],
578 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
579 self.store
580 .get_ranges_sort(&self.path, ranges, self.config)
581 .await
582 }
583}
584
585pub enum DynByteSource {
587 Buffer(BufferByteSource),
588 File(FileByteSource),
589 #[cfg(feature = "cloud")]
590 Cloud(ObjectStoreByteSource),
591}
592
593impl DynByteSource {
594 pub fn variant_name(&self) -> &str {
595 match self {
596 Self::Buffer(_) => "Buffer",
597 Self::File(_) => "File",
598 #[cfg(feature = "cloud")]
599 Self::Cloud(_) => "Cloud",
600 }
601 }
602
603 pub fn is_cloud(&self) -> bool {
604 match self {
605 Self::Buffer(_) => false,
606 Self::File(_) => false,
607 #[cfg(feature = "cloud")]
608 Self::Cloud(_) => true,
609 }
610 }
611
612 pub fn chunk_size(&self) -> Option<usize> {
613 match self {
614 Self::Buffer(_) => None,
615 Self::File(_) => None,
616 #[cfg(feature = "cloud")]
617 Self::Cloud(source) => Some(source.config.chunk_size),
618 }
619 }
620
621 pub fn concurrency_strategy(&self) -> Option<ConcurrencyStrategy> {
622 match self {
623 Self::Buffer(_) => None,
624 Self::File(_) => None,
626 #[cfg(feature = "cloud")]
627 Self::Cloud(source) => Some(source.concurrency_strategy()),
628 }
629 }
630}
631
632impl Default for DynByteSource {
633 fn default() -> Self {
634 Self::Buffer(BufferByteSource(Buffer::new()))
635 }
636}
637
638impl ByteSource for DynByteSource {
639 async fn get_size(&self) -> PolarsResult<usize> {
640 match self {
641 Self::Buffer(v) => v.get_size().await,
642 Self::File(v) => v.get_size().await,
643 #[cfg(feature = "cloud")]
644 Self::Cloud(v) => v.get_size().await,
645 }
646 }
647
648 async fn get_suffix(&self, n: usize) -> PolarsResult<(Buffer<u8>, usize)> {
649 match self {
650 Self::Buffer(v) => v.get_suffix(n).await,
651 Self::File(v) => v.get_suffix(n).await,
652 #[cfg(feature = "cloud")]
653 Self::Cloud(v) => v.get_suffix(n).await,
654 }
655 }
656
657 async fn get_range(&self, range: Range<usize>) -> PolarsResult<Buffer<u8>> {
658 match self {
659 Self::Buffer(v) => v.get_range(range).await,
660 Self::File(v) => v.get_range(range).await,
661 #[cfg(feature = "cloud")]
662 Self::Cloud(v) => v.get_range(range).await,
663 }
664 }
665
666 async fn get_ranges(
667 &self,
668 ranges: &mut [Range<usize>],
669 ) -> PolarsResult<PlHashMap<usize, Buffer<u8>>> {
670 match self {
671 Self::Buffer(v) => v.get_ranges(ranges).await,
672 Self::File(v) => v.get_ranges(ranges).await,
673 #[cfg(feature = "cloud")]
674 Self::Cloud(v) => v.get_ranges(ranges).await,
675 }
676 }
677}
678
679impl From<BufferByteSource> for DynByteSource {
680 fn from(value: BufferByteSource) -> Self {
681 Self::Buffer(value)
682 }
683}
684
685impl From<FileByteSource> for DynByteSource {
686 fn from(value: FileByteSource) -> Self {
687 Self::File(value)
688 }
689}
690
691#[cfg(feature = "cloud")]
692impl From<ObjectStoreByteSource> for DynByteSource {
693 fn from(value: ObjectStoreByteSource) -> Self {
694 Self::Cloud(value)
695 }
696}
697
698impl From<Buffer<u8>> for DynByteSource {
699 fn from(value: Buffer<u8>) -> Self {
700 Self::Buffer(BufferByteSource(value))
701 }
702}
703
704#[derive(Clone, Debug)]
705pub enum DynByteSourceBuilder {
706 Mmap,
707 FilePread(FileReadContext),
709 ObjectStore(FetchConfig),
711}
712
713impl DynByteSourceBuilder {
714 pub async fn try_build_from_path(
715 &self,
716 path: PlRefPath,
717 cloud_options: Option<&CloudOptions>,
718 io_metrics: Option<Arc<IOMetrics>>,
719 ) -> PolarsResult<DynByteSource> {
720 Ok(match self {
721 Self::Mmap => {
722 BufferByteSource::try_new_mmap_from_path(path.as_std_path(), cloud_options)
723 .await?
724 .into()
725 },
726 Self::FilePread(read_context) => {
727 FileByteSource::try_new_from_path(path, read_context.clone(), io_metrics)
728 .await?
729 .into()
730 },
731 Self::ObjectStore(fetch_config) => feature_gated!("cloud", {
732 ObjectStoreByteSource::try_new_from_path(
733 path,
734 cloud_options,
735 io_metrics,
736 *fetch_config,
737 )
738 .await?
739 .into()
740 }),
741 })
742 }
743
744 pub fn chunk_size(&self) -> Option<usize> {
745 match self {
746 Self::Mmap => None,
747 Self::FilePread(_) => None,
748 Self::ObjectStore(fetch_config) => Some(fetch_config.chunk_size),
749 }
750 }
751
752 pub fn concurrency_strategy(&self) -> Option<&ConcurrencyStrategy> {
753 match self {
754 Self::Mmap => None,
755 Self::FilePread(_) => None,
756 Self::ObjectStore(fetch_config) => Some(&fetch_config.strategy),
757 }
758 }
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764
765 const MIB: usize = 1 << 20;
766
767 fn source(dir: &std::path::Path, len: usize) -> (FileByteSource, Vec<u8>) {
769 let path = dir.join("f.bin");
770 let contents: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
771 std::fs::write(&path, &contents).unwrap();
772
773 let read_context = FileReadContext {
774 enable_o_direct: false,
775 concurrency: 4,
776 permits: Arc::new(Semaphore::new(4)),
777 advice: FileAdvice::Normal,
778 };
779 let source = FileByteSource::try_new_from_std(
780 std::fs::File::open(&path).unwrap(),
781 read_context,
782 None,
783 )
784 .unwrap();
785 (source, contents)
786 }
787
788 fn runtime() -> tokio::runtime::Runtime {
789 tokio::runtime::Builder::new_multi_thread()
790 .worker_threads(2)
791 .enable_all()
792 .build()
793 .unwrap()
794 }
795
796 #[test]
797 fn get_ranges_resolves_every_range() {
798 let dir = tempfile::tempdir().unwrap();
799 let (source, contents) = source(dir.path(), 17 * MIB);
800 let rt = runtime();
801
802 let cases: [(&str, &[Range<usize>]); 6] = [
803 ("disjoint", &[0..100, 500..600, 900..1000]),
804 ("unsorted", &[900..1000, 0..100, 500..600]),
805 ("partial overlap", &[0..100, 50..150]),
806 ("contained", &[0..200, 50..100]),
807 ("empty range at a span end", &[0..100, 100..100]),
809 (
812 "overlapping spans",
813 &[
814 0..4096,
815 5_000_000..14_000_000,
816 13_000_000..13_500_000,
817 13_500_000..16_000_000,
818 ],
819 ),
820 ];
821
822 for (name, ranges) in cases {
823 let mut ranges = ranges.to_vec();
824 let expect = ranges.clone();
825 let out = rt.block_on(source.get_ranges(&mut ranges)).unwrap();
826
827 assert_eq!(out.len(), expect.len(), "{name}");
828 for r in &expect {
829 let got = out
830 .get(&r.start)
831 .unwrap_or_else(|| panic!("{name}: {r:?} missing"));
832 assert_eq!(got.as_ref(), &contents[r.clone()], "{name}: {r:?}");
833 }
834 }
835 }
836}