Synvael/crates/client/src/tests/stats.rs

286 lines
8.4 KiB
Rust

// 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}");
}