ElyMark: renders asked of a failed page end at once (M-08)
The tally remembers why the page went down; until a load succeeds, render() ends each new request with that reason instead of calling a dead page. end_renders is a free function with a gpui test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c19d027a6
commit
22d2d04d1e
8 files changed
+98
-35
No files matched your search
@@ -27,4 +27,5 @@ serde_json.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
@@ -201,8 +201,6 @@ fn clear(dir: &Path) -> io::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
/// What tab `id` restores: by file, or untitled by number.
|
||||
|
||||
impl Desk {
|
||||
/// A tab just shown takes its kept mode, place, text.
|
||||
pub fn restore_shown(&mut self, id: u64, cx: &mut Context<Self>) {
|
||||
|
||||
+43
-10
@@ -11,11 +11,13 @@ pub struct Tally<R> {
|
||||
reported: u64,
|
||||
renders: HashMap<u64, R>,
|
||||
asked: u64,
|
||||
/// Why the page is down, until it loads again.
|
||||
down: Option<String>,
|
||||
}
|
||||
|
||||
impl<R> Tally<R> {
|
||||
pub fn new() -> Self {
|
||||
Tally { load: 0, changed: 0, reported: 0, renders: HashMap::new(), asked: 0 }
|
||||
Tally { load: 0, changed: 0, reported: 0, renders: HashMap::new(), asked: 0, down: None }
|
||||
}
|
||||
|
||||
pub fn load(&self) -> u64 {
|
||||
@@ -38,8 +40,19 @@ impl<R> Tally<R> {
|
||||
self.lapse()
|
||||
}
|
||||
|
||||
/// The renders owed, as the page fails or restarts.
|
||||
pub fn lapse(&mut self) -> Vec<R> {
|
||||
/// A page loaded: it answers again.
|
||||
pub fn loaded(&mut self) -> Vec<R> {
|
||||
self.down = None;
|
||||
self.restart()
|
||||
}
|
||||
|
||||
/// The page failed: owed and later renders end with `why`.
|
||||
pub fn fail(&mut self, why: String) -> Vec<R> {
|
||||
self.down = Some(why);
|
||||
self.lapse()
|
||||
}
|
||||
|
||||
fn lapse(&mut self) -> Vec<R> {
|
||||
self.renders.drain().map(|(_, render)| render).collect()
|
||||
}
|
||||
|
||||
@@ -62,10 +75,13 @@ impl<R> Tally<R> {
|
||||
}
|
||||
|
||||
/// Owes `render`; its number goes to the page.
|
||||
pub fn ask(&mut self, render: R) -> u64 {
|
||||
pub fn ask(&mut self, render: R) -> Result<u64, (R, String)> {
|
||||
if let Some(why) = &self.down {
|
||||
return Err((render, why.clone()));
|
||||
}
|
||||
self.asked += 1;
|
||||
self.renders.insert(self.asked, render);
|
||||
self.asked
|
||||
Ok(self.asked)
|
||||
}
|
||||
|
||||
pub fn answered(&mut self, ask: u64) -> Option<R> {
|
||||
@@ -99,12 +115,29 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_restart_ends_every_owed_render() {
|
||||
fn a_load_or_failure_ends_every_owed_render() {
|
||||
let mut tally = Tally::new();
|
||||
let ask = tally.ask("export");
|
||||
assert_eq!(tally.restart(), vec!["export"]);
|
||||
// Its late answer finds nobody waiting.
|
||||
assert_eq!(tally.answered(ask), None);
|
||||
let (html, text) = (tally.ask("html").expect("up"), tally.ask("text").expect("up"));
|
||||
let mut owed = tally.loaded();
|
||||
owed.sort();
|
||||
assert_eq!(owed, ["html", "text"]);
|
||||
// Late answers find nobody waiting.
|
||||
assert_eq!((tally.answered(html), tally.answered(text)), (None, None));
|
||||
let pdf = tally.ask("pdf").expect("up");
|
||||
assert_eq!(tally.fail("denied".into()), ["pdf"]);
|
||||
assert_eq!(tally.answered(pdf), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_page_refuses_renders_until_it_loads() {
|
||||
let mut tally = Tally::new();
|
||||
tally.fail("denied".into());
|
||||
assert_eq!(tally.ask("html"), Err(("html", "denied".to_string())));
|
||||
// A refresh is no load: the page is still down.
|
||||
assert!(tally.restart().is_empty());
|
||||
assert_eq!(tally.ask("text"), Err(("text", "denied".to_string())));
|
||||
assert!(tally.loaded().is_empty());
|
||||
assert!(tally.ask("pdf").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+29
-22
@@ -37,6 +37,20 @@ pub const LARGE: usize = 2 * 1024 * 1024;
|
||||
/// A render's answer: the page's reply, or why not.
|
||||
type Rendered = Box<dyn FnOnce(Result<Value, String>, &mut gpui::App)>;
|
||||
|
||||
/// Renders the page will never answer end with `why`.
|
||||
fn end_renders(pending: Vec<Rendered>, why: &str, cx: &mut gpui::App) {
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
log::warn!("mark: {} renders end: {why}", pending.len());
|
||||
let why = why.to_string();
|
||||
cx.defer(move |cx| {
|
||||
for done in pending {
|
||||
done(Err(why.clone()), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The page failed; the shell shows why.
|
||||
pub struct Failed(pub String);
|
||||
|
||||
@@ -186,7 +200,7 @@ impl MarkView {
|
||||
self.saved = mark(&text);
|
||||
// Reports and renders from before belong to the old text.
|
||||
let lapsed = self.tally.restart();
|
||||
self.end_renders(lapsed, "the text was replaced from disk", cx);
|
||||
end_renders(lapsed, "the text was replaced from disk", cx);
|
||||
crate::page::refresh(self.web.read(cx), &text, self.tally.load());
|
||||
cx.emit(Reported);
|
||||
}
|
||||
@@ -209,9 +223,13 @@ impl MarkView {
|
||||
done: impl FnOnce(Result<Value, String>, &mut gpui::App) + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let ask = self.tally.ask(Box::new(done));
|
||||
let ask = serde_json::json!({ "kind": kind, "ask": ask });
|
||||
crate::page::call(self.web.read(cx), "render", &ask);
|
||||
match self.tally.ask(Box::new(done)) {
|
||||
Ok(ask) => {
|
||||
let ask = serde_json::json!({ "kind": kind, "ask": ask });
|
||||
crate::page::call(self.web.read(cx), "render", &ask);
|
||||
}
|
||||
Err((done, why)) => end_renders(vec![done], &why, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// `text` replaces the editor's, as one undoable edit.
|
||||
@@ -257,8 +275,8 @@ impl MarkView {
|
||||
match event {
|
||||
// A reload shows the last text that arrived.
|
||||
Event::Loaded => {
|
||||
let lapsed = self.tally.restart();
|
||||
self.end_renders(lapsed, "the page reloaded", cx);
|
||||
let lapsed = self.tally.loaded();
|
||||
end_renders(lapsed, "the page reloaded", cx);
|
||||
self.shown = false;
|
||||
let text = self.doc.lock().expect("the text lock").current.clone();
|
||||
let at = (self.mode, self.tally.load(), self.place);
|
||||
@@ -268,8 +286,8 @@ impl MarkView {
|
||||
Event::Query(said, reply) => self.query(said, reply, cx),
|
||||
Event::Leave(url) => self.follow(url, cx),
|
||||
Event::Failed(why) => {
|
||||
let lapsed = self.tally.lapse();
|
||||
self.end_renders(lapsed, why, cx);
|
||||
let lapsed = self.tally.fail(why.clone());
|
||||
end_renders(lapsed, why, cx);
|
||||
cx.emit(Failed(why.clone()));
|
||||
}
|
||||
Event::Printed(path, _) => {
|
||||
@@ -278,20 +296,6 @@ impl MarkView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the old page will never answer end with `why`.
|
||||
fn end_renders(&mut self, pending: Vec<Rendered>, why: &str, cx: &mut Context<Self>) {
|
||||
if pending.is_empty() {
|
||||
return;
|
||||
}
|
||||
log::warn!("mark: {} renders lost with the page", pending.len());
|
||||
let why = why.to_string();
|
||||
cx.defer(move |cx| {
|
||||
for done in pending {
|
||||
done(Err(why.clone()), cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// A clicked link: web and mail open outside.
|
||||
fn follow(&mut self, url: &str, cx: &mut Context<Self>) {
|
||||
if ely_office::outward(url) {
|
||||
@@ -486,3 +490,6 @@ impl Render for MarkView {
|
||||
div().size_full().on_drop(drop).child(self.web.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
#[gpui::test]
|
||||
fn each_owed_render_ends_once_with_the_reason(cx: &mut gpui::TestAppContext) {
|
||||
let got = Rc::new(RefCell::new(Vec::<(u8, Result<Value, String>)>::new()));
|
||||
let pending = (0..2)
|
||||
.map(|n| {
|
||||
let got = got.clone();
|
||||
Box::new(move |rendered, _: &mut gpui::App| got.borrow_mut().push((n, rendered))) as super::Rendered
|
||||
})
|
||||
.collect();
|
||||
cx.update(|cx| super::end_renders(pending, "denied", cx));
|
||||
cx.run_until_parked();
|
||||
let denied = || Err("denied".to_string());
|
||||
assert_eq!(*got.borrow(), [(0, denied()), (1, denied())]);
|
||||
}
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
- [x] M-05 点页面里的链接什么也不发生 — 页面拦下链接单击,地址取链接标记原文,预览单击、可视 Cmd/Ctrl 单击交应用(审查 4 轮)
|
||||
- [x] M-06 `on_app_quit` 无条件删恢复点与崩溃锁,AppKit 直接终止时丢未保存内容 — 未保存时退出只放下锁、不清点,窗口释放时不再删(审查 1 轮)
|
||||
- [x] M-07 崩溃恢复的未命名副本按“第一个显示的未命名标签”配对,内容可能落错标签 — 未命名副本按新建时记下的标签号配对,经 Keeping::take(审查 2 轮)
|
||||
- [ ] M-08 页面重载后悬着的渲染回调永不完成,导出既不成功也不报错
|
||||
- [x] M-08 页面重载后悬着的渲染回调永不完成,导出既不成功也不报错 — 页面失败后挂起与新到的渲染都以错误结束,载入成功才恢复(审查 2 轮)
|
||||
- [ ] M-09 导出的保存面板出错被当作取消
|
||||
- [ ] M-10 关闭查找框只清当前标签的高亮
|
||||
- [ ] M-11 页面把搜索词写进应用日志
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
FAIL
|
||||
|
||||
apps/ely-mark/src/view.rs:212 — `render` queues requests after the page has failed; `heard` at line 270 only ends requests already pending. Native WSL CEF reproduction: deny read access to the disposable page's index.html and reload. Both existing renders correctly receive ERR_ACCESS_DENIED. Request another render while the fault is displayed: Chromium logs `ely is not defined`, and its callback never runs until a later successful reload ends it with `the page reloaded`. HTML, text and PDF exports therefore still hang without an output or export error when requested after failure. Retain the failure state and asynchronously reject new renders until a successful load clears it. Evidence: `/home/raidenmei/m08-native-probe.log`; rendering succeeds again after recovery.
|
||||
|
||||
apps/ely-mark/src/tally.rs:102 — The render regression checks one value returned by `restart`, but never verifies callback execution through `Loaded`/`Failed`, `end_renders`, or the export error handlers. In a disposable source copy, replacing the entire `end_renders` body with a no-op leaves all 22 Linux tests passing while silently dropping every interrupted export. The native CEF probe detects that mutation: after failure, zero callbacks complete instead of two. Add regression tests that exercise both lifecycle events with multiple owed renders, assert each callback receives its error exactly once and reaches the export failure path, reject late replies, and cover requests made after failure. Evidence: `/home/raidenmei/m08-mutation-tests.log`, `/home/raidenmei/m08-native-mutation.log`. Baseline cargo tests: Linux 22/22; Windows 20/21, with only the documented W-02 trailing-space Git-path failure (20/20 when excluded). Probe copies, their target directories and both platforms' incremental caches were removed before this verdict; only small evidence logs remain.
|
||||
@@ -0,0 +1 @@
|
||||
PASS
|
||||
Reference in new issue
Block a user