conspire 0.7.1

The Rust interface to conspire.
Documentation
use super::{Npy, NpyType};
use crate::io::Write;
use std::{
    fs::write,
    io::{self, Error, Result},
};

fn npy_bytes(version: u8, descr: &str, count: usize, data: &[u8]) -> Vec<u8> {
    let header = format!("{{'descr': '{descr}', 'fortran_order': False, 'shape': ({count}, ), }}");
    let mut bytes = b"\x93NUMPY".to_vec();
    bytes.push(version);
    bytes.push(0);
    if version == 2 {
        bytes.extend_from_slice(&(header.len() as u32).to_le_bytes());
    } else {
        bytes.extend_from_slice(&(header.len() as u16).to_le_bytes());
    }
    bytes.extend_from_slice(header.as_bytes());
    bytes.extend_from_slice(data);
    bytes
}

#[test]
fn not_npy_errors() {
    let path = "target/npy_not.npy";
    write(path, b"this is not a numpy file").unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn reads_version_2() {
    let path = "target/npy_v2.npy";
    write(path, npy_bytes(2, "|u1", 3, &[7, 8, 9])).unwrap();
    let npy = Npy::<u8>::read(path).unwrap();
    assert_eq!(npy.data, [7, 8, 9]);
    assert_eq!(npy.shape, [3]);
}

#[test]
fn unsupported_version_errors() {
    let path = "target/npy_v3.npy";
    write(path, npy_bytes(3, "|u1", 3, &[7, 8, 9])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn truncated_errors() {
    let path = "target/npy_truncated.npy";
    write(path, npy_bytes(1, "|u1", 10, &[1, 2])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

fn npy_raw(header: &[u8], data: &[u8]) -> Vec<u8> {
    let mut bytes = b"\x93NUMPY".to_vec();
    bytes.push(1);
    bytes.push(0);
    bytes.extend_from_slice(&(header.len() as u16).to_le_bytes());
    bytes.extend_from_slice(header);
    bytes.extend_from_slice(data);
    bytes
}

#[test]
fn non_utf8_header_errors() {
    let path = "target/npy_badutf8.npy";
    write(path, npy_raw(&[0xff, 0xfe, 0x00], &[])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn missing_descr_errors() {
    let path = "target/npy_nodescr.npy";
    write(
        path,
        npy_raw(b"{'fortran_order': False, 'shape': (3, ), }", &[1, 2, 3]),
    )
    .unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn big_endian_descr_errors() {
    let path = "target/npy_be.npy";
    let header = b"{'descr': '>u2', 'fortran_order': False, 'shape': (1, ), }";
    write(path, npy_raw(header, &[0, 1])).unwrap();
    assert!(Npy::<u16>::read(path).is_err());
}

#[test]
fn missing_shape_errors() {
    let path = "target/npy_noshape.npy";
    write(
        path,
        npy_raw(b"{'descr': '|u1', 'fortran_order': False, }", &[1]),
    )
    .unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn shape_without_paren_errors() {
    let path = "target/npy_noparen.npy";
    write(path, npy_raw(b"{'descr': '|u1', 'shape': 3, }", &[1, 2, 3])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn unclosed_shape_errors() {
    let path = "target/npy_unclosed.npy";
    write(
        path,
        npy_raw(b"{'descr': '|u1', 'shape': (3, }", &[1, 2, 3]),
    )
    .unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn bad_shape_entry_errors() {
    let path = "target/npy_badentry.npy";
    write(path, npy_raw(b"{'descr': '|u1', 'shape': (x, ), }", &[1])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn descr_without_value_quote_errors() {
    let path = "target/npy_descr_noquote.npy";
    write(path, npy_raw(b"{'descr': }", &[])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn descr_unterminated_value_errors() {
    let path = "target/npy_descr_unterminated.npy";
    write(path, npy_raw(b"{'descr': '|u1 }", &[])).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn write_to_bad_path_errors() {
    let npy = Npy {
        data: vec![1u8, 2, 3],
        shape: vec![3],
        fortran_order: true,
    };
    assert!(npy.write("target/nonexistent_dir/x.npy").is_err());
}

#[test]
fn read_missing_file_errors() {
    assert!(Npy::<u8>::read("target/does_not_exist.npy").is_err());
}

struct FailOnFlush;

impl io::Write for FailOnFlush {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        Ok(buf.len())
    }
    fn flush(&mut self) -> Result<()> {
        Err(Error::other("boom"))
    }
}

#[test]
fn write_to_propagates_flush_error() {
    let npy = Npy {
        data: vec![1u8, 2, 3],
        shape: vec![3],
        fortran_order: true,
    };
    assert!(npy.write_to(&mut FailOnFlush).is_err());
}

struct FailOnNthWrite {
    allowed: usize,
    count: usize,
}

impl io::Write for FailOnNthWrite {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        if self.count >= self.allowed {
            return Err(Error::other("boom"));
        }
        self.count += 1;
        Ok(buf.len())
    }
    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

#[test]
fn write_to_propagates_write_errors_at_every_step() {
    let npy = Npy {
        data: vec![1u8, 2, 3],
        shape: vec![3],
        fortran_order: true,
    };
    for allowed in 0..5 {
        assert!(
            npy.write_to(&mut FailOnNthWrite { allowed, count: 0 })
                .is_err()
        );
    }
    assert!(
        npy.write_to(&mut FailOnNthWrite {
            allowed: 5,
            count: 0
        })
        .is_ok()
    );
}

#[test]
fn read_truncated_prefix_errors() {
    let path = "target/npy_short_prefix.npy";
    write(path, &b"\x93NUMPY\x01\x00"[..]).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn read_truncated_version_2_header_length_errors() {
    let path = "target/npy_short_v2_prefix.npy";
    write(path, &b"\x93NUMPY\x02\x00\x00\x00"[..]).unwrap();
    assert!(Npy::<u8>::read(path).is_err());
}

#[test]
fn round_trip_fortran_order() {
    let data: Vec<u16> = (0..24).collect();
    let path = "target/npy_f.npy";
    Npy {
        data: data.clone(),
        shape: vec![2, 3, 4],
        fortran_order: true,
    }
    .write(path)
    .unwrap();
    let npy = Npy::<u16>::read(path).unwrap();
    assert_eq!(npy.data, data);
    assert_eq!(npy.shape, [2, 3, 4]);
    assert!(npy.fortran_order);
}

#[test]
fn round_trip_c_order_f64() {
    let data = vec![1.5_f64, -2.0, 3.25];
    let path = "target/npy_c.npy";
    Npy {
        data: data.clone(),
        shape: vec![3],
        fortran_order: false,
    }
    .write(path)
    .unwrap();
    let npy = Npy::<f64>::read(path).unwrap();
    assert_eq!(npy.data, data);
    assert_eq!(npy.shape, [3]);
    assert!(!npy.fortran_order);
}

#[test]
fn write_le_bytes_and_read_le_all_round_trip() {
    let data = vec![1.5_f64, -2.0, 3.25];
    let bytes = f64::write_le_bytes(&data);
    assert_eq!(f64::read_le_all(&bytes), data);

    let data = vec![1u8, 2, 3];
    let bytes = u8::write_le_bytes(&data);
    assert_eq!(u8::read_le_all(&bytes), data);
}

#[test]
fn dtype_mismatch_errors() {
    let path = "target/npy_dtype.npy";
    Npy {
        data: vec![1u8, 2, 3],
        shape: vec![3],
        fortran_order: true,
    }
    .write(path)
    .unwrap();
    assert!(Npy::<u16>::read(path).is_err());
}