use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use whiteout::interfaces::{
http_capability, HostHttpHandler, HostWorkerPool, HttpHandler, HttpResponder, WorkerPool,
WorkerTask,
};
struct EchoHandler {
body: Vec<u8>,
status: i32,
}
impl HttpHandler for EchoHandler {
fn capabilities(&self) -> u32 {
http_capability::HTTP2_MULTIPLEXING
}
fn get(&self, _url: &str, responder: HttpResponder) {
responder.respond(self.status, &self.body);
}
fn get_range(&self, _url: &str, start: u64, end: u64, responder: HttpResponder) {
let len = (end - start + 1) as usize;
let slice: Vec<u8> = self
.body
.iter()
.copied()
.skip(start as usize)
.take(len)
.collect();
responder.respond(206, &slice);
}
}
#[test]
fn capabilities_reach_cpp() {
let h = HostHttpHandler::new(EchoHandler {
body: Vec::new(),
status: 200,
});
assert_eq!(
h.capabilities_through_native(),
http_capability::HTTP2_MULTIPLEXING
);
}
#[test]
fn a_synchronous_reply_reaches_the_caller() {
let h = HostHttpHandler::new(EchoHandler {
body: b"hello cdn".to_vec(),
status: 200,
});
let out = h.get_through_native("http://example/x");
assert_eq!(out.status, 200);
assert_eq!(out.body, b"hello cdn");
assert!(out.error.is_empty());
}
#[test]
fn range_requests_carry_their_bounds() {
let h = HostHttpHandler::new(EchoHandler {
body: (0u8..32).collect(),
status: 200,
});
let out = h.get_range_through_native("http://example/x", 4, 7);
assert_eq!(out.status, 206);
assert_eq!(out.body, vec![4, 5, 6, 7]);
}
struct FailingHandler;
impl HttpHandler for FailingHandler {
fn get(&self, _url: &str, responder: HttpResponder) {
responder.fail("connection refused");
}
fn get_range(&self, _url: &str, _s: u64, _e: u64, responder: HttpResponder) {
responder.fail("connection refused");
}
}
#[test]
fn a_failure_reply_carries_its_message() {
let h = HostHttpHandler::new(FailingHandler);
let out = h.get_through_native("http://example/x");
assert_eq!(out.error, "connection refused");
assert!(out.body.is_empty());
}
struct SilentHandler;
impl HttpHandler for SilentHandler {
fn get(&self, _url: &str, _responder: HttpResponder) {
}
fn get_range(&self, _url: &str, _s: u64, _e: u64, _responder: HttpResponder) {}
}
#[test]
fn dropping_a_responder_cancels_rather_than_hanging() {
let h = HostHttpHandler::new(SilentHandler);
let out = h.get_through_native("http://example/x");
assert!(
!out.error.is_empty(),
"a dropped responder must surface as an error"
);
}
struct PanickingHandler;
impl HttpHandler for PanickingHandler {
fn get(&self, _url: &str, _responder: HttpResponder) {
panic!("deliberate panic inside get");
}
fn get_range(&self, _url: &str, _s: u64, _e: u64, _responder: HttpResponder) {
panic!("deliberate panic inside get_range");
}
}
#[test]
fn a_panicking_handler_is_contained_and_still_replies() {
let h = HostHttpHandler::new(PanickingHandler);
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let out = h.get_through_native("http://example/x");
std::panic::set_hook(prev);
assert!(!out.error.is_empty());
}
struct DeferredHandler;
impl HttpHandler for DeferredHandler {
fn get(&self, _url: &str, responder: HttpResponder) {
std::thread::spawn(move || {
responder.respond(200, b"from another thread");
})
.join()
.unwrap();
}
fn get_range(&self, _url: &str, _s: u64, _e: u64, responder: HttpResponder) {
responder.respond(206, b"");
}
}
#[test]
fn a_responder_can_cross_threads() {
let h = HostHttpHandler::new(DeferredHandler);
let out = h.get_through_native("http://example/x");
assert_eq!(out.status, 200);
assert_eq!(out.body, b"from another thread");
}
#[derive(Default)]
struct InlinePool {
ran: AtomicUsize,
}
impl WorkerPool for InlinePool {
fn submit(&self, task: WorkerTask) {
self.ran.fetch_add(1, Ordering::SeqCst);
task.run();
}
fn wait_idle(&self) {}
fn thread_count(&self) -> usize {
1
}
}
#[test]
fn thread_count_reaches_cpp() {
let p = HostWorkerPool::new(InlinePool::default());
assert_eq!(p.thread_count_through_native(), 1);
}
#[test]
fn a_submitted_task_actually_runs() {
let pool = HostWorkerPool::new(InlinePool::default());
let mut sentinel = 0i32;
pool.submit_sentinel_through_native(&mut sentinel);
pool.wait_idle_through_native();
assert_eq!(sentinel, 1, "the C++ task never ran");
}
#[derive(Default)]
struct ThreadedPool {
pending: Mutex<Vec<std::thread::JoinHandle<()>>>,
}
impl WorkerPool for ThreadedPool {
fn submit(&self, task: WorkerTask) {
let h = std::thread::spawn(move || task.run());
self.pending.lock().unwrap().push(h);
}
fn wait_idle(&self) {
for h in self.pending.lock().unwrap().drain(..) {
let _ = h.join();
}
}
fn thread_count(&self) -> usize {
4
}
}
#[test]
fn a_task_can_run_on_another_thread() {
let pool = HostWorkerPool::new(ThreadedPool::default());
assert_eq!(pool.thread_count_through_native(), 4);
let mut sentinel = 0i32;
pool.submit_sentinel_through_native(&mut sentinel);
pool.wait_idle_through_native();
assert_eq!(sentinel, 1, "the deferred task never ran");
}
struct DroppingPool;
impl WorkerPool for DroppingPool {
fn submit(&self, _task: WorkerTask) {
}
fn wait_idle(&self) {}
fn thread_count(&self) -> usize {
1
}
}
#[test]
fn dropping_a_task_cancels_it_cleanly() {
let pool = HostWorkerPool::new(DroppingPool);
let mut sentinel = 0i32;
pool.submit_sentinel_through_native(&mut sentinel);
pool.wait_idle_through_native();
assert_eq!(sentinel, 0, "a dropped task must not run");
}
#[test]
fn a_panicking_pool_is_contained() {
struct PanickingPool;
impl WorkerPool for PanickingPool {
fn submit(&self, _task: WorkerTask) {
panic!("deliberate panic inside submit");
}
fn wait_idle(&self) {}
fn thread_count(&self) -> usize {
1
}
}
let pool = HostWorkerPool::new(PanickingPool);
let mut sentinel = 0i32;
let prev = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
pool.submit_sentinel_through_native(&mut sentinel);
std::panic::set_hook(prev);
assert_eq!(sentinel, 0);
assert_eq!(pool.thread_count_through_native(), 1);
}
#[test]
fn many_pools_and_handlers_drop_cleanly() {
for _ in 0..128 {
let p = Arc::new(HostWorkerPool::new(InlinePool::default()));
let h = HostHttpHandler::new(EchoHandler {
body: b"x".to_vec(),
status: 200,
});
assert_eq!(p.thread_count_through_native(), 1);
assert_eq!(h.get_through_native("u").status, 200);
}
}
#[test]
fn the_library_submits_real_work_to_a_rust_pool() {
use whiteout::textures::{PixelFormat, Texture};
struct CountingPool(Arc<AtomicUsize>);
impl WorkerPool for CountingPool {
fn submit(&self, task: WorkerTask) {
self.0.fetch_add(1, Ordering::SeqCst);
task.run();
}
fn wait_idle(&self) {}
fn thread_count(&self) -> usize {
4
}
}
let counter = Arc::new(AtomicUsize::new(0));
let pool = HostWorkerPool::new(CountingPool(Arc::clone(&counter)));
let mut tex = Texture::create_2d(PixelFormat::RGBA8, 256, 256, 1).expect("create failed");
tex.data_mut().fill(0x7F);
let converted = tex
.copy_as_format(PixelFormat::BC1, Some(&pool))
.expect("conversion failed");
assert_eq!(converted.format(), PixelFormat::BC1);
assert!(
counter.load(Ordering::SeqCst) > 0,
"the library never submitted work to the Rust pool"
);
}