use std::ffi::c_void;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Mutex, RwLock};
use yog_abi::{YogItemDef, YogServer, YogStartupGrantDef, YogStr};
use yog_hot_reload::{GenerationAllocator, LoadedModule};
use super::*;
static SERIAL: Mutex<()> = Mutex::new(());
fn module() -> LoadedModule {
LoadedModule::this_process(GenerationAllocator::new().next())
}
fn registering(f: impl FnOnce(*mut c_void)) -> Box<RwLock<RuntimeHandlers>> {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let handlers = Box::new(RwLock::new(RuntimeHandlers::default()));
*CURRENT_REGISTRATION.lock().unwrap() = Some(module());
f(&*handlers as *const RwLock<RuntimeHandlers> as *mut c_void);
*CURRENT_REGISTRATION.lock().unwrap() = None;
handlers
}
unsafe extern "C" fn no_command(
_: *mut c_void,
_: *const YogServer,
_: *const yog_abi::YogCommandEvent,
_: *mut u8,
_: u32,
_: *mut u32,
) {
}
unsafe extern "C" fn count_task(ud: *mut c_void, _: *const YogServer) {
unsafe { (*(ud as *const AtomicUsize)).fetch_add(1, Ordering::SeqCst) };
}
#[test]
fn commands_keep_every_registration_under_one_name() {
let h = registering(|ctx| unsafe {
api_register_command(ctx, YogStr::from_str("hex"), std::ptr::null_mut(), no_command);
api_register_typed_command(ctx, YogStr::from_str("hex"), YogStr::from_str("int"), std::ptr::null_mut(), no_command);
});
let h = h.read().unwrap();
let entries = &h.commands["hex"];
assert_eq!(entries.len(), 2, "a second registration doesn't replace the first");
assert_eq!(entries[0].1, None);
assert_eq!(entries[1].1.as_deref(), Some("int"));
}
#[test]
fn items_are_copied_out_of_the_c_struct() {
let h = registering(|ctx| unsafe {
let def = YogItemDef {
id: YogStr::from_str("mymod:ruby"),
max_stack: 16,
name: YogStr::from_str(""),
tooltip: YogStr::from_str("Shiny"),
max_damage: 0,
fire_resistant: true,
fuel_ticks: 0,
food_nutrition: 4,
food_saturation: 0.3,
food_always_eat: false,
};
api_register_item(ctx, &def);
});
let h = h.read().unwrap();
let item = &h.items[0];
assert_eq!(item.id, "mymod:ruby");
assert_eq!(item.max_stack, 16);
assert_eq!(item.name, None, "an empty name is no name");
assert_eq!(item.tooltip.as_deref(), Some("Shiny"));
assert!(item.fire_resistant);
assert_eq!(item.food.as_ref().map(|f| f.nutrition), Some(4));
}
#[test]
fn startup_grants_split_their_items() {
let h = registering(|ctx| unsafe {
let g = YogStartupGrantDef {
id: YogStr::from_str("mymod:starter"),
items: YogStr::from_str("mymod:staff|minecraft:book"),
book: YogStr::from_str(""),
command: YogStr::from_str("say hi"),
};
api_register_startup_grant(ctx, &g);
});
let h = h.read().unwrap();
let g = &h.startup_grants[0];
assert_eq!(g.items, ["mymod:staff", "minecraft:book"]);
assert_eq!(g.book, None);
assert_eq!(g.command.as_deref(), Some("say hi"));
}
#[test]
fn books_get_the_recipes_registered_before_and_after_them() {
let recipe = r#"{"type":"shapeless","ingredients":["minecraft:stick"],"result":"minecraft:torch"}"#;
let book = r#"{"id":"mymod:guide","name":"Guide","categories":[],"entries":[]}"#;
let h = registering(|ctx| unsafe {
api_register_recipe_json(ctx, YogStr::from_str("mymod"), YogStr::from_str("a"), YogStr::from_str(recipe));
api_register_book(ctx, YogStr::from_str("mymod:guide"), YogStr::from_str(book));
api_register_recipe_json(ctx, YogStr::from_str("mymod"), YogStr::from_str("b"), YogStr::from_str(recipe));
api_register_book(ctx, YogStr::from_str("mymod:broken"), YogStr::from_str("{ not json"));
});
let h = h.read().unwrap();
assert_eq!(h.recipes.len(), 2);
assert!(h.book_renderers.lock().unwrap().contains_key("mymod:guide"));
assert!(h.books.contains_key("mymod:broken"), "its JSON is kept for the host");
assert!(!h.book_renderers.lock().unwrap().contains_key("mymod:broken"), "but it has no renderer");
}
#[test]
fn scheduled_tasks_run_when_due() {
let fired = Box::new(AtomicUsize::new(0));
let ud = &*fired as *const AtomicUsize as *mut c_void;
let h = registering(|ctx| unsafe {
api_schedule_once(ctx, 0, ud, count_task);
api_schedule_once(ctx, 3, ud, count_task);
api_schedule_repeating(ctx, 2, ud, count_task);
api_schedule_repeating(ctx, 0, ud, count_task);
});
let h = h.read().unwrap();
let mut per_tick = Vec::new();
for _ in 0..6 {
let due = h.scheduler.lock().unwrap().due();
per_tick.push(due.len());
}
assert_eq!(per_tick, [2, 2, 2, 2, 1, 2]);
assert!(h.scheduler.lock().unwrap().once_tasks.is_empty(), "once tasks are gone once run");
}
type Ping = unsafe extern "C" fn(*mut c_void) -> bool;
unsafe extern "C" fn ping_yes(ud: *mut c_void) -> bool {
unsafe { (*(ud as *const AtomicUsize)).fetch_add(1, Ordering::SeqCst) };
true
}
unsafe extern "C" fn ping_no(ud: *mut c_void) -> bool {
unsafe { (*(ud as *const AtomicUsize)).fetch_add(1, Ordering::SeqCst) };
false
}
#[test]
fn pre_phase_handlers_stop_at_the_first_no_and_skip_retired_mods() {
let calls = Box::new(AtomicUsize::new(0));
let ud = &*calls as *const AtomicUsize as *mut c_void;
let live = module();
let retired = module();
retired.retire(|| {});
let entries: Vec<Entry<Ping>> = vec![
(retired.clone(), ud, ping_no as Ping),
(live.clone(), ud, ping_yes as Ping),
(live.clone(), ud, ping_no as Ping),
(live.clone(), ud, ping_yes as Ping),
];
assert!(!allowed(&entries, |ud, f| unsafe { f(ud) }), "a handler said no");
assert_eq!(calls.load(Ordering::SeqCst), 2, "the retired one is skipped, the last never runs");
assert!(allowed(&entries, |_, _| -> bool { panic!("a mod panicked") }), "a panic doesn't cancel the action");
}
#[test]
fn tsv_values_cannot_break_lines() {
assert_eq!(tsv_clean("a\tb\nc\rd"), "a b c d");
}