//! The native sink (extensions/native-sink.md): the committed corpus //! `live-conformance/native-sink/vectors.json` regenerates byte for byte from its fixed seeds and //! is the only file there besides its README; every call in it gets the answer it states; every //! reason §8 names is reached by a vector; and the C ABI (`ffi_sink`) gives the answers the Rust //! module gives, through the symbols a C host links. #![cfg(feature = "native_sink")] use std::ffi::CString; use std::path::PathBuf; use serde_json::{json, Value}; use wai::ffi::{wai_buffer_free, WaiBuffer, WAI_OK}; use wai::ffi_sink::*; use wai::native_sink::corpus; fn wai_dir() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..") } fn committed() -> Vec { std::fs::read(wai_dir().join("live-conformance/native-sink/vectors.json")).unwrap() } #[test] fn the_committed_corpus_regenerates_from_its_seeds() { assert!(committed() == corpus::bytes(&corpus::generate()), "live-conformance/native-sink/vectors.json differs from `wai_native_sink_vectors gen`"); let mut files: Vec = std::fs::read_dir(wai_dir().join("live-conformance/native-sink")).unwrap().flatten().map(|e| e.file_name().to_string_lossy().into_owned()).collect(); files.sort(); assert_eq!(files, ["README.md", "vectors.json"]); } #[test] fn every_call_gets_its_stated_answer() { let doc: Value = serde_json::from_slice(&committed()).unwrap(); corpus::verify(&doc).unwrap(); } /// Every reason of §8 is reached by a vector, and the extension's table lists exactly those. #[test] fn every_reason_refuses_a_vector() { let doc: Value = serde_json::from_slice(&committed()).unwrap(); let text = std::fs::read_to_string(wai_dir().join("extensions/native-sink.md")).unwrap(); let mut reached = std::collections::BTreeSet::new(); for c in doc["cases"].as_array().unwrap() { for call in c["calls"].as_array().unwrap() { for k in ["refused", "error"] { if let Some(r) = call["answer"][k].as_str() { reached.insert(r.to_string()); } } } } for c in doc["configs"].as_array().unwrap() { reached.insert(c["refused"].as_str().unwrap().to_string()); } let table: std::collections::BTreeSet = text .split("## 8. Reasons") .nth(1) .unwrap() .split("\n## ") .next() .unwrap() .lines() .filter_map(|l| l.strip_prefix("| `").and_then(|r| r.split('`').next()).map(str::to_string)) .collect(); assert_eq!(reached, table, "the reasons the corpus reaches and §8's table"); } fn take(b: WaiBuffer) -> Vec { let v = unsafe { std::slice::from_raw_parts(b.data, b.len) }.to_vec(); unsafe { wai_buffer_free(b) }; v } fn empty() -> WaiBuffer { WaiBuffer { data: std::ptr::null_mut(), len: 0, cap: 0 } } /// Every case, run through the C ABI, gets the answers the corpus states for it. #[test] fn the_c_abi_gives_the_corpus_answers() { assert_eq!(wai_sink_abi_version(), WAI_SINK_ABI_VERSION); let doc: Value = serde_json::from_slice(&committed()).unwrap(); let obj = |n: &Value| -> Vec { n.as_str().map(|n| wai::stream_audit::unhex(doc["objects"][n].as_str().unwrap()).unwrap()).unwrap_or_default() }; for case in doc["cases"].as_array().unwrap() { let cfg = CString::new(case["config"].to_string()).unwrap(); let seed = wai::stream_audit::unhex(case["seed"].as_str().unwrap()).unwrap(); let mut s: *mut WaiSink = std::ptr::null_mut(); assert_eq!(unsafe { wai_sink_open(cfg.as_ptr(), seed.as_ptr(), 32, case["start_us"].as_u64().unwrap(), &mut s) }, WAI_OK); for call in case["calls"].as_array().unwrap() { let d = &call["do"]; let want = &call["answer"]; let mut out = empty(); let got: Value = match d["call"].as_str().unwrap() { "playlist" => { let url = CString::new(d["url"].as_str().unwrap()).unwrap(); let b = obj(&d["body"]); let r = obj(&d["receipt"]); let rp = if d["receipt"].is_null() { std::ptr::null() } else { r.as_ptr() }; let st = unsafe { wai_sink_playlist(s, url.as_ptr(), b.as_ptr(), b.len(), rp, r.len(), &mut out) }; if st == WAI_OK { serde_json::from_slice(&take(out)).unwrap() } else { json!({"error": last_reason()}) } } "parent" => { let url = CString::new(d["url"].as_str().unwrap()).unwrap(); let name = CString::new(d["name"].as_str().unwrap()).unwrap(); let b = obj(&d["body"]); let bp = if d["body"].is_null() { std::ptr::null() } else { b.as_ptr() }; let st = unsafe { wai_sink_playlist_parent(s, url.as_ptr(), name.as_ptr(), bp, b.len()) }; if st == WAI_OK { json!({"ok": true}) } else { json!({"error": last_reason()}) } } "object" => { let meta = CString::new(d["meta"].to_string()).unwrap(); let b = obj(&d["body"]); let st = unsafe { wai_sink_object(s, meta.as_ptr(), b.as_ptr(), b.len(), &mut out) }; if st == WAI_OK { serde_json::from_slice(&take(out)).unwrap() } else { json!({"error": last_reason()}) } } "event" => { let n = CString::new(d["name"].as_str().unwrap()).unwrap(); let st = unsafe { wai_sink_event(s, n.as_ptr(), d["t_us"].as_u64().unwrap()) }; if st == WAI_OK { json!({"ok": true}) } else { json!({"error": last_reason()}) } } "observe" => { let o = CString::new(d["obs"].to_string()).unwrap(); let st = unsafe { wai_sink_observe(s, o.as_ptr(), &mut out) }; if st == WAI_OK { serde_json::from_slice(&take(out)).unwrap() } else { json!({"error": last_reason()}) } } "energy" => { let e = CString::new(d["entry"].to_string()).unwrap(); let st = unsafe { wai_sink_energy(s, e.as_ptr()) }; if st == WAI_OK { json!({"ok": true}) } else { json!({"error": last_reason()}) } } "attach" => { let n = CString::new(d["name"].as_str().unwrap()).unwrap(); let b = obj(&d["body"]); let st = unsafe { wai_sink_attach(s, n.as_ptr(), b.as_ptr(), b.len()) }; if st == WAI_OK { json!({"ok": true}) } else { json!({"error": last_reason()}) } } "cmcd" => { let st = unsafe { wai_sink_take_cmcd(s, &mut out) }; if st == WAI_OK { json!({"cmcd": String::from_utf8(take(out)).unwrap()}) } else { json!({"error": last_reason()}) } } "end" => { let n = CString::new(d["ended"].as_str().unwrap()).unwrap(); let st = unsafe { wai_sink_end(s, n.as_ptr(), d["t_us"].as_u64().unwrap(), &mut out) }; if st == WAI_OK { json!({"bundle": String::from_utf8(take(out)).unwrap()}) } else { json!({"error": last_reason()}) } } other => panic!("{other}"), }; assert_eq!(&got, want, "{}: {d}", case["name"]); } unsafe { wai_sink_free(s) }; } } fn last_reason() -> String { let p = wai::ffi::wai_last_error(); let s = unsafe { std::ffi::CStr::from_ptr(p) }.to_string_lossy().into_owned(); s.split(':').next().unwrap().to_string() } /// A session with no seed has a fresh key: two sessions never share one (stream-audit §3.2). #[test] fn a_session_without_a_seed_has_a_fresh_key() { let cfg = CString::new(json!({"channel": "film-library", "owner": "00".repeat(32)}).to_string()).unwrap(); let mut players = std::collections::BTreeSet::new(); for _ in 0..8 { let mut s: *mut WaiSink = std::ptr::null_mut(); assert_eq!(unsafe { wai_sink_open(cfg.as_ptr(), std::ptr::null(), 0, 1, &mut s) }, WAI_OK); let mut out = empty(); assert_eq!(unsafe { wai_sink_info(s, &mut out) }, WAI_OK); let info: Value = serde_json::from_slice(&take(out)).unwrap(); players.insert(info["player"].as_str().unwrap().to_string()); unsafe { wai_sink_free(s) }; } assert_eq!(players.len(), 8); } /// Bad inputs return the sink's status codes, never a panic across the boundary. #[test] fn the_c_abi_refuses_bad_inputs_with_its_codes() { let mut s: *mut WaiSink = std::ptr::null_mut(); let bad = CString::new("{\"channel\": 1}").unwrap(); assert_eq!(unsafe { wai_sink_open(bad.as_ptr(), std::ptr::null(), 0, 1, &mut s) }, WAI_ERR_SINK_INPUT); assert!(s.is_null()); let not_json = CString::new("{").unwrap(); assert_eq!(unsafe { wai_sink_open(not_json.as_ptr(), std::ptr::null(), 0, 1, &mut s) }, WAI_ERR_SINK_INPUT); let cfg = CString::new(json!({"channel": "film-library", "owner": "00".repeat(32)}).to_string()).unwrap(); assert_eq!(unsafe { wai_sink_open(cfg.as_ptr(), [1u8; 31].as_ptr(), 31, 1, &mut s) }, wai::ffi::WAI_ERR_INVALID_ARG); assert_eq!(unsafe { wai_sink_open(cfg.as_ptr(), std::ptr::null(), 0, 1, &mut s) }, WAI_OK); let mut out = empty(); let e = CString::new("ended").unwrap(); assert_eq!(unsafe { wai_sink_end(std::ptr::null(), e.as_ptr(), 2, &mut out) }, wai::ffi::WAI_ERR_INVALID_ARG); let u = CString::new("https://a.example/x").unwrap(); assert_eq!(unsafe { wai_sink_playlist(s, u.as_ptr(), std::ptr::null(), 4, std::ptr::null(), 0, &mut out) }, wai::ffi::WAI_ERR_INVALID_ARG); let meta = CString::new(json!({"url": "https://a.example/x", "t_us": 2}).to_string()).unwrap(); assert_eq!(unsafe { wai_sink_object(s, meta.as_ptr(), b"x".as_ptr(), 1, &mut out) }, WAI_OK); unsafe { wai_buffer_free(out) }; let mut out = empty(); assert_eq!(unsafe { wai_sink_end(s, e.as_ptr(), 3, &mut out) }, WAI_OK); unsafe { wai_buffer_free(out) }; let mut out = empty(); assert_eq!(unsafe { wai_sink_end(s, e.as_ptr(), 4, &mut out) }, WAI_ERR_SINK_ENDED); assert!(out.data.is_null()); unsafe { wai_sink_free(s) }; }