#![forbid(unsafe_code)]
use std::fs;
use std::path::PathBuf;
use std::time::{Instant, SystemTime, UNIX_EPOCH};
use fsci_arrayapi::{
ArangeRequest, ArrayApiErrorKind, CreationRequest, DType, ExecutionMode, MemoryOrder,
ScalarValue, Shape, arange_with_audit, backend::CoreArrayBackend, from_slice_with_audit,
sync_audit_ledger,
};
use fsci_runtime::{AuditAction, Fingerprinter};
use serde::Serialize;
const PACKET_ID: &str = "FSCI-P2C-007";
#[derive(Debug, Clone, Serialize)]
struct CaseDiff {
case_id: String,
pass: bool,
note: String,
}
#[derive(Debug, Clone, Serialize)]
struct DiffLog {
test_id: String,
category: String,
case_count: usize,
pass: bool,
timestamp_ms: u128,
duration_ns: u128,
cases: Vec<CaseDiff>,
}
fn output_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!("fixtures/artifacts/{PACKET_ID}/diff"))
}
fn ensure_output_dir() {
fs::create_dir_all(output_dir()).expect("create audit_creation diff dir");
}
fn timestamp_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_millis())
}
fn emit_log(log: &DiffLog) {
ensure_output_dir();
let path = output_dir().join(format!("{}.json", log.test_id));
let json = serde_json::to_string_pretty(log).expect("serialize log");
fs::write(path, json).expect("write log");
}
fn ledger_snapshot(ledger: &fsci_arrayapi::SyncSharedAuditLedger) -> (usize, Vec<String>) {
let g = ledger.lock().expect("acquire ledger");
let reasons: Vec<String> = g
.entries()
.iter()
.map(|e| match &e.action {
AuditAction::FailClosed { reason } => reason.clone(),
other => format!("{other:?}"),
})
.collect();
(g.len(), reasons)
}
#[test]
fn diff_arrayapi_creation_audit_record() {
let start = Instant::now();
let mut diffs: Vec<CaseDiff> = Vec::new();
let backend = CoreArrayBackend::new(ExecutionMode::Strict);
{
let ledger = sync_audit_ledger();
let pre = ledger_snapshot(&ledger).0;
let req = ArangeRequest {
start: ScalarValue::F64(0.0),
stop: ScalarValue::F64(3.0),
step: ScalarValue::F64(1.0),
dtype: Some(DType::Float64),
};
let res = arange_with_audit(&backend, &req, &ledger);
let (post, _reasons) = ledger_snapshot(&ledger);
diffs.push(CaseDiff {
case_id: "arange_success_ok".into(),
pass: res.is_ok(),
note: format!("ok={}", res.is_ok()),
});
diffs.push(CaseDiff {
case_id: "arange_success_no_ledger".into(),
pass: pre == 0 && post == 0,
note: format!("pre={pre} post={post}"),
});
}
{
let ledger = sync_audit_ledger();
let req = ArangeRequest {
start: ScalarValue::F64(0.0),
stop: ScalarValue::F64(3.0),
step: ScalarValue::F64(0.0),
dtype: Some(DType::Float64),
};
let res = arange_with_audit(&backend, &req, &ledger);
let (post, reasons) = ledger_snapshot(&ledger);
let kind_matches = res
.as_ref()
.err()
.is_some_and(|e| e.kind == ArrayApiErrorKind::InvalidStep);
let reason_matches = reasons.first().is_some_and(|r| r == "arange::InvalidStep");
diffs.push(CaseDiff {
case_id: "arange_zero_step_err".into(),
pass: kind_matches,
note: format!("err={:?}", res.err()),
});
diffs.push(CaseDiff {
case_id: "arange_zero_step_ledger".into(),
pass: post == 1 && reason_matches,
note: format!("post={post} reasons={reasons:?}"),
});
}
{
let ledger = sync_audit_ledger();
let req = ArangeRequest {
start: ScalarValue::F64(0.0),
stop: ScalarValue::F64(3.0),
step: ScalarValue::F64(f64::NAN),
dtype: Some(DType::Float64),
};
let res = arange_with_audit(&backend, &req, &ledger);
let (post, reasons) = ledger_snapshot(&ledger);
diffs.push(CaseDiff {
case_id: "arange_nan_step_err".into(),
pass: res.is_err(),
note: format!("err={:?}", res.err()),
});
diffs.push(CaseDiff {
case_id: "arange_nan_step_ledger".into(),
pass: post == 1 && reasons.first().is_some_and(|r| r == "arange::InvalidStep"),
note: format!("post={post} reasons={reasons:?}"),
});
}
{
let ledger = sync_audit_ledger();
let req = CreationRequest {
shape: Shape::new(vec![2, 2]),
dtype: DType::Float64,
order: MemoryOrder::C,
};
let values = vec![
ScalarValue::F64(1.0),
ScalarValue::F64(2.0),
ScalarValue::F64(3.0),
ScalarValue::F64(4.0),
];
let res = from_slice_with_audit(&backend, &values, &req, &ledger);
let (post, _reasons) = ledger_snapshot(&ledger);
diffs.push(CaseDiff {
case_id: "from_slice_success_ok".into(),
pass: res.is_ok(),
note: format!("ok={}", res.is_ok()),
});
diffs.push(CaseDiff {
case_id: "from_slice_success_no_ledger".into(),
pass: post == 0,
note: format!("post={post}"),
});
}
{
let ledger = sync_audit_ledger();
let req = CreationRequest {
shape: Shape::new(vec![usize::MAX, 2]),
dtype: DType::Float64,
order: MemoryOrder::C,
};
let values = vec![ScalarValue::F64(1.0)];
let res = from_slice_with_audit(&backend, &values, &req, &ledger);
let (post, reasons) = ledger_snapshot(&ledger);
let kind_matches = res
.as_ref()
.err()
.is_some_and(|e| e.kind == ArrayApiErrorKind::Overflow);
diffs.push(CaseDiff {
case_id: "from_slice_overflow_err".into(),
pass: kind_matches,
note: format!("err={:?}", res.err()),
});
diffs.push(CaseDiff {
case_id: "from_slice_overflow_ledger".into(),
pass: post == 1 && reasons.first().is_some_and(|r| r == "from_slice::Overflow"),
note: format!("post={post} reasons={reasons:?}"),
});
}
{
let ledger = sync_audit_ledger();
let req = CreationRequest {
shape: Shape::new(vec![3]),
dtype: DType::Float64,
order: MemoryOrder::C,
};
let values = vec![ScalarValue::F64(1.0), ScalarValue::F64(2.0)];
let res = from_slice_with_audit(&backend, &values, &req, &ledger);
let (post, reasons) = ledger_snapshot(&ledger);
let kind_matches = res
.as_ref()
.err()
.is_some_and(|e| e.kind == ArrayApiErrorKind::InvalidShape);
diffs.push(CaseDiff {
case_id: "from_slice_len_mismatch_err".into(),
pass: kind_matches,
note: format!("err={:?}", res.err()),
});
diffs.push(CaseDiff {
case_id: "from_slice_len_mismatch_ledger".into(),
pass: post == 1
&& reasons
.first()
.is_some_and(|r| r == "from_slice::InvalidShape"),
note: format!("post={post} reasons={reasons:?}"),
});
}
{
let ledger = sync_audit_ledger();
let req = ArangeRequest {
start: ScalarValue::F64(0.0),
stop: ScalarValue::F64(3.0),
step: ScalarValue::F64(0.0),
dtype: Some(DType::Float64),
};
let _ = arange_with_audit(&backend, &req, &ledger);
let g = ledger.lock().expect("acquire");
let entry = &g.entries()[0];
let mut expected = Fingerprinter::new("fsci_arrayapi::arange");
expected.str("array_api").str("Strict");
for value in [0.0, 3.0, 0.0] {
expected.str("F64").f64(value);
}
expected.str("Some(Float64)");
let expected_fp = expected.finish();
diffs.push(CaseDiff {
case_id: "arange_fingerprint_matches".into(),
pass: entry.input_fingerprint == expected_fp,
note: format!("got={} expected={}", entry.input_fingerprint, expected_fp),
});
diffs.push(CaseDiff {
case_id: "arange_outcome_rejected".into(),
pass: entry.outcome == "rejected",
note: format!("outcome={}", entry.outcome),
});
}
let all_pass = diffs.iter().all(|d| d.pass);
let log = DiffLog {
test_id: "diff_arrayapi_creation_audit_record".into(),
category: "arange_with_audit + from_slice_with_audit ledger semantics".into(),
case_count: diffs.len(),
pass: all_pass,
timestamp_ms: timestamp_ms(),
duration_ns: start.elapsed().as_nanos(),
cases: diffs.clone(),
};
emit_log(&log);
for d in &diffs {
if !d.pass {
eprintln!("audit_creation mismatch: {} — {}", d.case_id, d.note);
}
}
assert!(
all_pass,
"arange/from_slice audit coverage failed: {} cases",
diffs.len(),
);
}