286 lines
8.4 KiB
Rust
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}");
|
|
}
|