// SPDX-License-Identifier: AGPL-3.0-only //! Unit tests for the statistics accumulator, derived arithmetic, and formatter in [`crate::stats`]. use super::*; /// Asserts two f32 values agree to within a tolerance that survives the accumulated division and multiplication. fn close(actual: f32, expected: f32) { assert!( (actual - expected).abs() < 0.01, "expected {expected}, got {actual}" ); } #[test] fn an_empty_window_reports_zeroes_rather_than_dividing_by_zero() { let stats = summarise_frames(0, 0.0, f32::INFINITY, 0.0, STATS_INTERVAL); close(stats.average_fps, 0.0); close(stats.mean_frame_ms, 0.0); // The sentinel the accumulator starts from must not leak into the reported minimum. close(stats.min_frame_ms, 0.0); assert_eq!(stats.frames, 0); } #[test] fn a_steady_window_reports_the_matching_rate_and_frametime() { // Sixty frames of 16.667 ms each, filling one second of wall clock. let stats = summarise_frames(60, 1.0, 1.0 / 60.0, 1.0 / 60.0, Duration::from_secs(1)); close(stats.average_fps, 60.0); close(stats.mean_frame_ms, 16.67); close(stats.min_frame_ms, 16.67); close(stats.max_frame_ms, 16.67); } #[test] fn a_spike_is_visible_in_the_maximum_while_the_mean_stays_flat() { // Fifty-nine cheap frames plus one 40 ms stall: an average of sixty frames per second conceals what the maximum exposes. let stats = summarise_frames(60, 1.0, 0.010, 0.040, Duration::from_secs(1)); close(stats.average_fps, 60.0); close(stats.mean_frame_ms, 16.67); close(stats.max_frame_ms, 40.0); close(stats.min_frame_ms, 10.0); } #[test] fn a_single_frame_window_is_summarised_without_special_casing() { let stats = summarise_frames(1, 0.5, 0.5, 0.5, Duration::from_secs(1)); close(stats.average_fps, 1.0); close(stats.mean_frame_ms, 500.0); close(stats.max_frame_ms, 500.0); } #[test] fn zero_delta_frames_do_not_produce_a_non_finite_frametime() { let stats = summarise_frames(4, 0.0, 0.0, 0.0, Duration::from_secs(1)); assert!(stats.mean_frame_ms.is_finite()); close(stats.mean_frame_ms, 0.0); close(stats.average_fps, 4.0); } #[test] fn a_window_closes_only_once_the_interval_has_elapsed() { let start = Instant::now(); let mut accumulator = FrameAccumulator::new(start); accumulator.record(0.016); assert!( accumulator .take_window(start + Duration::from_millis(999)) .is_none() ); assert!(accumulator.take_window(start + STATS_INTERVAL).is_some()); } #[test] fn closing_a_window_resets_the_accumulators() { let start = Instant::now(); let mut accumulator = FrameAccumulator::new(start); accumulator.record(0.100); let _ = accumulator.take_window(start + STATS_INTERVAL); accumulator.record(0.010); let second = accumulator .take_window(start + STATS_INTERVAL + STATS_INTERVAL) .unwrap_or(FrameStats { average_fps: 0.0, mean_frame_ms: 0.0, min_frame_ms: 0.0, max_frame_ms: 0.0, frames: 0, }); // The 100 ms frame belonged to the first window and must not leak into the second's extremes. assert_eq!(second.frames, 1); close(second.max_frame_ms, 10.0); close(second.min_frame_ms, 10.0); } #[test] fn a_negative_position_floors_into_the_block_below_rather_than_truncating_toward_zero() { let stats = camera_stats( Vec3::new(-0.5, 1.5, -33.0), Vec3::NEG_Z, 0.0, 0.0, Vec3::ZERO, 0.0, ); assert_eq!(stats.block, IVec3::new(-1, 1, -33)); // Chunk-local coordinates stay non-negative on the negative side of the origin. assert!(stats.local.cmpge(IVec3::ZERO).all()); assert_eq!(usize::try_from(stats.local.x), Ok(CHUNK_SIZE - 1)); } #[test] fn each_horizontal_direction_maps_to_its_cardinal_and_axis() { for (forward, expected) in [ (Vec3::X, ("east", "+X")), (Vec3::NEG_X, ("west", "-X")), (Vec3::Z, ("south", "+Z")), (Vec3::NEG_Z, ("north", "-Z")), ] { assert_eq!(facing_for(forward), expected); } } #[test] fn pitch_does_not_change_the_reported_cardinal_direction() { // A steeply downward vector still faces north, since only the horizontal components decide. assert_eq!(facing_for(Vec3::new(0.0, -0.99, -0.1)), ("north", "-Z")); } #[test] fn speed_is_the_distance_covered_over_the_frame_delta() { let stats = camera_stats( Vec3::ZERO, Vec3::NEG_Z, 0.0, 0.0, Vec3::new(3.0, 4.0, 0.0), 0.5, ); // A 5-block displacement over half a second is ten blocks per second. close(stats.speed, 10.0); } #[test] fn a_zero_frame_delta_reports_no_speed_rather_than_infinity() { let stats = camera_stats(Vec3::ZERO, Vec3::NEG_Z, 0.0, 0.0, Vec3::X, 0.0); assert!(stats.speed.is_finite()); close(stats.speed, 0.0); } #[test] fn yaw_and_pitch_are_reported_in_degrees() { let stats = camera_stats( Vec3::ZERO, Vec3::NEG_Z, std::f32::consts::PI, std::f32::consts::FRAC_PI_2, Vec3::ZERO, 0.0, ); close(stats.yaw_degrees, 180.0); close(stats.pitch_degrees, 90.0); } #[test] fn byte_counts_convert_to_mebibytes_on_the_binary_scale() { close(mib(1024 * 1024), 1.0); close(mib(0), 0.0); close(mib(1024 * 1024 * 3 / 2), 1.5); } #[test] fn the_formatter_emits_every_always_present_section() { let host = HostInfo { cpu_brand: "Test CPU".to_owned(), logical_cores: 8, os: "Test OS".to_owned(), kernel: "1.2.3".to_owned(), client_build: "0.0.0", }; let snapshot = Snapshot { frame: FrameStats { average_fps: 60.0, mean_frame_ms: 16.67, min_frame_ms: 15.0, max_frame_ms: 40.0, frames: 60, }, camera: camera_stats( Vec3::new(1.0, 2.0, 3.0), Vec3::NEG_Z, 0.0, 0.0, Vec3::ZERO, 0.0, ), chunks: None, render: None, gpu: None, memory: None, net: None, server: None, identity: None, host: &host, usage: HostUsage { process_cpu_percent: 12.5, process_memory_bytes: 1024 * 1024, process_virtual_bytes: 2 * 1024 * 1024, system_total_bytes: 16 * 1024 * 1024, system_available_bytes: 8 * 1024 * 1024, cpu_frequency_mhz: 4200, }, }; let panel = format_panel(&snapshot); assert!(panel.contains("60.0 fps"), "{panel}"); assert!(panel.contains("max 40.00 ms"), "{panel}"); assert!(panel.contains("facing north (-Z)"), "{panel}"); assert!(panel.contains("Test CPU x8 @ 4200 MHz"), "{panel}"); assert!(panel.contains("cpu 12.5%"), "{panel}"); assert!(panel.contains("rss 1.0 MiB"), "{panel}"); // Sections whose source is absent are omitted rather than printed as placeholders. assert!(!panel.contains("gpu "), "{panel}"); assert!(!panel.contains("net "), "{panel}"); } #[test] fn absent_device_memory_figures_are_named_rather_than_reported_as_zero() { let host = HostInfo { cpu_brand: "Test CPU".to_owned(), logical_cores: 1, os: "Test OS".to_owned(), kernel: "1.2.3".to_owned(), client_build: "0.0.0", }; let snapshot = Snapshot { frame: FrameStats { average_fps: 0.0, mean_frame_ms: 0.0, min_frame_ms: 0.0, max_frame_ms: 0.0, frames: 0, }, camera: camera_stats(Vec3::ZERO, Vec3::NEG_Z, 0.0, 0.0, Vec3::ZERO, 0.0), chunks: None, render: None, gpu: None, memory: Some(MemoryUsage { heap_usage_bytes: None, heap_budget_bytes: None, allocator_allocated_bytes: 1024 * 1024, allocator_capacity_bytes: 2 * 1024 * 1024, }), net: None, server: None, identity: None, host: &host, usage: HostUsage { process_cpu_percent: 0.0, process_memory_bytes: 0, process_virtual_bytes: 0, system_total_bytes: 0, system_available_bytes: 0, cpu_frequency_mhz: 0, }, }; let panel = format_panel(&snapshot); assert!(panel.contains("heap unavailable"), "{panel}"); assert!(panel.contains("allocator 1.0 / 2.0 MiB"), "{panel}"); }