use std::io::Write;
use dataprof::{ChunkSize, EngineType, ProfileReport, Profiler};
use serde_json::{Value, json};
struct File {
label: &'static str,
bytes: &'static [u8],
columns: &'static [(&'static str, usize)],
rows: usize,
}
const FILES: [File; 4] = [
File {
label: "BOM, blank line",
bytes: b"\xEF\xBB\xBF\na;b\n1;\n",
columns: &[("a", 0), ("b", 1)],
rows: 1,
},
File {
label: "BOM, blank lines",
bytes: b"\xEF\xBB\xBF\n\r\n\na;b\n1;\n2;3\n",
columns: &[("a", 0), ("b", 1)],
rows: 2,
},
File {
label: "BOM, quoted multi-line header",
bytes: b"\xEF\xBB\xBF\"a\nb\";c\n1;\n2;3\n",
columns: &[("a\nb", 0), ("c", 1)],
rows: 2,
},
File {
label: "BOM, no blank line",
bytes: b"\xEF\xBB\xBFa;b\n1;\n",
columns: &[("a", 0), ("b", 1)],
rows: 1,
},
];
fn shape(report: &ProfileReport) -> Value {
let columns: Vec<Value> = report
.column_profiles
.iter()
.map(|column| json!([column.name, column.null_count]))
.collect();
json!({ "columns": columns, "rows": report.execution.rows_processed })
}
#[test]
fn every_chunk_size_reads_the_whole_header() {
let mut wrong = Vec::new();
for File {
label,
bytes,
columns,
rows,
} in FILES
{
let mut file = tempfile::Builder::new().suffix(".csv").tempfile().unwrap();
file.write_all(bytes).unwrap();
file.flush().unwrap();
let expected = json!({
"columns": columns.iter().map(|(name, nulls)| json!([name, nulls])).collect::<Vec<_>>(),
"rows": rows,
});
for engine in [EngineType::Incremental, EngineType::Auto] {
for size in 1..=bytes.len() + 1 {
match Profiler::new()
.engine(engine)
.chunk_size(ChunkSize::Fixed(size))
.analyze_file(file.path())
{
Ok(report) if shape(&report) == expected => {}
Ok(report) => wrong.push(format!(
"[{label}] {engine:?} chunk {size}: {}",
shape(&report)
)),
Err(error) => {
wrong.push(format!("[{label}] {engine:?} chunk {size} failed: {error}"))
}
}
}
}
}
assert!(wrong.is_empty(), "{}", wrong.join("\n"));
}