/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ //! Guards against OpenGL knowledge leaking out of the device layer. //! //! Scans the crate for references to `gleam` or the `gl::` module outside //! `src/device/`. Files that still have them must be listed in `ALLOWLIST`. //! A stale entry fails the test too, so the list only shrinks. use std::fs; use std::path::{Path, PathBuf}; const DEVICE_DIR: &str = "device"; /// Files outside the device layer that still reference GL. Remove entries as /// the leaks are fixed. Do not add new entries. const ALLOWLIST: &[&str] = &[ "lib.rs", "renderer/init.rs", ]; /// Files that are GL-specific by design and are never expected to be cleaned up. const PERMANENT_ALLOWLIST: &[&str] = &[ // The SWGL compositor drives the software GL implementation directly. "compositor/sw_compositor.rs", ]; fn is_ident_char(c: u8) -> bool { c.is_ascii_alphanumeric() || c == b'_' } fn line_has_gl_reference(line: &str) -> bool { if line.contains("gleam") { return true; } let bytes = line.as_bytes(); let mut start = 0; while let Some(pos) = line[start..].find("gl::") { let idx = start + pos; let preceded_by_ident = idx > 0 && is_ident_char(bytes[idx - 1]); if !preceded_by_ident { return true; } start = idx + 4; } false } fn collect_rust_files(dir: &Path, out: &mut Vec) { for entry in fs::read_dir(dir).expect("failed to read source directory") { let path = entry.expect("failed to read directory entry").path(); if path.is_dir() { collect_rust_files(&path, out); } else if path.extension().map_or(false, |ext| ext == "rs") { out.push(path); } } } #[test] fn gl_does_not_leak_outside_device_layer() { let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); let mut files = Vec::new(); collect_rust_files(&src_dir, &mut files); let mut unexpected = Vec::new(); let mut seen_allowlisted = Vec::new(); for path in files { let rel = path .strip_prefix(&src_dir) .unwrap() .to_str() .unwrap() .replace('\\', "/"); if rel.starts_with(&format!("{}/", DEVICE_DIR)) { continue; } let source = fs::read_to_string(&path).expect("failed to read source file"); let leaks: Vec<(usize, &str)> = source .lines() .enumerate() .filter(|(_, line)| line_has_gl_reference(line)) .map(|(i, line)| (i + 1, line.trim())) .collect(); if leaks.is_empty() { continue; } if PERMANENT_ALLOWLIST.contains(&rel.as_str()) { continue; } if ALLOWLIST.contains(&rel.as_str()) { seen_allowlisted.push(rel); continue; } for (line_no, line) in leaks { unexpected.push(format!("{}:{}: {}", rel, line_no, line)); } } let stale: Vec<&&str> = ALLOWLIST .iter() .filter(|entry| !seen_allowlisted.iter().any(|seen| seen == *entry)) .collect(); assert!( unexpected.is_empty(), "GL references found outside src/{}/. Route them through the Device API \ instead of adding to the allowlist:\n{}", DEVICE_DIR, unexpected.join("\n"), ); assert!( stale.is_empty(), "Allowlist entries no longer contain GL references; remove them from \ ALLOWLIST in tests/gl_leaks.rs: {:?}", stale, ); }