use std::{io::Write, path::PathBuf};
use biodream::{
BiopacError, Channel, ChannelData, ChannelMetadata, Datafile, ReadOptions, read_bytes,
read_file, read_stream,
};
fn build_pre4_acq(channels: usize, samples_per_channel: usize) -> Vec<u8> {
let n = samples_per_channel;
let chan_hdr_len: i32 = 252;
let chan_hdr_usize: usize = usize::try_from(chan_hdr_len).unwrap_or(252);
let mut buf: Vec<u8> = Vec::new();
buf.extend_from_slice(&0i16.to_le_bytes());
buf.extend_from_slice(&38i32.to_le_bytes());
buf.extend_from_slice(&256i32.to_le_bytes());
buf.extend_from_slice(&i16::try_from(channels).unwrap_or(0).to_le_bytes());
buf.extend_from_slice(&[0u8; 4]);
buf.extend_from_slice(&1.0f64.to_le_bytes());
buf.extend(std::iter::repeat_n(0u8, 228));
buf.extend_from_slice(&i16::try_from(chan_hdr_len).unwrap_or(252).to_le_bytes());
buf.extend_from_slice(&[0u8; 2]);
assert_eq!(buf.len(), 256, "graph header must be 256 bytes");
for ch in 0..channels {
let start = buf.len();
let mut ch_buf = [0u8; 252];
ch_buf[0..4].copy_from_slice(&chan_hdr_len.to_le_bytes());
let name = format!("CH{ch}");
let name_src = name.as_bytes();
let len = name_src.len().min(39);
if let (Some(dst), Some(src)) = (ch_buf.get_mut(6..6 + len), name_src.get(..len)) {
dst.copy_from_slice(src);
}
if let Some(dst) = ch_buf.get_mut(68..70) {
dst.copy_from_slice(b"mV");
}
ch_buf[88..92].copy_from_slice(&i32::try_from(n).unwrap_or(0).to_le_bytes());
ch_buf[92..100].copy_from_slice(&1.0f64.to_le_bytes());
ch_buf[100..108].copy_from_slice(&0.0f64.to_le_bytes());
ch_buf[250..252].copy_from_slice(&1i16.to_le_bytes());
buf.extend_from_slice(&ch_buf);
assert_eq!(
buf.len() - start,
chan_hdr_usize,
"channel header must be exactly {chan_hdr_usize} bytes"
);
}
buf.extend_from_slice(&0i32.to_le_bytes());
for _ in 0..channels {
buf.extend_from_slice(&4u16.to_le_bytes()); buf.extend_from_slice(&2u16.to_le_bytes()); }
for s in 0..n {
for ch in 0..channels {
let sample = i16::try_from(ch * 10 + s).unwrap_or(0).to_le_bytes();
buf.extend_from_slice(&sample);
}
}
buf.extend_from_slice(&8i32.to_le_bytes()); buf.extend_from_slice(&0i32.to_le_bytes());
buf.extend_from_slice(&0i32.to_le_bytes());
buf
}
fn get_channel(channels: &[Channel], idx: usize) -> Result<&Channel, BiopacError> {
channels
.get(idx)
.ok_or_else(|| BiopacError::InvalidChannel(format!("no channel {idx}")))
}
fn get_meta(meta: &[ChannelMetadata], idx: usize) -> Result<&ChannelMetadata, BiopacError> {
meta.get(idx)
.ok_or_else(|| BiopacError::InvalidChannel(format!("no metadata {idx}")))
}
fn write_tmp(bytes: &[u8], name: &str) -> Result<PathBuf, BiopacError> {
let dir = std::env::temp_dir();
let path = dir.join(name);
let mut f = std::fs::File::create(&path).map_err(BiopacError::Io)?;
f.write_all(bytes).map_err(BiopacError::Io)?;
Ok(path)
}
#[test]
fn read_bytes_two_channels() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let result = read_bytes(&blob)?;
assert!(
result.warnings.is_empty(),
"unexpected warnings: {:?}",
result.warnings
);
let df = result.value;
assert_eq!(df.channel_count(), 2);
Ok(())
}
#[test]
fn read_bytes_channel_names_and_units() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
assert_eq!(get_channel(&df.channels, 0)?.name, "CH0");
assert_eq!(get_channel(&df.channels, 0)?.units, "mV");
assert_eq!(get_channel(&df.channels, 1)?.name, "CH1");
assert_eq!(get_channel(&df.channels, 1)?.units, "mV");
Ok(())
}
#[test]
fn read_bytes_sample_data() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
assert_eq!(get_channel(&df.channels, 0)?.point_count, 4);
assert_eq!(get_channel(&df.channels, 1)?.point_count, 4);
let ch0_floats = get_channel(&df.channels, 0)?.scaled_samples();
let ch1_floats = get_channel(&df.channels, 1)?.scaled_samples();
assert_eq!(ch0_floats, vec![0.0, 1.0, 2.0, 3.0]);
assert_eq!(ch1_floats, vec![10.0, 11.0, 12.0, 13.0]);
Ok(())
}
#[test]
fn read_bytes_no_markers() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
assert_eq!(df.marker_count(), 0);
Ok(())
}
#[test]
fn read_bytes_metadata() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
assert_eq!(df.metadata.file_revision.0, 38);
assert!((df.metadata.samples_per_second - 1000.0).abs() < f64::EPSILON);
assert!(!df.metadata.compressed);
Ok(())
}
#[test]
fn read_stream_identical_to_read_bytes() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 5);
let from_bytes = read_bytes(&blob)?.value;
let from_stream = read_stream(std::io::Cursor::new(&blob))?.value;
assert_eq!(from_bytes.channel_count(), from_stream.channel_count());
assert_eq!(from_bytes.marker_count(), from_stream.marker_count());
for (b, s) in from_bytes.channels.iter().zip(from_stream.channels.iter()) {
assert_eq!(b.name, s.name);
assert_eq!(b.scaled_samples(), s.scaled_samples());
}
Ok(())
}
#[test]
fn read_file_roundtrip() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_read_file.acq")?;
let from_file = read_file(&path)?.value;
let from_bytes = read_bytes(&blob)?.value;
assert_eq!(from_file.channel_count(), from_bytes.channel_count());
for (f, b) in from_file.channels.iter().zip(from_bytes.channels.iter()) {
assert_eq!(f.name, b.name);
assert_eq!(f.scaled_samples(), b.scaled_samples());
}
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn datafile_channel_by_name() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
let ch = df.channel("CH0");
assert!(ch.is_some(), "channel 'CH0' should exist");
assert_eq!(
ch.ok_or_else(|| BiopacError::InvalidChannel("CH0".into()))?
.name,
"CH0"
);
assert!(df.channel("NOTEXIST").is_none());
Ok(())
}
#[test]
fn datafile_channels_iterator() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 4);
let df = read_bytes(&blob)?.value;
let names: Vec<&str> = df.channels().map(|c| c.name.as_str()).collect();
assert_eq!(names, vec!["CH0", "CH1", "CH2"]);
Ok(())
}
#[test]
fn datafile_revision() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let df = read_bytes(&blob)?.value;
assert_eq!(df.revision().0, 38);
Ok(())
}
#[test]
fn datafile_samples_per_second() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let df = read_bytes(&blob)?.value;
assert!((df.samples_per_second() - 1000.0).abs() < f64::EPSILON);
Ok(())
}
#[test]
fn datafile_duration() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = read_bytes(&blob)?.value;
let dur = df
.duration()
.ok_or_else(|| BiopacError::InvalidChannel("no duration".into()))?;
assert!((dur - 0.004).abs() < 1e-9, "expected 0.004 s, got {dur}");
Ok(())
}
#[test]
fn datafile_duration_none_for_empty() {
let df = Datafile {
metadata: biodream::GraphMetadata {
file_revision: biodream::FileRevision::new(38),
samples_per_second: 1000.0,
channel_count: 0,
byte_order: biodream::ByteOrder::LittleEndian,
compressed: false,
title: None,
acquisition_datetime: None,
max_samples_per_second: None,
},
channels: Vec::new(),
markers: Vec::new(),
journal: None,
};
assert!(df.duration().is_none());
}
#[test]
fn read_options_all_channels_default() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 4);
let df = ReadOptions::new().read_bytes(&blob)?.value;
assert_eq!(df.channel_count(), 3);
Ok(())
}
#[test]
fn read_options_channel_filter() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 4);
let df = ReadOptions::new()
.channels(&[0, 2])
.read_bytes(&blob)?
.value;
assert_eq!(df.channel_count(), 2);
assert_eq!(get_channel(&df.channels, 0)?.name, "CH0");
assert_eq!(get_channel(&df.channels, 1)?.name, "CH2");
Ok(())
}
#[test]
fn read_options_channel_filter_single() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 4);
let df = ReadOptions::new().channels(&[1]).read_bytes(&blob)?.value;
assert_eq!(df.channel_count(), 1);
assert_eq!(get_channel(&df.channels, 0)?.name, "CH1");
Ok(())
}
#[test]
fn read_options_out_of_range_index_silently_dropped() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = ReadOptions::new()
.channels(&[0, 99])
.read_bytes(&blob)?
.value;
assert_eq!(df.channel_count(), 1);
assert_eq!(get_channel(&df.channels, 0)?.name, "CH0");
Ok(())
}
#[test]
fn read_options_scaled_converts_to_float() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let df = ReadOptions::new().scaled(true).read_bytes(&blob)?.value;
match &get_channel(&df.channels, 0)?.data {
ChannelData::Float(_) | ChannelData::Scaled { .. } | ChannelData::Raw(_) => {
}
}
let floats = get_channel(&df.channels, 0)?.scaled_samples();
assert_eq!(floats, vec![0.0, 1.0, 2.0, 3.0]);
Ok(())
}
#[test]
fn read_options_read_stream() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = ReadOptions::new()
.channels(&[0])
.read_stream(std::io::Cursor::new(&blob))?
.value;
assert_eq!(df.channel_count(), 1);
assert_eq!(get_channel(&df.channels, 0)?.name, "CH0");
Ok(())
}
#[test]
fn read_options_read_file() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_read_opts_file.acq")?;
let df = ReadOptions::new().channels(&[1]).read_file(&path)?.value;
assert_eq!(df.channel_count(), 1);
assert_eq!(get_channel(&df.channels, 0)?.name, "CH1");
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn read_options_build_is_identity() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let df = ReadOptions::new()
.channels(&[0])
.scaled(false)
.build()
.read_bytes(&blob)?
.value;
assert_eq!(df.channel_count(), 1);
Ok(())
}
#[test]
fn open_file_does_not_load_data() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_open.acq")?;
let lazy = biodream::open_file(&path)?;
assert!(
!lazy.is_data_loaded(),
"open_file must not read sample data"
);
assert_eq!(lazy.channel_count(), 2);
assert_eq!(get_meta(&lazy.channel_metadata, 0)?.name, "CH0");
assert_eq!(get_meta(&lazy.channel_metadata, 1)?.name, "CH1");
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_parses_markers() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_markers.acq")?;
let lazy = biodream::open_file(&path)?;
assert_eq!(lazy.markers.len(), 0, "synthetic file has no markers");
assert!(lazy.journal.is_none(), "synthetic file has no journal");
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_load_channel() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_load_ch.acq")?;
let lazy = biodream::open_file(&path)?;
assert!(!lazy.is_data_loaded());
let ch0 = lazy.load_channel(0)?;
assert_eq!(ch0.name, "CH0");
assert_eq!(ch0.point_count, 4);
assert!(lazy.is_data_loaded());
let ch1 = lazy.load_channel(1)?;
assert_eq!(ch1.name, "CH1");
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_load_all() -> Result<(), BiopacError> {
let blob = build_pre4_acq(3, 4);
let path = write_tmp(&blob, "biodream_test_lazy_load_all.acq")?;
let lazy = biodream::open_file(&path)?;
let channels = lazy.load_all()?;
assert_eq!(channels.len(), 3);
assert_eq!(get_channel(channels, 0)?.name, "CH0");
assert_eq!(get_channel(channels, 2)?.name, "CH2");
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_into_datafile_before_load() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_into_df_cold.acq")?;
let lazy = biodream::open_file(&path)?;
assert!(!lazy.is_data_loaded());
let df = lazy.into_datafile()?;
assert_eq!(df.channel_count(), 2);
assert_eq!(
get_channel(&df.channels, 0)?.scaled_samples(),
vec![0.0, 1.0, 2.0, 3.0]
);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_into_datafile_after_load() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_into_df_warm.acq")?;
let lazy = biodream::open_file(&path)?;
let _ = lazy.load_channel(0)?; assert!(lazy.is_data_loaded());
let df = lazy.into_datafile()?;
assert_eq!(df.channel_count(), 2);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_out_of_bounds_channel() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_oob.acq")?;
let lazy = biodream::open_file(&path)?;
let result = lazy.load_channel(99);
assert!(result.is_err(), "index 99 should be out of bounds");
assert!(
matches!(result, Err(BiopacError::InvalidChannel(_))),
"expected InvalidChannel error"
);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn open_file_debug_repr() -> Result<(), BiopacError> {
let blob = build_pre4_acq(2, 4);
let path = write_tmp(&blob, "biodream_test_lazy_debug.acq")?;
let lazy = biodream::open_file(&path)?;
let s = format!("{lazy:?}");
assert!(s.contains("LazyDatafile"));
assert!(s.contains("channel_count"));
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn crate_read_bytes_is_accessible() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let df = biodream::read_bytes(&blob)?.value;
assert_eq!(df.channel_count(), 1);
Ok(())
}
#[test]
fn crate_read_stream_is_accessible() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let df = biodream::read_stream(std::io::Cursor::new(&blob))?.value;
assert_eq!(df.channel_count(), 1);
Ok(())
}
#[test]
fn crate_read_file_is_accessible() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let path = write_tmp(&blob, "biodream_test_crate_read_file.acq")?;
let df = biodream::read_file(&path)?.value;
assert_eq!(df.channel_count(), 1);
let _ = std::fs::remove_file(path);
Ok(())
}
#[test]
fn crate_open_file_is_accessible() -> Result<(), BiopacError> {
let blob = build_pre4_acq(1, 4);
let path = write_tmp(&blob, "biodream_test_crate_open_file.acq")?;
let lazy = biodream::open_file(&path)?;
assert_eq!(lazy.channel_count(), 1);
let _ = std::fs::remove_file(path);
Ok(())
}