use std::time::Duration;
use bevy::prelude::*;
use bevy::remote::{BrpResult, RemoteMethodSystemId, RemoteMethods};
use bevy::time::{TimeUpdateStrategy, Virtual};
use bevy_fairy::{ClockRequest, DebuggerPlugin};
use serde_json::{json, Value};
const FRAME: Duration = Duration::from_millis(16);
fn app_with(plugin: DebuggerPlugin) -> App {
let mut app = App::new();
app.add_plugins((MinimalPlugins, bevy::input::InputPlugin, plugin));
app.insert_resource(TimeUpdateStrategy::ManualDuration(FRAME));
app.update();
app
}
fn call(app: &mut App, params: Option<Value>) -> BrpResult {
let id = match app.world().resource::<RemoteMethods>().get("bevy_fairy/time") {
Some(RemoteMethodSystemId::Instant(id)) => *id,
_ => panic!("bevy_fairy/time is not registered"),
};
app.world_mut().run_system_with(id, params).expect("handler runs")
}
fn run(app: &mut App, frames: u32) -> Duration {
let before = app.world().resource::<Time<Virtual>>().elapsed();
for _ in 0..frames {
app.update();
}
app.world().resource::<Time<Virtual>>().elapsed() - before
}
fn host_unpauses(mut time: ResMut<Time<Virtual>>) {
time.unpause();
}
fn host_sets_speed_to_one(mut time: ResMut<Time<Virtual>>) {
time.set_relative_speed(1.0);
}
fn host_pauses(mut time: ResMut<Time<Virtual>>) {
time.pause();
}
fn host_slows_to_half(mut time: ResMut<Time<Virtual>>) {
time.set_relative_speed(0.5);
}
#[test]
fn a_pause_holds_against_a_host_that_unpauses_every_frame() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_unpauses);
let free = run(&mut app, 10);
assert!(free > Duration::ZERO, "the clock runs before any request");
call(&mut app, Some(json!({ "pause": true }))).expect("time answers");
let moved = run(&mut app, 60);
assert_eq!(moved, Duration::ZERO, "virtual time moved while the agent held a pause");
let read = call(&mut app, None).expect("an empty call reads the clock");
assert_eq!(read["held"], true, "{read}");
assert_eq!(read["host_owned"], false, "{read}");
}
#[test]
fn a_speed_holds_against_a_host_that_resets_it_every_frame() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_sets_speed_to_one);
let baseline = run(&mut app, 60);
call(&mut app, Some(json!({ "speed": 4.0 }))).expect("time answers");
let fast = run(&mut app, 60);
let ratio = fast.as_secs_f64() / baseline.as_secs_f64();
assert!((ratio - 4.0).abs() < 0.1, "clock ran {ratio}x, wanted 4x");
let read = call(&mut app, None).expect("read");
assert_eq!(read["held"], true, "{read}");
assert!((read["effective_speed"].as_f64().unwrap() - 4.0).abs() < 1e-6, "{read}");
}
#[test]
fn a_host_that_owns_the_clock_is_never_written_to_and_learns_the_request() {
let mut app = app_with(DebuggerPlugin::default().host_owns_clock());
app.add_systems(Update, host_unpauses);
let reply = call(&mut app, Some(json!({ "pause": true, "speed": 2.0 }))).expect("answers");
assert_eq!(reply["host_owned"], true, "{reply}");
let request = app.world().resource::<ClockRequest>();
assert!(request.pause);
assert_eq!(request.speed, 2.0);
let moved = run(&mut app, 30);
assert!(moved > Duration::ZERO, "the crate wrote Time<Virtual> in host-owned mode");
let read = call(&mut app, None).expect("read");
assert_eq!(read["held"], false, "a host that ignores the request is not held: {read}");
assert_eq!(read["host_owned"], true, "{read}");
}
#[test]
fn a_request_survives_a_host_state_change_until_cleared() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_unpauses);
call(&mut app, Some(json!({ "pause": true }))).expect("time answers");
run(&mut app, 5);
app.world_mut().insert_resource(Time::<Virtual>::default());
assert_eq!(run(&mut app, 30), Duration::ZERO, "the pause was lost with the host's reset");
call(&mut app, Some(json!({ "pause": false }))).expect("clear");
assert!(run(&mut app, 5) > Duration::ZERO, "clearing the pause did not resume the clock");
}
#[test]
fn an_empty_call_reports_the_clock_and_whether_the_request_held() {
let mut app = app_with(DebuggerPlugin::default());
run(&mut app, 3);
let read = call(&mut app, None).expect("read");
assert_eq!(read["paused"], false, "{read}");
assert_eq!(read["speed"], 1.0, "{read}");
assert_eq!(read["held"], true, "{read}");
assert_eq!(read["host_owned"], false, "{read}");
}
#[test]
fn a_request_is_not_called_held_before_a_frame_has_run_under_it() {
let mut app = app_with(DebuggerPlugin::default());
run(&mut app, 3);
let reply = call(&mut app, Some(json!({ "pause": true }))).expect("answers");
assert!(reply["held"].is_null(), "held claimed with no frame observed: {reply}");
assert!(reply["unavailable"].is_string(), "{reply}");
}
#[test]
fn a_speed_the_clock_cannot_represent_is_refused_not_applied() {
let mut app = app_with(DebuggerPlugin::default());
for bad in [1e30, 1e21, f32::MAX, 1000.5] {
let err = call(&mut app, Some(json!({ "speed": bad }))).expect_err("a speed above the bound");
assert!(err.message.contains("speed"), "{err:?}");
}
assert_eq!(app.world().resource::<ClockRequest>().speed, 1.0, "a refused speed was stored");
run(&mut app, 3); let read = call(&mut app, None).expect("read");
assert_eq!(read["speed"], 1.0, "{read}");
}
#[test]
fn the_largest_speed_the_command_accepts_runs() {
let mut app = app_with(DebuggerPlugin::default());
call(&mut app, Some(json!({ "speed": 1000.0 }))).expect("the bound is allowed");
run(&mut app, 3);
let read = call(&mut app, None).expect("read");
assert_eq!(read["speed"], 1000.0, "{read}");
}
#[test]
fn a_speed_the_engine_clamps_is_held_and_says_it_was_clamped() {
let mut app = app_with(DebuggerPlugin::default());
call(&mut app, Some(json!({ "speed": 20.0 }))).expect("answers");
run(&mut app, 3);
let read = call(&mut app, None).expect("read");
assert_eq!(read["held"], true, "no host system touched the clock: {read}");
assert_eq!(read["clamped_by_max_delta"], true, "{read}");
assert!((read["effective_speed"].as_f64().unwrap() - 15.625).abs() < 1e-3, "{read}");
assert_eq!(read["speed"], 20.0, "the speed in force is the one asked for: {read}");
call(&mut app, Some(json!({ "speed": 4.0 }))).expect("answers");
run(&mut app, 3);
let read = call(&mut app, None).expect("read");
assert_eq!(read["held"], true, "{read}");
assert_eq!(read["clamped_by_max_delta"], false, "{read}");
}
#[test]
fn a_host_that_ignores_a_clamped_speed_is_still_not_held() {
let mut app = app_with(DebuggerPlugin::default().host_owns_clock());
call(&mut app, Some(json!({ "speed": 20.0 }))).expect("answers");
run(&mut app, 3);
let read = call(&mut app, None).expect("read");
assert_eq!(read["held"], false, "{read}");
assert_eq!(read["clamped_by_max_delta"], false, "the host's 1x speed was not clamped: {read}");
}
#[test]
fn an_unpause_the_host_overrides_is_not_held() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_pauses);
run(&mut app, 5);
call(&mut app, Some(json!({ "pause": false }))).expect("answers");
assert_eq!(run(&mut app, 30), Duration::ZERO, "the host's pause held the clock");
let read = call(&mut app, None).expect("read");
assert_eq!(read["paused"], true, "{read}");
assert_eq!(read["held"], false, "asked to run, clock paused, and held said true: {read}");
}
#[test]
fn a_speed_of_one_the_host_overrides_is_not_held() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_slows_to_half);
run(&mut app, 5);
call(&mut app, Some(json!({ "speed": 1.0 }))).expect("answers");
run(&mut app, 30);
let read = call(&mut app, None).expect("read");
assert!((read["effective_speed"].as_f64().unwrap() - 0.5).abs() < 1e-6, "{read}");
assert_eq!(read["held"], false, "asked for real time, clock at half, and held said true: {read}");
}
#[test]
fn an_unpause_and_a_speed_of_one_nobody_overrides_are_held() {
let mut app = app_with(DebuggerPlugin::default());
call(&mut app, Some(json!({ "pause": true }))).expect("answers");
run(&mut app, 5);
call(&mut app, Some(json!({ "pause": false, "speed": 1.0 }))).expect("answers");
run(&mut app, 5);
let read = call(&mut app, None).expect("read");
assert_eq!(read["paused"], false, "{read}");
assert_eq!(read["held"], true, "{read}");
}
#[test]
fn a_pause_is_held_beside_a_hosts_own_slow_motion() {
let mut app = app_with(DebuggerPlugin::default());
app.add_systems(Update, host_slows_to_half);
run(&mut app, 5);
call(&mut app, Some(json!({ "pause": true }))).expect("answers");
assert_eq!(run(&mut app, 30), Duration::ZERO, "the pause froze the clock");
let read = call(&mut app, None).expect("read");
assert_eq!(read["paused"], true, "{read}");
assert_eq!(read["held"], true, "a frozen clock obeyed a pause: {read}");
}