matten 0.20.16

A family-car multidimensional array (tensor) library for small numerical trials / PoCs.
Documentation
#[cfg(any(feature = "json", feature = "csv"))]
use crate::{MattenError, Tensor};

// ---- serde round-trip ---------------------------------------------------

#[cfg(feature = "json")]
#[test]
fn serde_canonical_roundtrip() {
    let t = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], &[2, 2]);
    let json = serde_json::to_string(&t).unwrap();
    assert!(json.contains("\"shape\""));
    assert!(json.contains("\"data\""));
    let t2: Tensor = serde_json::from_str(&json).unwrap();
    assert_eq!(t, t2);
}

#[cfg(feature = "json")]
#[test]
fn serde_canonical_scalar() {
    let s = Tensor::scalar(42.0);
    let json = serde_json::to_string(&s).unwrap();
    let s2: Tensor = serde_json::from_str(&json).unwrap();
    assert_eq!(s, s2);
}

#[cfg(feature = "json")]
#[test]
fn serde_deserialize_rejects_shape_mismatch() {
    let bad = r#"{"shape":[2,2],"data":[1.0,2.0,3.0]}"#;
    let err: Result<Tensor, _> = serde_json::from_str(bad);
    assert!(err.is_err());
}

// ---- from_json object form ----------------------------------------------

#[cfg(feature = "json")]
#[test]
fn from_json_object_form() {
    let t = Tensor::from_json(r#"{"shape":[2,2],"data":[1.0,2.0,3.0,4.0]}"#).unwrap();
    assert_eq!(t.shape(), &[2, 2]);
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
}

#[cfg(feature = "json")]
#[test]
fn from_json_object_1d() {
    let t = Tensor::from_json(r#"{"shape":[3],"data":[1.0,2.0,3.0]}"#).unwrap();
    assert!(t.is_vector());
    assert_eq!(t.len(), 3);
}

#[cfg(feature = "json")]
#[test]
fn from_json_object_scalar() {
    let t = Tensor::from_json(r#"{"shape":[],"data":[99.0]}"#).unwrap();
    assert!(t.is_scalar());
    assert_eq!(t.as_slice(), &[99.0]);
}

#[cfg(feature = "json")]
#[test]
fn from_json_object_missing_field_is_err() {
    assert!(matches!(
        Tensor::from_json(r#"{"shape":[2]}"#),
        Err(MattenError::Parse { .. })
    ));
    assert!(matches!(
        Tensor::from_json(r#"{"data":[1.0]}"#),
        Err(MattenError::Parse { .. })
    ));
}

#[cfg(feature = "json")]
#[test]
fn from_json_object_shape_mismatch_is_err() {
    let err = Tensor::from_json(r#"{"shape":[2,2],"data":[1.0,2.0,3.0]}"#).unwrap_err();
    assert!(matches!(err, MattenError::Parse { .. }));
}

// ---- from_json nested-array form ----------------------------------------

#[cfg(feature = "json")]
#[test]
fn from_json_nested_2d() {
    let t = Tensor::from_json("[[1.0,2.0],[3.0,4.0]]").unwrap();
    assert_eq!(t.shape(), &[2, 2]);
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
}

#[cfg(feature = "json")]
#[test]
fn from_json_nested_1d() {
    let t = Tensor::from_json("[1.0,2.0,3.0]").unwrap();
    assert_eq!(t.shape(), &[3]);
}

#[cfg(feature = "json")]
#[test]
fn from_json_ragged_is_err() {
    let err = Tensor::from_json("[[1.0,2.0],[3.0]]").unwrap_err();
    assert!(matches!(err, MattenError::Parse { .. }));
    assert!(err.to_string().contains("ragged"));
}

#[cfg(feature = "json")]
#[test]
fn from_json_non_numeric_is_err() {
    assert!(matches!(
        Tensor::from_json(r#"[[1.0,"hello"]]"#),
        Err(MattenError::Parse { .. })
    ));
    assert!(matches!(
        Tensor::from_json(r#"[[1.0,null]]"#),
        Err(MattenError::Parse { .. })
    ));
    assert!(matches!(
        Tensor::from_json(r#"[[1.0,true]]"#),
        Err(MattenError::Parse { .. })
    ));
}

#[cfg(feature = "json")]
#[test]
fn from_json_malformed_never_panics() {
    for bad in &["{", "null", "\"string\"", "", "[][]", "[[[[]]]]]"] {
        let _ = Tensor::from_json(bad); // must not panic
    }
}

// ---- from_csv -----------------------------------------------------------

#[cfg(feature = "csv")]
#[test]
fn from_csv_basic() {
    let t = Tensor::from_csv("1.0,2.0\n3.0,4.0\n").unwrap();
    assert_eq!(t.shape(), &[2, 2]);
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
}

#[cfg(feature = "csv")]
#[test]
fn from_csv_1_row() {
    let t = Tensor::from_csv("1.0,2.0,3.0\n").unwrap();
    assert_eq!(t.shape(), &[1, 3]);
}

#[cfg(feature = "csv")]
#[test]
fn from_csv_whitespace_fields() {
    let t = Tensor::from_csv(" 1.0 , 2.0 \n 3.0 , 4.0 \n").unwrap();
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
}

#[cfg(feature = "csv")]
#[test]
fn from_csv_ragged_is_err() {
    let err = Tensor::from_csv("1.0,2.0\n3.0\n").unwrap_err();
    assert!(matches!(err, MattenError::Parse { .. }));
}

#[cfg(feature = "csv")]
#[test]
fn from_csv_non_numeric_is_err() {
    let err = Tensor::from_csv("1.0,active\n3.0,4.0\n").unwrap_err();
    assert!(matches!(err, MattenError::Parse { .. }));
    assert!(err.to_string().contains("column"));
}

#[cfg(feature = "csv")]
#[test]
fn from_csv_empty_is_err() {
    assert!(matches!(
        Tensor::from_csv(""),
        Err(MattenError::Parse { .. })
    ));
}

// ---- load_json / load_csv via fixture files -----------------------------

#[cfg(feature = "json")]
#[test]
fn load_json_missing_file_is_io_err() {
    let err = Tensor::load_json("/nonexistent/path/tensor.json").unwrap_err();
    assert!(matches!(err, MattenError::Io { .. }));
}

#[cfg(feature = "csv")]
#[test]
fn load_csv_missing_file_is_io_err() {
    let err = Tensor::load_csv("/nonexistent/path/data.csv").unwrap_err();
    assert!(matches!(err, MattenError::Io { .. }));
}

#[cfg(feature = "json")]
#[test]
fn load_json_fixture() {
    let t = Tensor::load_json("examples/data/tensor_2x2.json").unwrap();
    assert_eq!(t.shape(), &[2, 2]);
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
}

#[cfg(feature = "csv")]
#[test]
fn load_csv_fixture() {
    let t = Tensor::load_csv("examples/data/numeric_2x3.csv").unwrap();
    assert_eq!(t.shape(), &[2, 3]);
    assert_eq!(t.as_slice(), &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
}

// ---- allocation / size limit tests (RFC-009 §13) -------------------------

#[cfg(feature = "json")]
#[test]
fn from_json_oversized_array_is_err() {
    // MAX_JSON_ELEMENTS = 1 << 24 (~16M). Build a JSON array description
    // that would claim to be larger than the limit without actually
    // allocating 16M elements — check that the length guard triggers.
    // We test the nested-form path by crafting a deeply nested description
    // instead (the depth limit triggers first at depth > MAX_NESTING=8).
    let deep = (0..10).fold("1.0".to_string(), |acc, _| format!("[{acc}]"));
    let err = Tensor::from_json(&deep).unwrap_err();
    assert!(matches!(err, MattenError::Parse { .. }));
    assert!(
        err.to_string().contains("depth")
            || err.to_string().contains("limit")
            || err.to_string().contains("Parse")
    );
}

#[cfg(feature = "json")]
#[test]
fn from_json_slice_str_length_limit() {
    // slice_str has MAX_SLICE_STR_BYTES = 512
    let t = Tensor::zeros(&[2]);
    let long_spec = "0:1,".repeat(200); // > 512 bytes
    let err = t.slice_str(&long_spec).unwrap_err();
    assert!(matches!(err, MattenError::Slice { .. }));
    assert!(err.to_string().contains("maximum length"));
}