use dataloader_rs::{
DataLoader, Dataset, Transform,
collator::Collator,
error::Result,
sampler::{DistributedSampler, RandomSampler, SequentialSampler},
};
use std::{
collections::HashSet,
sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
thread,
time::Duration,
};
struct RangeDs(usize);
impl Dataset for RangeDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
Ok(index)
}
fn len(&self) -> usize {
self.0
}
}
struct AlwaysErrDs(usize);
impl Dataset for AlwaysErrDs {
type Item = usize;
fn get(&self, _: usize) -> Result<usize> {
Err("always fails".into())
}
fn len(&self) -> usize {
self.0
}
}
struct PartialErrDs {
fail_at: usize,
len: usize,
}
impl Dataset for PartialErrDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
if index == self.fail_at {
Err(format!("fail at {index}").into())
} else {
Ok(index)
}
}
fn len(&self) -> usize {
self.len
}
}
struct SlowDs {
len: usize,
in_flight: Arc<AtomicUsize>,
peak: Arc<AtomicUsize>,
}
impl Dataset for SlowDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
let current = self.in_flight.fetch_add(1, Ordering::SeqCst) + 1;
self.peak.fetch_max(current, Ordering::SeqCst);
thread::sleep(Duration::from_micros(200));
self.in_flight.fetch_sub(1, Ordering::SeqCst);
Ok(index)
}
fn len(&self) -> usize {
self.len
}
}
struct ConcurrentDetectorDs {
len: usize,
in_flight: Arc<AtomicUsize>,
was_concurrent: Arc<AtomicBool>,
}
impl Dataset for ConcurrentDetectorDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
let prev = self.in_flight.fetch_add(1, Ordering::SeqCst);
if prev > 0 {
self.was_concurrent.store(true, Ordering::Relaxed);
}
thread::sleep(Duration::from_micros(300));
self.in_flight.fetch_sub(1, Ordering::SeqCst);
Ok(index)
}
fn len(&self) -> usize {
self.len
}
}
struct BatchOverrideDs {
len: usize,
get_calls: Arc<AtomicUsize>,
get_batch_calls: Arc<AtomicUsize>,
}
impl Dataset for BatchOverrideDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
self.get_calls.fetch_add(1, Ordering::Relaxed);
Ok(index)
}
fn get_batch(&self, indices: &[usize]) -> Result<Vec<usize>> {
self.get_batch_calls.fetch_add(1, Ordering::Relaxed);
indices.iter().map(|&i| self.get(i)).collect()
}
fn len(&self) -> usize {
self.len
}
}
struct SumCollator;
impl Collator<usize> for SumCollator {
type Batch = usize;
fn collate(&self, items: Vec<usize>) -> Result<usize> {
Ok(items.iter().sum())
}
}
struct ErrCollator;
impl Collator<usize> for ErrCollator {
type Batch = usize;
fn collate(&self, _items: Vec<usize>) -> Result<usize> {
Err("collate always fails".into())
}
}
#[test]
fn seq_all_items_covered_small() {
let mut loader = DataLoader::builder(RangeDs(10)).batch_size(3).build();
let mut items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
items.sort_unstable();
assert_eq!(items, (0..10).collect::<Vec<_>>());
}
#[test]
fn seq_all_items_covered_large() {
let n = 1000usize;
let mut loader = DataLoader::builder(RangeDs(n)).batch_size(17).build();
let mut items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
items.sort_unstable();
assert_eq!(items, (0..n).collect::<Vec<_>>());
}
#[test]
fn seq_exact_batches_no_drop_last() {
let mut loader = DataLoader::builder(RangeDs(10)).batch_size(3).build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 4);
assert_eq!(batches[3], vec![9]); }
#[test]
fn seq_exact_batches_drop_last() {
let mut loader = DataLoader::builder(RangeDs(10))
.batch_size(3)
.drop_last(true)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 3);
for b in &batches {
assert_eq!(b.len(), 3, "every batch must be full when drop_last=true");
}
}
#[test]
fn seq_single_batch_when_bs_equals_n() {
let mut loader = DataLoader::builder(RangeDs(6)).batch_size(6).build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0], vec![0, 1, 2, 3, 4, 5]);
}
#[test]
fn seq_bs_larger_than_n() {
let mut loader = DataLoader::builder(RangeDs(3))
.batch_size(10)
.drop_last(false)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 1, "should get exactly one partial batch");
assert_eq!(batches[0], vec![0, 1, 2]);
}
#[test]
fn seq_bs_larger_than_n_drop_last() {
let mut loader = DataLoader::builder(RangeDs(3))
.batch_size(10)
.drop_last(true)
.build();
assert_eq!(
loader.iter().count(),
0,
"drop_last=true with bs>N must yield no batches"
);
}
#[test]
fn seq_empty_dataset() {
let mut loader = DataLoader::builder(RangeDs(0)).batch_size(4).build();
assert_eq!(loader.iter().count(), 0);
}
#[test]
fn seq_single_item_dataset() {
let mut loader = DataLoader::builder(RangeDs(1)).batch_size(1).build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 1);
assert_eq!(batches[0], vec![0]);
}
#[test]
fn seq_batch_size_one() {
let mut loader = DataLoader::builder(RangeDs(5)).batch_size(1).build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 5);
for (i, batch) in batches.iter().enumerate() {
assert_eq!(batch.len(), 1);
assert_eq!(batch[0], i);
}
}
#[test]
fn seq_for_loop_sugar() {
let mut loader = DataLoader::builder(RangeDs(8)).batch_size(4).build();
let mut count = 0usize;
for batch in &mut loader {
let batch = batch.unwrap();
assert_eq!(batch.len(), 4);
count += 1;
}
assert_eq!(count, 2);
}
#[test]
fn seq_reusable_across_epochs() {
let mut loader = DataLoader::builder(RangeDs(12)).batch_size(4).build();
let e1: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
for _ in 0..4 {
let en: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(
e1, en,
"every epoch must be identical for SequentialSampler"
);
}
}
#[test]
fn seq_exact_size_iterator() {
let mut loader = DataLoader::builder(RangeDs(13)).batch_size(5).build();
let mut iter = loader.iter();
assert_eq!(iter.len(), 3);
iter.next().unwrap().unwrap();
assert_eq!(iter.len(), 2);
iter.next().unwrap().unwrap();
assert_eq!(iter.len(), 1);
iter.next().unwrap().unwrap();
assert_eq!(iter.len(), 0);
assert!(iter.next().is_none(), "iterator should be exhausted");
}
#[test]
fn seq_custom_collator_sum() {
let mut loader = DataLoader::builder(RangeDs(6))
.batch_size(3)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![3, 12]);
}
#[test]
fn seq_collator_error_propagates() {
let mut loader = DataLoader::builder(RangeDs(6))
.batch_size(3)
.collator(ErrCollator)
.build();
for result in loader.iter() {
assert!(
result.is_err(),
"collator error must propagate as Err batch"
);
}
}
#[test]
fn seq_sum_collator_exact_values() {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![6, 22, 38]);
}
#[test]
fn seq_sum_collator_drop_last_exact() {
let mut loader = DataLoader::builder(RangeDs(10))
.batch_size(4)
.drop_last(true)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![6, 22]);
}
#[test]
fn par_sum_collator_exact_values() {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.num_workers(4)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![6, 22, 38]);
}
#[test]
fn par_sum_collator_matches_sequential() {
let n = 30;
let bs = 6;
let mut seq = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.collator(SumCollator)
.build();
let mut par = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.num_workers(4)
.collator(SumCollator)
.build();
let seq_sums: Vec<usize> = seq.iter().map(|b| b.unwrap()).collect();
let par_sums: Vec<usize> = par.iter().map(|b| b.unwrap()).collect();
assert_eq!(seq_sums, par_sums);
}
#[test]
fn item_concurrency_sum_collator_exact_values() {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.item_concurrency(4)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![6, 22, 38]);
}
#[test]
fn item_concurrency_with_workers_sum_collator_exact_values() {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.num_workers(2)
.item_concurrency(2)
.collator(SumCollator)
.build();
let sums: Vec<usize> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(sums, vec![6, 22, 38]);
}
macro_rules! seq_loader {
($n:expr, $bs:expr, $seed:expr) => {
DataLoader::builder(RangeDs($n))
.batch_size($bs)
.sampler(RandomSampler::new($seed))
.build()
};
}
macro_rules! par_loader {
($n:expr, $bs:expr, $seed:expr, $workers:expr) => {
DataLoader::builder(RangeDs($n))
.batch_size($bs)
.sampler(RandomSampler::new($seed))
.num_workers($workers)
.prefetch_depth($workers.max(1))
.build()
};
}
#[test]
fn par_same_items_as_seq_2w() {
let seed = 42;
let mut seq = seq_loader!(20, 4, seed);
let mut par = par_loader!(20, 4, seed, 2);
let seq_items: HashSet<usize> = seq.iter().flat_map(|b| b.unwrap()).collect();
let par_items: HashSet<usize> = par.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seq_items, par_items);
}
#[test]
fn par_same_items_as_seq_4w() {
let seed = 7;
let mut seq = seq_loader!(24, 4, seed);
let mut par = par_loader!(24, 4, seed, 4);
let seq_items: HashSet<usize> = seq.iter().flat_map(|b| b.unwrap()).collect();
let par_items: HashSet<usize> = par.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seq_items, par_items);
}
#[test]
fn par_same_items_as_seq_8w() {
let seed = 99;
let mut seq = seq_loader!(40, 5, seed);
let mut par = par_loader!(40, 5, seed, 8);
let seq_items: HashSet<usize> = seq.iter().flat_map(|b| b.unwrap()).collect();
let par_items: HashSet<usize> = par.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seq_items, par_items);
}
#[test]
fn par_same_items_as_seq_more_workers_than_batches() {
let seed = 1;
let mut seq = seq_loader!(10, 2, seed);
let mut par = par_loader!(10, 2, seed, 20);
let seq_items: HashSet<usize> = seq.iter().flat_map(|b| b.unwrap()).collect();
let par_items: HashSet<usize> = par.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seq_items, par_items);
assert_eq!(par_items.len(), 10);
}
#[test]
fn par_empty_dataset() {
let mut loader = DataLoader::builder(RangeDs(0))
.batch_size(4)
.num_workers(4)
.build();
assert_eq!(loader.iter().count(), 0);
}
#[test]
fn par_single_batch_4w() {
let mut loader = DataLoader::builder(RangeDs(8))
.batch_size(8)
.num_workers(4)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 1);
let mut items = batches.into_iter().flatten().collect::<Vec<_>>();
items.sort_unstable();
assert_eq!(items, (0..8).collect::<Vec<_>>());
}
#[test]
fn par_drop_last() {
let mut loader = DataLoader::builder(RangeDs(11))
.batch_size(3)
.num_workers(4)
.drop_last(true)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 3);
for b in &batches {
assert_eq!(b.len(), 3);
}
}
#[test]
fn par_reusable_across_epochs() {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.num_workers(2)
.build();
let e1: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
for _ in 0..4 {
let en: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(e1, en);
}
}
#[test]
fn par_early_drop_no_hang() {
let mut loader = DataLoader::builder(RangeDs(100))
.batch_size(5)
.num_workers(2)
.prefetch_depth(4)
.build();
{
let mut iter = loader.iter();
iter.next().unwrap().unwrap(); }
let count = loader.iter().count();
assert_eq!(count, 20); }
#[test]
fn par_early_drop_zero_batches_no_hang() {
let mut loader = DataLoader::builder(RangeDs(50))
.batch_size(5)
.num_workers(4)
.prefetch_depth(4)
.build();
{
let _iter = loader.iter();
}
let count = loader.iter().count();
assert_eq!(count, 10);
}
#[test]
fn par_break_then_full_epoch() {
let n = 30;
let bs = 5;
let mut loader = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.num_workers(2)
.prefetch_depth(3)
.build();
for (i, b) in (&mut loader).into_iter().enumerate() {
b.unwrap();
if i == 1 {
break;
}
}
let items: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(items.len(), n);
}
#[test]
fn par_actually_concurrent() {
let in_flight = Arc::new(AtomicUsize::new(0));
let was_concurrent = Arc::new(AtomicBool::new(false));
let ds = ConcurrentDetectorDs {
len: 20,
in_flight: Arc::clone(&in_flight),
was_concurrent: Arc::clone(&was_concurrent),
};
let mut loader = DataLoader::builder(ds)
.batch_size(10) .num_workers(4)
.prefetch_depth(2)
.build();
for batch in &mut loader {
batch.unwrap();
}
assert!(
was_concurrent.load(Ordering::Relaxed),
"expected concurrent get() calls but execution appeared sequential"
);
}
#[test]
fn item_concurrency_same_items_as_seq() {
let seed = 55u64;
let n = 24;
let bs = 6;
let mut seq = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.sampler(RandomSampler::new(seed))
.build();
let mut par = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.sampler(RandomSampler::new(seed))
.num_workers(2)
.item_concurrency(4)
.build();
let seq_items: HashSet<usize> = seq.iter().flat_map(|b| b.unwrap()).collect();
let par_items: HashSet<usize> = par.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seq_items, par_items);
}
#[test]
fn item_concurrency_actually_parallel() {
let peak = Arc::new(AtomicUsize::new(0));
let in_flight = Arc::new(AtomicUsize::new(0));
let ds = SlowDs {
len: 16,
in_flight: Arc::clone(&in_flight),
peak: Arc::clone(&peak),
};
let mut loader = DataLoader::builder(ds)
.batch_size(8) .num_workers(1)
.item_concurrency(4)
.build();
for batch in &mut loader {
batch.unwrap();
}
assert!(
peak.load(Ordering::SeqCst) > 1,
"expected concurrent intra-batch get() calls, but peak concurrency was 1"
);
}
#[test]
fn item_concurrency_with_large_batch() {
let n = 128;
let mut loader = DataLoader::builder(RangeDs(n))
.batch_size(64)
.num_workers(2)
.item_concurrency(8)
.build();
let mut items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
items.sort_unstable();
assert_eq!(items, (0..n).collect::<Vec<_>>());
}
#[test]
fn sampler_sequential_is_deterministic() {
let mut loader = DataLoader::builder(RangeDs(15)).batch_size(5).build();
let e1: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
let e2: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
let e3: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(e1, e2);
assert_eq!(e2, e3);
}
#[test]
fn sampler_random_differs_across_epochs() {
let mut loader = DataLoader::builder(RangeDs(40))
.batch_size(8)
.sampler(RandomSampler::new(999))
.build();
let e1: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
let e2: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
let e3: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_ne!(e1, e2, "epoch 1 and 2 should differ");
assert_ne!(e2, e3, "epoch 2 and 3 should differ");
}
#[test]
fn sampler_random_covers_all_indices() {
let n = 100;
let mut loader = DataLoader::builder(RangeDs(n))
.batch_size(7)
.sampler(RandomSampler::new(42))
.build();
let seen: HashSet<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
assert_eq!(seen.len(), n);
assert_eq!(*seen.iter().min().unwrap(), 0);
assert_eq!(*seen.iter().max().unwrap(), n - 1);
}
#[test]
fn sampler_random_reproducible_with_same_seed() {
let n = 30;
let bs = 5;
let seed = 12345;
let mut l1 = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.sampler(RandomSampler::new(seed))
.build();
let mut l2 = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.sampler(RandomSampler::new(seed))
.build();
let out1: Vec<Vec<usize>> = l1.iter().map(|b| b.unwrap()).collect();
let out2: Vec<Vec<usize>> = l2.iter().map(|b| b.unwrap()).collect();
assert_eq!(out1, out2, "same seed → same first-epoch order");
}
#[test]
fn sampler_distributed_disjoint_coverage() {
let n = 20;
let world_size = 4;
let mut all_items: Vec<usize> = Vec::new();
for rank in 0..world_size {
let mut loader = DataLoader::builder(RangeDs(n))
.batch_size(2)
.sampler(DistributedSampler::new(SequentialSampler, rank, world_size))
.build();
let epoch_items: Vec<usize> = loader.iter().flat_map(|b| b.unwrap()).collect();
all_items.extend(epoch_items);
}
assert_eq!(all_items.len(), n, "all items across all ranks = N");
let unique: HashSet<usize> = all_items.into_iter().collect();
assert_eq!(unique.len(), n, "no index should appear more than once");
}
#[test]
fn sampler_distributed_each_rank_gets_equal_share() {
let n = 20;
let world_size = 4;
let expected = n / world_size;
for rank in 0..world_size {
let mut loader = DataLoader::builder(RangeDs(n))
.batch_size(1)
.sampler(DistributedSampler::new(SequentialSampler, rank, world_size))
.build();
let count: usize = loader.iter().flat_map(|b| b.unwrap()).count();
assert_eq!(
count, expected,
"rank {rank} must receive exactly {expected} items"
);
}
}
#[test]
fn error_in_first_item_fails_batch() {
let mut loader = DataLoader::builder(PartialErrDs { fail_at: 0, len: 8 })
.batch_size(4)
.build();
let results: Vec<_> = loader.iter().collect();
assert!(results[0].is_err(), "first batch must be Err (fail_at=0)");
assert!(results[1].is_ok(), "second batch must succeed");
}
#[test]
fn error_in_last_item_fails_batch() {
let mut loader = DataLoader::builder(PartialErrDs { fail_at: 3, len: 8 })
.batch_size(4)
.build();
let results: Vec<_> = loader.iter().collect();
assert!(results[0].is_err(), "first batch must be Err (fail_at=3)");
assert!(results[1].is_ok(), "second batch must succeed");
}
#[test]
fn error_propagates_parallel() {
let mut loader = DataLoader::builder(PartialErrDs {
fail_at: 5,
len: 16,
})
.batch_size(4)
.num_workers(4)
.build();
let results: Vec<_> = loader.iter().collect();
let has_err = results.iter().any(|r| r.is_err());
assert!(has_err, "parallel loader must propagate dataset errors");
}
#[test]
fn error_all_items_fail() {
let mut loader = DataLoader::builder(AlwaysErrDs(8)).batch_size(4).build();
for result in loader.iter() {
assert!(
result.is_err(),
"every batch must be Err when all items fail"
);
}
}
#[test]
fn error_loader_reusable_after_error_epoch() {
let mut loader = DataLoader::builder(PartialErrDs { fail_at: 0, len: 6 })
.batch_size(3)
.build();
let _ = loader.iter().count();
let count = loader.iter().count();
assert_eq!(
count, 2,
"second epoch must produce the same number of batches"
);
}
#[test]
fn collator_error_skips_collation() {
use std::sync::atomic::AtomicUsize;
struct CountingCollator {
calls: Arc<AtomicUsize>,
}
impl Collator<usize> for CountingCollator {
type Batch = Vec<usize>;
fn collate(&self, items: Vec<usize>) -> Result<Vec<usize>> {
self.calls.fetch_add(1, Ordering::SeqCst);
Ok(items)
}
}
let calls = Arc::new(AtomicUsize::new(0));
let collator = CountingCollator {
calls: Arc::clone(&calls),
};
let mut loader = DataLoader::builder(PartialErrDs { fail_at: 3, len: 8 })
.batch_size(4)
.collator(collator)
.build();
let results: Vec<_> = loader.iter().collect();
assert_eq!(results.len(), 2);
assert!(results[0].is_err(), "first batch should be Err");
assert!(results[1].is_ok(), "second batch should be Ok");
assert_eq!(
calls.load(Ordering::SeqCst),
1,
"collate must be called exactly once (only for the successful batch)"
);
}
#[test]
fn error_middle_batch_others_succeed() {
let mut loader = DataLoader::builder(PartialErrDs {
fail_at: 5,
len: 12,
})
.batch_size(4)
.build();
let results: Vec<_> = loader.iter().collect();
assert_eq!(results.len(), 3);
assert!(results[0].is_ok(), "batch 0 should succeed");
assert!(
results[1].is_err(),
"batch 1 (indices 4-7) should fail at index 5"
);
assert!(results[2].is_ok(), "batch 2 should succeed");
}
#[test]
fn prefetch_depth_one() {
let mut loader = DataLoader::builder(RangeDs(20))
.batch_size(4)
.prefetch_depth(1)
.num_workers(4)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 5);
let mut items: Vec<usize> = batches.into_iter().flatten().collect();
items.sort_unstable();
assert_eq!(items, (0..20).collect::<Vec<_>>());
}
#[test]
fn prefetch_depth_larger_than_batches() {
let mut loader = DataLoader::builder(RangeDs(10))
.batch_size(2)
.prefetch_depth(1000)
.num_workers(4)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 5);
let mut items: Vec<usize> = batches.into_iter().flatten().collect();
items.sort_unstable();
assert_eq!(items, (0..10).collect::<Vec<_>>());
}
#[test]
fn loader_len_and_batch_len() {
let n = 17;
let bs = 5;
let loader = DataLoader::builder(RangeDs(n)).batch_size(bs).build();
assert_eq!(loader.len(), n, "loader.len() must equal dataset size");
assert_eq!(loader.batch_len(), 4, "batch_len() must equal ceil(N/bs)");
let loader_dl = DataLoader::builder(RangeDs(n))
.batch_size(bs)
.drop_last(true)
.build();
assert_eq!(loader_dl.batch_len(), 3);
}
#[test]
fn loader_is_empty() {
let empty = DataLoader::builder(RangeDs(0)).build();
assert!(empty.is_empty());
let non_empty = DataLoader::builder(RangeDs(1)).build();
assert!(!non_empty.is_empty());
}
#[test]
fn get_batch_override_is_called() {
let get_batch_calls = Arc::new(AtomicUsize::new(0));
let get_calls = Arc::new(AtomicUsize::new(0));
let ds = BatchOverrideDs {
len: 9,
get_calls: Arc::clone(&get_calls),
get_batch_calls: Arc::clone(&get_batch_calls),
};
let mut loader = DataLoader::builder(ds).batch_size(3).build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches.len(), 3);
assert_eq!(
get_batch_calls.load(Ordering::Relaxed),
3,
"get_batch must be called exactly once per batch"
);
assert_eq!(
get_calls.load(Ordering::Relaxed),
9,
"get must be called once per item inside get_batch"
);
}
const MODES: [(usize, usize); 4] = [(0, 1), (0, 3), (3, 1), (3, 3)];
#[test]
fn the_transform_receives_each_items_index() {
for (num_workers, item_concurrency) in MODES {
let mut loader = DataLoader::builder(RangeDs(40))
.batch_size(6)
.num_workers(num_workers)
.item_concurrency(item_concurrency)
.sampler(RandomSampler::new(3))
.transform(|index: usize, item: usize| -> Result<(usize, usize)> { Ok((index, item)) })
.build();
let pairs: Vec<(usize, usize)> = loader.iter().flat_map(|b| b.unwrap()).collect();
assert!(pairs.iter().all(|(index, item)| index == item));
let mut indices: Vec<usize> = pairs.iter().map(|&(index, _)| index).collect();
assert_ne!(indices, (0..40).collect::<Vec<_>>(), "shuffled");
indices.sort_unstable();
assert_eq!(indices, (0..40).collect::<Vec<_>>());
}
}
#[test]
fn transforms_chain_and_feed_the_collator() {
let double = |_: usize, item: usize| -> Result<usize> { Ok(item * 2) };
let label = |index: usize, item: usize| -> Result<String> { Ok(format!("{index}:{item}")) };
let mut loader = DataLoader::builder(RangeDs(4))
.batch_size(2)
.collator(JoinCollator)
.transform(double.then(label))
.build();
let batches: Vec<String> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches, ["0:0,1:2", "2:4,3:6"]);
}
struct JoinCollator;
impl Collator<String> for JoinCollator {
type Batch = String;
fn collate(&self, items: Vec<String>) -> Result<String> {
Ok(items.join(","))
}
}
#[test]
fn a_failed_transform_fails_its_batch_only() {
for (num_workers, item_concurrency) in MODES {
let mut loader = DataLoader::builder(RangeDs(12))
.batch_size(4)
.num_workers(num_workers)
.item_concurrency(item_concurrency)
.transform(|index: usize, item: usize| -> Result<String> {
match index {
5 => Err("no five".into()),
_ => Ok(format!("#{item}")),
}
})
.build();
let batches: Vec<_> = loader.iter().collect();
assert_eq!(batches[0].as_ref().unwrap()[3], "#3");
assert_eq!(batches[1].as_ref().unwrap_err().to_string(), "no five");
assert!(batches[2].is_ok());
}
}
#[test]
fn a_panic_in_the_transform_is_re_raised() {
for (num_workers, item_concurrency) in MODES {
let mut loader = DataLoader::builder(RangeDs(8))
.batch_size(4)
.num_workers(num_workers)
.item_concurrency(item_concurrency)
.transform(|index: usize, item: usize| -> Result<usize> {
assert_ne!(index, 6, "boom in transform");
Ok(item)
})
.build();
let panic =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| loader.iter().count()))
.unwrap_err();
let message = panic.downcast_ref::<String>().unwrap();
assert!(message.contains("boom in transform"), "{message}");
}
}
struct ShortBatchDs;
impl Dataset for ShortBatchDs {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
Ok(index)
}
fn get_batch(&self, indices: &[usize]) -> Result<Vec<usize>> {
Ok(indices[1..].to_vec())
}
fn len(&self) -> usize {
4
}
}
#[test]
fn a_short_get_batch_fails_the_batch() {
let mut loader = DataLoader::builder(ShortBatchDs).batch_size(2).build();
let err = loader.iter().next().unwrap().unwrap_err();
assert_eq!(err.to_string(), "get_batch returned 1 items for 2 indices");
}
#[test]
#[allow(deprecated)]
fn items_in_parallel_is_a_deprecated_alias() {
let mut loader = DataLoader::builder(RangeDs(6))
.batch_size(3)
.items_in_parallel(2)
.build();
let batches: Vec<Vec<usize>> = loader.iter().map(|b| b.unwrap()).collect();
assert_eq!(batches, vec![vec![0, 1, 2], vec![3, 4, 5]]);
}
struct TimesTen(usize);
impl Dataset for TimesTen {
type Item = usize;
fn get(&self, index: usize) -> Result<usize> {
Ok(index * 10)
}
fn len(&self) -> usize {
self.0
}
}
fn indexed_epoch(workers: usize, item_concurrency: usize, drop_last: bool) -> Vec<Vec<usize>> {
let mut loader = DataLoader::builder(TimesTen(23))
.batch_size(4)
.drop_last(drop_last)
.num_workers(workers)
.item_concurrency(item_concurrency)
.sampler(RandomSampler::new(3))
.build();
let iter = loader.iter_with_indices();
assert_eq!(iter.len(), if drop_last { 5 } else { 6 });
iter.map(|batch| {
let (indices, items) = batch.unwrap();
assert_eq!(items, indices.iter().map(|i| i * 10).collect::<Vec<_>>());
indices
})
.collect()
}
#[test]
fn iter_with_indices_pairs_items_with_their_indices() {
for (workers, item_concurrency) in [(0, 1), (2, 1), (0, 3), (3, 2)] {
let chunks = indexed_epoch(workers, item_concurrency, false);
let mut seen: Vec<usize> = chunks.iter().flatten().copied().collect();
assert_ne!(seen, (0..23).collect::<Vec<_>>(), "the sampler shuffles");
seen.sort_unstable();
assert_eq!(seen, (0..23).collect::<Vec<_>>());
assert_eq!(chunks.last().unwrap().len(), 3);
}
}
#[test]
fn iter_with_indices_drop_last() {
for workers in [0, 2] {
let chunks = indexed_epoch(workers, 1, true);
assert_eq!(chunks.len(), 5);
assert!(chunks.iter().all(|c| c.len() == 4));
}
}
#[test]
fn iter_with_indices_same_order_as_iter() {
let build = || {
DataLoader::builder(RangeDs(17))
.batch_size(5)
.num_workers(2)
.sampler(RandomSampler::new(11))
.build()
};
let plain: Vec<Vec<usize>> = build().iter().map(|b| b.unwrap()).collect();
let indexed: Vec<Vec<usize>> = build().iter_with_indices().map(|b| b.unwrap().0).collect();
assert_eq!(plain, indexed);
}
#[test]
fn iter_with_indices_keeps_alignment_after_a_failed_batch() {
for workers in [0, 2] {
let mut loader = DataLoader::builder(PartialErrDs {
fail_at: 5,
len: 12,
})
.batch_size(3)
.num_workers(workers)
.build();
let batches: Vec<_> = loader.iter_with_indices().collect();
assert_eq!(batches.len(), 4);
assert!(batches[1].is_err());
for (k, batch) in batches.iter().enumerate().filter(|(k, _)| *k != 1) {
let (indices, items) = batch.as_ref().unwrap();
assert_eq!(indices, &(3 * k..3 * k + 3).collect::<Vec<_>>());
assert_eq!(items, indices);
}
}
}