tenferro-cpu 0.4.0

CPU backend, kernels, provider selection, and CPU resource pools for tenferro.
use std::panic::{catch_unwind, AssertUnwindSafe};

use super::{
    dynamic_slice, f32_index_to_i64, f64_index_to_i64, inline_col_major_strides, typed_concatenate,
    BufferPool,
};
use strided_kernel::ExecContext;
use tenferro_tensor::{Error, ErrorKind, Tensor, TypedTensor, ValidationError, ValidationKind};

#[test]
fn typed_concatenate_rejects_empty_typed_inputs_without_panicking() {
    let mut buffers = BufferPool::new();

    let result = catch_unwind(AssertUnwindSafe(|| {
        typed_concatenate::<f64>(&mut buffers, &ExecContext::serial(), &[], 0)
    }));

    assert!(result.is_ok(), "empty typed concatenate should return Err");
    assert!(matches!(
        result.unwrap().unwrap_err(),
        Error::Validation {
            op: "concatenate",
            ..
        }
    ));
}

#[test]
fn typed_concatenate_accepts_nonempty_typed_inputs() {
    let mut buffers = BufferPool::new();
    let a = TypedTensor::from_vec_col_major(vec![2], vec![1.0, 2.0]).unwrap();
    let b = TypedTensor::from_vec_col_major(vec![1], vec![3.0]).unwrap();
    let inputs = vec![&a, &b];

    let out = typed_concatenate(&mut buffers, &ExecContext::serial(), &inputs, 0).unwrap();

    assert_eq!(out.shape(), &[3]);
    assert_eq!(out.host_data().unwrap(), &[1.0, 2.0, 3.0]);
}

#[test]
fn inline_strides_cover_zero_extent_and_spill_without_overflow() {
    let dims = [2, 0, usize::MAX, 3, 5, 7, 11, 13, 17];
    let strides = inline_col_major_strides("test_static_indexing", &dims).unwrap();

    assert_eq!(strides.len(), 9);
    assert_eq!(&strides[..4], &[1, 2, 0, 0]);
}

#[test]
fn inline_strides_report_isize_overflow_as_typed_error() {
    let error = inline_col_major_strides("test_static_indexing", &[usize::MAX, 2])
        .expect_err("oversized dimensions must not panic or wrap");

    assert_eq!(
        error.kind(),
        ErrorKind::Validation(ValidationKind::InvalidArgument)
    );
}

#[test]
fn float_index_validation_identifies_the_index_argument() {
    for error in [
        f32_index_to_i64(1.5).unwrap_err(),
        f64_index_to_i64(f64::NAN).unwrap_err(),
    ] {
        assert!(matches!(
            error,
            Error::Validation {
                op: "index_tensor",
                source: ValidationError::InvalidArgument {
                    argument: "index",
                    ..
                },
            }
        ));
    }
}

#[test]
fn dynamic_slice_validation_identifies_the_starts_argument() {
    let input = Tensor::from_vec_col_major(vec![3], vec![1.0_f64, 2.0, 3.0]).unwrap();
    let wrong_rank = Tensor::from_vec_col_major(vec![], vec![0_i64]).unwrap();
    let wrong_length = Tensor::from_vec_col_major(vec![2], vec![0_i64, 1]).unwrap();

    for starts in [&wrong_rank, &wrong_length] {
        let error = dynamic_slice(&input, starts, &[1]).unwrap_err();
        assert!(matches!(
            error,
            Error::Validation {
                op: "dynamic_slice",
                source: ValidationError::InvalidArgument {
                    argument: "starts",
                    ..
                },
            }
        ));
    }
}