use std::path::{Path, PathBuf};
const CARGO_TOML: &str = include_str!("../Cargo.toml");
const FORBIDDEN_CRATES: [&str; 2] = ["smol", "async-io"];
const SELF: &str = "undying_thread_guard.rs";
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
fn code_only(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let mut chars = source.chars().peekable();
let mut in_block = false;
while let Some(c) = chars.next() {
if in_block {
if c == '*' && chars.peek() == Some(&'/') {
chars.next();
in_block = false;
}
continue;
}
if c == '/' {
match chars.peek() {
Some('/') => {
for c in chars.by_ref() {
if c == '\n' {
out.push('\n');
break;
}
}
continue;
}
Some('*') => {
chars.next();
in_block = true;
continue;
}
_ => {}
}
}
out.push(c);
}
out
}
fn rust_files(dir: &Path) -> Vec<PathBuf> {
let mut files = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("source directory is readable") {
let path = entry.expect("directory entry is readable").path();
if path.is_dir() {
stack.push(path);
} else if path.extension().is_some_and(|ext| ext == "rs") {
files.push(path);
}
}
}
files.sort();
files
}
fn dependency_keys(toml: &str, header: &str) -> Vec<String> {
let start = toml
.find(&format!("\n{header}\n"))
.unwrap_or_else(|| panic!("Cargo.toml declares no `{header}` table"))
+ 1
+ header.len()
+ 1;
let rest = &toml[start..];
let end = rest
.match_indices("\n[")
.next()
.map(|(i, _)| i + 1)
.unwrap_or(rest.len());
rest[..end]
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.filter_map(|line| line.split_once('='))
.map(|(key, _)| key.trim().trim_matches('"').to_string())
.collect()
}
#[test]
fn the_manifest_declares_no_async_runtime() {
for table in ["[dependencies]", "[dev-dependencies]"] {
let keys = dependency_keys(CARGO_TOML, table);
assert!(
keys.len() > 1,
"read {} dependencies out of `{table}` — the parser found nothing to check",
keys.len(),
);
for forbidden in FORBIDDEN_CRATES {
let underscored = forbidden.replace('-', "_");
assert!(
!keys
.iter()
.any(|key| key == forbidden || *key == underscored),
"`{table}` declares `{forbidden}`. Constructing its `Timer` spawns the \
process-global `async-io` thread, whose `main_loop` never returns, and it \
aborts the test binary at exit (#190). Use \
`cx.background_executor().timer(duration)` — see this module's docs.",
);
}
}
}
#[test]
fn no_source_file_reaches_for_one() {
let src = repo_root().join("src");
let files = rust_files(&src);
assert!(
files.len() > 20,
"walked src/ and found {} Rust files — the walker is broken",
files.len(),
);
let mut scanned = 0usize;
for file in &files {
if file.file_name().is_some_and(|name| name == SELF) {
continue;
}
scanned += 1;
let code = code_only(&std::fs::read_to_string(file).expect("source file is readable"));
for forbidden in FORBIDDEN_CRATES {
let path = format!("{}::", forbidden.replace('-', "_"));
assert!(
!code.contains(&path),
"{} uses `{path}` outside a comment. That crate's `Timer` spawns the \
`async-io` thread, which never exits and aborts the test binary at \
teardown (#190). Use `cx.background_executor().timer(duration)`.",
file.display(),
);
}
}
assert_eq!(
scanned,
files.len() - 1,
"exactly one file — `{SELF}`, which names the forbidden strings in its own literals \
— is exempt from this scan. {} were skipped.",
files.len() - scanned,
);
}
#[test]
fn the_two_delays_are_scheduled_on_the_executor() {
for (relative, expected) in [("src/input/blink.rs", 2), ("src/elements/toast.rs", 1)] {
let path = repo_root().join(relative);
let code = code_only(&std::fs::read_to_string(&path).expect("source file is readable"));
let found = code.matches("background_executor().timer(").count();
assert_eq!(
found, expected,
"{relative} schedules {found} delay(s) on `background_executor().timer(`, \
expected {expected}. A delay that goes anywhere else is how the `async-io` \
thread came back (#190) — if the call site legitimately moved, move this \
expectation with it.",
);
}
}