49d9552476
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VdfEGXA7aJ6ftMsCc9PJze
99 lines
2.9 KiB
Rust
99 lines
2.9 KiB
Rust
//! A circuit moves between threads, one at a time, and its callbacks run on whichever thread
|
|
//! steps it (`context/code/zset-ffi.md`, decision 10). Evidence: `context/test-evidence/zset-ffi.md`.
|
|
|
|
use std::ffi::c_void;
|
|
use std::ptr::null_mut;
|
|
use std::sync::Mutex;
|
|
use std::thread::{self, ThreadId};
|
|
use zset_ffi::*;
|
|
|
|
const OK: i32 = 0;
|
|
|
|
fn is_send<T: Send>() {}
|
|
|
|
#[test]
|
|
fn the_opaque_handles_are_send() {
|
|
is_send::<ZsCircuit>();
|
|
is_send::<ZsRebuild>();
|
|
}
|
|
|
|
/// The caller's context: which threads ran the callback.
|
|
struct Seen(Mutex<Vec<ThreadId>>);
|
|
|
|
extern "C" fn keep_all(ctx: *mut c_void, _: *const u8, _: usize) -> i32 {
|
|
// SAFETY: `ctx` is the `Seen` the test keeps alive across every step.
|
|
unsafe { &*(ctx as *const Seen) }
|
|
.0
|
|
.lock()
|
|
.unwrap()
|
|
.push(thread::current().id());
|
|
1
|
|
}
|
|
|
|
/// A raw handle moved to another thread; the test is the one-at-a-time owner.
|
|
struct Moved(*mut ZsCircuit);
|
|
unsafe impl Send for Moved {}
|
|
|
|
fn push_key(c: *mut ZsCircuit, input: u32, key: &[u8]) {
|
|
let mut z = null_mut();
|
|
assert_eq!(unsafe { zs_zset_new(&mut z) }, OK);
|
|
assert_eq!(unsafe { zs_zset_insert(z, key.as_ptr(), key.len(), 1) }, OK);
|
|
assert_eq!(unsafe { zs_circuit_push(c, input, z) }, OK);
|
|
unsafe { zs_zset_free(z) };
|
|
}
|
|
|
|
fn step_and_count(c: *mut ZsCircuit, sink: u32) -> usize {
|
|
assert_eq!(unsafe { zs_circuit_step(c) }, OK);
|
|
let mut out = null_mut();
|
|
assert_eq!(unsafe { zs_circuit_take(c, sink, &mut out) }, OK);
|
|
let mut n = 0;
|
|
let mut buf = [0u8; 64];
|
|
assert_eq!(
|
|
unsafe { zs_zset_encode(out, buf.as_mut_ptr(), buf.len(), &mut n) },
|
|
OK
|
|
);
|
|
unsafe { zs_zset_free(out) };
|
|
n / 13
|
|
}
|
|
|
|
#[test]
|
|
fn a_circuit_stepped_on_two_threads_runs_its_callbacks_on_the_thread_that_steps() {
|
|
let seen = Seen(Mutex::new(Vec::new()));
|
|
let ctx = &seen as *const Seen as *mut c_void;
|
|
let (mut c, mut input, mut kept, mut sink) = (null_mut(), 0, 0, 0);
|
|
assert_eq!(unsafe { zs_circuit_new(&mut c) }, OK);
|
|
assert_eq!(unsafe { zs_circuit_input(c, &mut input) }, OK);
|
|
assert_eq!(
|
|
unsafe { zs_circuit_filter(c, input, Some(keep_all), ctx, &mut kept) },
|
|
OK
|
|
);
|
|
assert_eq!(unsafe { zs_circuit_output(c, kept, &mut sink) }, OK);
|
|
|
|
push_key(c, input, b"a");
|
|
assert_eq!(step_and_count(c, sink), 1);
|
|
let here = thread::current().id();
|
|
|
|
let moved = Moved(c);
|
|
let there = thread::spawn(move || {
|
|
let moved = moved;
|
|
push_key(moved.0, input, b"b");
|
|
let n = step_and_count(moved.0, sink);
|
|
(n, thread::current().id(), moved)
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
assert_eq!(
|
|
there.0, 1,
|
|
"[threads] the second thread's step saw its own push"
|
|
);
|
|
assert_ne!(there.1, here);
|
|
|
|
let ids = seen.0.lock().unwrap().clone();
|
|
assert_eq!(
|
|
ids,
|
|
vec![here, there.1],
|
|
"[threads] each callback ran where it was stepped"
|
|
);
|
|
unsafe { zs_circuit_free(there.2.0) };
|
|
}
|