use std::path::PathBuf;
use std::sync::atomic::{AtomicUsize, Ordering};
use matten_data::{CsvBatchReader, MattenDataError, Table};
static COUNTER: AtomicUsize = AtomicUsize::new(0);
fn temp_csv(content: &str) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"matten_data_streaming_test_{}_{n}.csv",
std::process::id()
));
std::fs::write(&path, content).unwrap();
path
}
fn temp_csv_bytes(content: &[u8]) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"matten_data_streaming_test_{}_{n}.csv",
std::process::id()
));
std::fs::write(&path, content).unwrap();
path
}
fn numeric_flat(table: &Table) -> Vec<f64> {
table
.try_numeric()
.expect("numeric conversion")
.to_tensor()
.expect("to_tensor")
.as_slice()
.to_vec()
}
fn column_names(table: &Table) -> Vec<String> {
table.column_names().to_vec()
}
#[test]
fn exact_batching_3_3_3_1() {
let path = temp_csv("a,b\n1,10\n2,20\n3,30\n4,40\n5,50\n6,60\n7,70\n8,80\n9,90\n10,100");
let mut reader = CsvBatchReader::open(&path, 3).unwrap();
let sizes: Vec<usize> = std::iter::from_fn(|| reader.next_batch().unwrap())
.map(|t| t.row_count())
.collect();
assert_eq!(sizes, vec![3, 3, 3, 1]);
assert!(reader.next_batch().unwrap().is_none());
}
#[test]
fn file_smaller_than_one_batch_is_a_single_batch() {
let path = temp_csv("a,b\n1,2\n3,4");
let mut reader = CsvBatchReader::open(&path, 100).unwrap();
let batch = reader.next_batch().unwrap().expect("one batch");
assert_eq!(batch.row_count(), 2);
assert!(reader.next_batch().unwrap().is_none());
}
#[test]
fn header_only_file_yields_no_batches() {
let path = temp_csv("a,b,c");
let mut reader = CsvBatchReader::open(&path, 10).unwrap();
assert!(reader.next_batch().unwrap().is_none());
}
#[test]
fn repeated_calls_after_exhaustion_keep_returning_none() {
let path = temp_csv("a\n1\n2");
let mut reader = CsvBatchReader::open(&path, 10).unwrap();
assert!(reader.next_batch().unwrap().is_some());
assert!(reader.next_batch().unwrap().is_none());
assert!(reader.next_batch().unwrap().is_none());
assert!(reader.next_batch().unwrap().is_none());
}
#[test]
fn headers_are_applied_to_every_batch_not_just_the_first() {
let path = temp_csv("x,y\n1,2\n3,4\n5,6\n7,8");
let mut reader = CsvBatchReader::open(&path, 2).unwrap();
let first = reader.next_batch().unwrap().unwrap();
let second = reader.next_batch().unwrap().unwrap();
assert_eq!(column_names(&first), vec!["x".to_string(), "y".to_string()]);
assert_eq!(
column_names(&second),
vec!["x".to_string(), "y".to_string()]
);
}
#[test]
fn equivalence_concatenated_batches_equal_from_csv_path() {
let content = "a,b,c\n1,2,3\n4,5,6\n7,8,9\n10,11,12\n13,14,15\n16,17,18\n19,20,21";
let path = temp_csv(content);
let whole = Table::from_csv_path(&path).unwrap();
let whole_flat = numeric_flat(&whole);
let mut reader = CsvBatchReader::open(&path, 3).unwrap();
let mut batched_flat = Vec::new();
let mut batched_rows = 0;
while let Some(batch) = reader.next_batch().unwrap() {
assert_eq!(column_names(&batch), column_names(&whole));
batched_rows += batch.row_count();
batched_flat.extend(numeric_flat(&batch));
}
assert_eq!(batched_rows, whole.row_count());
assert_eq!(batched_flat, whole_flat);
}
#[test]
fn equivalence_trailing_newline() {
let content = "a,b\n1,2\n3,4\n5,6\n";
let path = temp_csv(content);
let whole = Table::from_csv_path(&path).unwrap();
let whole_flat = numeric_flat(&whole);
let mut reader = CsvBatchReader::open(&path, 2).unwrap();
let mut batched_flat = Vec::new();
let mut batched_rows = 0;
while let Some(batch) = reader.next_batch().unwrap() {
batched_rows += batch.row_count();
batched_flat.extend(numeric_flat(&batch));
}
assert_eq!(batched_rows, whole.row_count());
assert_eq!(batched_flat, whole_flat);
}
#[test]
fn equivalence_blank_line_before_end() {
let content = "a,b\n1,2\n3,4\n\n5,6";
let path = temp_csv(content);
let whole = Table::from_csv_path(&path).unwrap();
let whole_flat = numeric_flat(&whole);
let mut reader = CsvBatchReader::open(&path, 2).unwrap();
let mut batched_flat = Vec::new();
let mut batched_rows = 0;
while let Some(batch) = reader.next_batch().unwrap() {
batched_rows += batch.row_count();
batched_flat.extend(numeric_flat(&batch));
}
assert_eq!(batched_rows, whole.row_count());
assert_eq!(batched_flat, whole_flat);
}
#[test]
fn equivalence_non_numeric_columns_via_debug() {
let header = "name,active,note";
let data_lines = [
"alice,true,hello",
"bob,false,",
"café,true,\"a, b\"",
"dan,false,world",
];
let content = format!("{header}\n{}", data_lines.join("\n"));
let path = temp_csv(&content);
let whole = Table::from_csv_path(&path).unwrap();
let mut reader = CsvBatchReader::open(&path, 2).unwrap();
let mut start = 0;
while let Some(batch) = reader.next_batch().unwrap() {
let end = start + batch.row_count();
let sub_file = format!("{header}\n{}", data_lines[start..end].join("\n"));
let sub_path = temp_csv(&sub_file);
let sub_table = Table::from_csv_path(&sub_path).unwrap();
assert_eq!(
format!("{batch:?}"),
format!("{sub_table:?}"),
"batch covering rows {start}..{end} must be Debug-identical to \
Table::from_csv_path loading just those rows"
);
start = end;
}
assert_eq!(start, whole.row_count());
let mut single_batch_reader = CsvBatchReader::open(&path, 100).unwrap();
let single_batch = single_batch_reader
.next_batch()
.unwrap()
.expect("one batch covering the whole file");
assert!(single_batch_reader.next_batch().unwrap().is_none());
assert_eq!(format!("{single_batch:?}"), format!("{whole:?}"));
}
#[test]
fn only_line_terminators_is_empty_input_on_both_paths() {
let content = "\n\n";
let path = temp_csv(content);
assert!(matches!(
Table::from_csv_path(&path),
Err(MattenDataError::EmptyInput)
));
assert!(matches!(
CsvBatchReader::open(&path, 10),
Err(MattenDataError::EmptyInput)
));
}
#[test]
fn blank_but_not_empty_file_diverges_from_from_csv_path_documented() {
let content = " \n \n";
let path = temp_csv(content);
assert!(matches!(
Table::from_csv_path(&path),
Err(MattenDataError::EmptyInput)
));
assert!(matches!(
CsvBatchReader::open(&path, 10),
Err(MattenDataError::Csv { .. })
));
}
#[test]
fn invalid_utf8_diverges_in_variant_and_timing_documented() {
let mut content = b"a,b\n1,2\n3,4\n".to_vec();
content.extend_from_slice(b"\xff\xfe,6\n"); let path = temp_csv_bytes(&content);
assert!(matches!(
Table::from_csv_path(&path),
Err(MattenDataError::Io { .. })
));
let mut reader = CsvBatchReader::open(&path, 2).unwrap();
let first = reader
.next_batch()
.unwrap()
.expect("valid batch delivered before the bad bytes");
assert_eq!(first.row_count(), 2);
assert!(matches!(
reader.next_batch(),
Err(MattenDataError::Csv { .. })
));
}
#[test]
fn malformed_row_same_variant_and_line_number_as_from_csv_path() {
let content = "a,b,c\n1,2,3\n4,5,6\n7,8\n10,11,12";
let path = temp_csv(content);
let whole_err = Table::from_csv_path(&path).unwrap_err();
let mut reader = CsvBatchReader::open(&path, 10).unwrap();
let batch_err = reader.next_batch().unwrap_err();
match (&whole_err, &batch_err) {
(
MattenDataError::RaggedRow {
row: r1,
expected: e1,
actual: a1,
},
MattenDataError::RaggedRow {
row: r2,
expected: e2,
actual: a2,
},
) => {
assert_eq!(
r1, r2,
"line number must match Table::from_csv_path exactly"
);
assert_eq!(e1, e2);
assert_eq!(a1, a2);
}
other => panic!("expected RaggedRow on both paths, got {other:?}"),
}
}
#[test]
fn line_number_parity_at_a_batch_boundary() {
let content = "a,b,c\n1,2,3\n4,5,6\n7,8,9\n10,11,12\n13,14,15\n16,17,18\n19,20\n22,23,24";
let path = temp_csv(content);
let whole_err = Table::from_csv_path(&path).unwrap_err();
let mut reader = CsvBatchReader::open(&path, 3).unwrap();
reader.next_batch().unwrap(); reader.next_batch().unwrap(); let batch_err = reader.next_batch().unwrap_err();
match (&whole_err, &batch_err) {
(
MattenDataError::RaggedRow { row: r1, .. },
MattenDataError::RaggedRow { row: r2, .. },
) => {
assert_eq!(r1, r2, "line number must match across the batch boundary");
}
other => panic!("expected RaggedRow on both paths, got {other:?}"),
}
}
#[test]
fn reader_is_unusable_after_an_error() {
let content = "a,b\n1,2\n3\n5,6";
let path = temp_csv(content);
let mut reader = CsvBatchReader::open(&path, 10).unwrap();
assert!(reader.next_batch().is_err());
assert!(reader.next_batch().unwrap().is_none());
assert!(reader.next_batch().unwrap().is_none());
}
#[test]
fn batch_rows_zero_is_rejected_at_open() {
let path = temp_csv("a,b\n1,2");
assert!(matches!(
CsvBatchReader::open(&path, 0),
Err(MattenDataError::InvalidBatchSize)
));
}
#[test]
fn missing_file_is_rejected_at_open() {
let path = std::env::temp_dir().join("matten_data_streaming_test_does_not_exist.csv");
assert!(matches!(
CsvBatchReader::open(&path, 10),
Err(MattenDataError::Io { .. })
));
}