use std::path::{Path, PathBuf};
fn engine_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn gpu_crate_root() -> PathBuf {
engine_root()
.parent()
.expect("crates/frust-engine has a parent: crates/")
.join("frust-gpu")
}
#[test]
fn g1_engine_shader_directory_has_real_files_and_passes_the_downlevel_lint() {
let shaders_dir = engine_root().join("shaders");
assert!(
shaders_dir.is_dir(),
"{} must exist and hold the ported strip/clear/copy shaders",
shaders_dir.display()
);
let wgsl_files: Vec<PathBuf> = std::fs::read_dir(&shaders_dir)
.unwrap_or_else(|err| panic!("reading {}: {err}", shaders_dir.display()))
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("wgsl"))
.collect();
assert!(
!wgsl_files.is_empty(),
"{} has no .wgsl files — an empty directory would let `lint_wgsl_dir` pass vacuously, \
hiding the fact that the shader port never landed",
shaders_dir.display()
);
let violations = frust_gpu::lint_wgsl_dir(&shaders_dir)
.unwrap_or_else(|err| panic!("scanning {}: {err}", shaders_dir.display()));
assert!(
violations.is_empty(),
"the engine's shipped shaders violate frust-gpu's downlevel design rules: {violations:?}"
);
}
const ALLOWED_ENGINE_FEATURES: wgpu::Features = wgpu::Features::TIMESTAMP_QUERY;
#[test]
fn g2_webgl2_downlevel_limits_satisfy_the_webgl2_ceiling() {
let caps = frust_gpu::TierCaps::fake(frust_gpu::DownlevelProfile::WebGl2);
let webgl2_defaults = wgpu::Limits::downlevel_webgl2_defaults();
let declared = wgpu::Limits {
max_texture_dimension_2d: caps.max_texture_dimension_2d,
max_texture_array_layers: caps.max_texture_array_layers,
max_bind_groups: caps.max_bind_groups,
max_uniform_buffer_binding_size: u64::from(caps.max_uniform_buffer_binding_size),
min_uniform_buffer_offset_alignment: caps.min_uniform_buffer_offset_alignment,
max_vertex_attributes: caps.max_vertex_attributes,
..webgl2_defaults.clone()
};
assert!(
declared.check_limits(&webgl2_defaults),
"the engine's downlevel-profile GPU limits exceed the WebGL2 ceiling: {declared:?}"
);
assert!(
frust_gpu::check_limits_against_webgl2(&declared).is_empty(),
"frust-gpu's own WebGL2 lint disagrees with wgpu's `check_limits` for {declared:?}"
);
}
#[test]
fn g2_requested_features_never_exceed_the_perf_trace_timestamp_query_allowance() {
for profile in [
frust_gpu::DownlevelProfile::Full,
frust_gpu::DownlevelProfile::WebGl2,
] {
let caps = frust_gpu::TierCaps::fake(profile);
let would_request_under_perf_trace = if caps.has_timestamp_query {
wgpu::Features::TIMESTAMP_QUERY
} else {
wgpu::Features::empty()
};
assert!(
ALLOWED_ENGINE_FEATURES.contains(would_request_under_perf_trace),
"{profile:?} caps would let a perf-trace build request \
{would_request_under_perf_trace:?}, outside the engine's allowed feature set \
{ALLOWED_ENGINE_FEATURES:?}"
);
}
}
#[test]
fn g2_headless_target_usage_excludes_storage_binding() {
let path = gpu_crate_root().join("src/headless.rs");
let contents = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("reading {}: {err}", path.display()));
let usage_lines: Vec<&str> = contents
.lines()
.map(str::trim)
.filter(|line| !line.starts_with("//"))
.filter(|line| line.contains("usage") && line.contains("TextureUsages"))
.collect();
assert!(
!usage_lines.is_empty(),
"expected to find HeadlessTarget's texture-usage declaration (a `usage: \
wgpu::TextureUsages::...` line) in {}",
path.display()
);
for line in usage_lines {
assert!(
!line.contains("STORAGE_BINDING"),
"HeadlessTarget must never request STORAGE_BINDING — it renders through ordinary \
render passes, never compute (see {}'s module doc): {line}",
path.display()
);
}
}
const BANNED_NEEDLES: &[&str] = &["unwrap()", ".expect(", "panic!("];
fn rust_files_recursive(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries {
let path = entry
.unwrap_or_else(|err| panic!("dir entry in {}: {err}", dir.display()))
.path();
if path.is_dir() {
rust_files_recursive(&path, out);
} else if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn e17_scanned_files() -> Vec<PathBuf> {
let src = engine_root().join("src");
let mut files = Vec::new();
for name in ["cache", "compile", "effects", "filters", "gpu", "text"] {
rust_files_recursive(&src.join(name), &mut files);
}
let renderer_rs = src.join("renderer.rs");
if renderer_rs.is_file() {
files.push(renderer_rs);
}
rust_files_recursive(&src.join("schedule"), &mut files);
files.sort();
files
}
fn production_lines(contents: &str) -> Vec<(usize, &str)> {
let mut out = Vec::new();
for (i, raw) in contents.lines().enumerate() {
let trimmed = raw.trim_start();
if trimmed.starts_with("#[cfg(test)]") || trimmed.starts_with("mod tests") {
break;
}
if trimmed.starts_with("//") {
continue;
}
out.push((i, raw));
}
out
}
#[test]
fn e17_engine_render_paths_never_unwrap_expect_or_panic_outside_tests() {
let mut failures = Vec::new();
for path in e17_scanned_files() {
let contents = std::fs::read_to_string(&path)
.unwrap_or_else(|err| panic!("reading {}: {err}", path.display()));
for (i, line) in production_lines(&contents) {
for needle in BANNED_NEEDLES {
if line.contains(needle) {
failures.push(format!(
"{}:{}: `{needle}` outside #[cfg(test)] — E17 requires every rendering \
path to return an error rather than panic: {}",
path.display(),
i + 1,
line.trim()
));
}
}
}
}
assert!(
failures.is_empty(),
"E17 violated ({} hit(s)):\n{}",
failures.len(),
failures.join("\n")
);
}
#[test]
fn e17_scan_currently_covers_at_least_cache_compile_effects_filters_gpu_and_text() {
let files = e17_scanned_files();
for name in ["cache", "compile", "effects", "filters", "gpu", "text"] {
assert!(
files
.iter()
.any(|path| path.components().any(|c| c.as_os_str() == name)),
"the E17 scan covers no .rs file under src/{name}/: {files:?}"
);
}
}