tetration 0.1.8

Chunked tensor .tet format, tet CLI, and optional C ABI (tetration-ffi)
Documentation
//! C ABI smoke tests (`cargo test --lib --features tetration-ffi ffi`).

use std::ffi::{CStr, CString};
use std::ptr;

use crate::ffi::{
    TET_ABI_VERSION, tet_abi_version, tet_close, tet_last_error, tet_open, tet_query_json,
    tet_string_free, tet_summary_json, tet_verify_json,
};

use super::fixture::tracked_small_tet_dir;

const FFI_EXPORTS: &[&str] = &[
    "tet_abi_version",
    "tet_clear_error",
    "tet_close",
    "tet_last_error",
    "tet_open",
    "tet_query_json",
    "tet_string_free",
    "tet_summary_json",
    "tet_verify_json",
];

fn header_abi_version(header: &str) -> u32 {
    for line in header.lines() {
        let line = line.trim();
        if let Some(rest) = line.strip_prefix("#define TET_ABI_VERSION") {
            let digits: String = rest.chars().filter(char::is_ascii_digit).collect();
            return digits.parse().expect("TET_ABI_VERSION digits");
        }
    }
    panic!("TET_ABI_VERSION not found in include/tetration.h");
}

#[test]
fn ffi_header_in_sync_with_rust() {
    let header = include_str!("../../include/tetration.h");
    let rust_src = include_str!("../ffi/mod.rs");
    assert_eq!(header_abi_version(header), TET_ABI_VERSION);
    for sym in FFI_EXPORTS {
        assert!(
            header.contains(&format!("{sym}(")),
            "{sym} missing from include/tetration.h"
        );
        assert!(
            rust_src.contains(&format!("fn {sym}")),
            "{sym} missing from src/ffi/mod.rs"
        );
    }
}

fn sample_path() -> CString {
    let p = tracked_small_tet_dir().join("sample.tet");
    assert!(
        p.is_file(),
        "missing {}; commit fixtures/small/tet/ or regenerate",
        p.display()
    );
    CString::new(p.to_str().expect("fixture path utf-8")).expect("no nul in path")
}

#[test]
fn ffi_abi_version() {
    assert_eq!(unsafe { tet_abi_version() }, TET_ABI_VERSION);
}

#[test]
fn ffi_open_summary_query_close() {
    let path = sample_path();
    unsafe {
        let h = tet_open(path.as_ptr());
        assert!(
            !h.is_null(),
            "{}",
            CStr::from_ptr(tet_last_error()).to_string_lossy()
        );

        let summary = tet_summary_json(h);
        assert!(
            !summary.is_null(),
            "{}",
            CStr::from_ptr(tet_last_error()).to_string_lossy()
        );
        let summary_s = CStr::from_ptr(summary).to_str().unwrap();
        assert!(summary_s.contains("temperature"));
        tet_string_free(summary);

        let q = CString::new(r#"{"dataset":"temperature","mean":[]}"#).unwrap();
        let out = tet_query_json(h, q.as_ptr());
        assert!(
            !out.is_null(),
            "{}",
            CStr::from_ptr(tet_last_error()).to_string_lossy()
        );
        let out_s = CStr::from_ptr(out).to_str().unwrap();
        assert!(out_s.contains("operation_mean"));
        tet_string_free(out);

        tet_close(h);
        tet_close(ptr::null_mut());
    }
}

#[test]
fn ffi_verify_json() {
    let path = sample_path();
    unsafe {
        let out = tet_verify_json(path.as_ptr());
        assert!(
            !out.is_null(),
            "{}",
            CStr::from_ptr(tet_last_error()).to_string_lossy()
        );
        let s = CStr::from_ptr(out).to_str().unwrap();
        assert!(s.contains("\"ok\":true"));
        tet_string_free(out);
    }
}

#[test]
fn ffi_open_invalid_path() {
    let bad = CString::new("/nonexistent/tetration/sample.tet").unwrap();
    unsafe {
        let h = tet_open(bad.as_ptr());
        assert!(h.is_null());
        assert!(!CStr::from_ptr(tet_last_error()).to_bytes().is_empty());
    }
}