use std::fs;
use std::path::{Path, PathBuf};
fn workspace_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(Path::parent)
.expect("crates/frust has a grandparent (the workspace root)")
.to_path_buf()
}
const FRAME_FILES: &[&str] = &[
"crates/frust-shell-android/src/app/frame.rs",
"crates/frust-shell-ios/src/app/frame.rs",
];
fn read_frame_file(rel: &str) -> String {
let path = workspace_root().join(rel);
fs::read_to_string(&path).unwrap_or_else(|e| panic!("reading {}: {e}", path.display()))
}
fn is_comment_only(line: &str) -> bool {
line.trim_start().starts_with("//")
}
fn hits(src: &str, needle: &str) -> Vec<(usize, String)> {
src.lines()
.enumerate()
.filter(|(_, line)| !is_comment_only(line) && line.contains(needle))
.map(|(i, line)| (i + 1, line.trim().to_string()))
.collect()
}
#[test]
fn each_shell_commits_last_focus_ime_gen_exactly_once() {
for rel in FRAME_FILES {
let contents = read_frame_file(rel);
let found = hits(&contents, "last_focus_ime_gen = ");
assert_eq!(
found.len(),
1,
"{rel} must contain exactly one `self.last_focus_ime_gen = ` commit site — \
its single generation-cache commit, past the `decide_paced(..).is_skip()` \
early return. A second commit site (or zero) means this scan can no longer \
tell the peek from the commit; found {}:\n{}",
found.len(),
found
.iter()
.map(|(n, l)| format!(" line {n}: {l}"))
.collect::<Vec<_>>()
.join("\n"),
);
}
}
#[test]
fn each_shell_commits_last_focus_ime_gen_only_past_the_skip_return() {
for rel in FRAME_FILES {
let contents = read_frame_file(rel);
let skip_hits = hits(&contents, "is_skip()");
assert_eq!(
skip_hits.len(),
1,
"{rel} must contain exactly one `is_skip()` early-return check — the \
`decide_paced(..).is_skip()` gate this test orders the commit against; \
found {}, expected the scan to see exactly one call site",
skip_hits.len(),
);
let (skip_line, skip_text) = &skip_hits[0];
let commit_hits = hits(&contents, "last_focus_ime_gen = ");
let (commit_line, commit_text) = commit_hits.first().unwrap_or_else(|| {
panic!(
"{rel} has no `self.last_focus_ime_gen = ` commit site to order against \
`is_skip()` — see `each_shell_commits_last_focus_ime_gen_exactly_once`"
)
});
assert!(
commit_line > skip_line,
"{rel}: the `last_focus_ime_gen` commit (line {commit_line}: `{commit_text}`) \
must appear AFTER the `is_skip()` early return (line {skip_line}: \
`{skip_text}`), not before it. A commit sited before the skip return drains \
the focus/IME edge on a skipped tick and re-opens the deferred-repaint bug \
`dee29bb` fixed — the edge must survive a Skip so the next tick that actually \
runs still observes it.",
);
}
}
#[test]
fn no_shell_drains_last_focus_ime_gen_via_mem_replace() {
for rel in FRAME_FILES {
let contents = read_frame_file(rel);
let found: Vec<(usize, String)> = contents
.lines()
.enumerate()
.filter(|(_, line)| {
!is_comment_only(line)
&& line.contains("mem::replace")
&& line.contains("last_focus_ime_gen")
})
.map(|(i, line)| (i + 1, line.trim().to_string()))
.collect();
assert!(
found.is_empty(),
"{rel} drains `last_focus_ime_gen` via `mem::replace` ({} site(s)) — the old \
drain-at-gather-time shape `dee29bb` replaced with a peek (a plain read) at \
gather time and a plain assignment committed only past the `is_skip()` early \
return. A `mem::replace` here re-opens the same deferred-repaint bug:\n{}",
found.len(),
found
.iter()
.map(|(n, l)| format!(" line {n}: {l}"))
.collect::<Vec<_>>()
.join("\n"),
);
}
}