diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index a86c72ef7d..392a3a5d22 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -138,12 +138,19 @@ impl PdfCraftApp { Ok(token) } + /// How many opens may wait for the worker: its bound, less one place while a password prompt + /// is open, so the prompt's retry fits even before the worker has taken any of the others. + #[cfg(not(target_arch = "wasm32"))] + fn open_slots(&self) -> usize { + open_worker::MAX_QUEUED_OPENS - usize::from(self.password_prompt.is_some()) + } + /// Hand waiting files to the worker, oldest first, while its queue has room. Every queued - /// job has a `pending_opens` entry, so fewer entries than the bound means room. + /// job has a `pending_opens` entry, so fewer entries than [`Self::open_slots`] means room. #[cfg(not(target_arch = "wasm32"))] fn submit_deferred_opens(&mut self) { let mut queue = std::mem::take(&mut self.deferred_opens); - while self.deferred_opens.is_empty() && self.pending_opens.len() < open_worker::MAX_QUEUED_OPENS { + while self.deferred_opens.is_empty() && self.pending_opens.len() < self.open_slots() { let Some(next) = queue.pop_front() else { break }; self.open_path_request(&next.path, next.then, next.display_override); } @@ -730,7 +737,7 @@ impl PdfCraftApp { self.startup_superseded = true; // The worker queue is bounded. A long file list (a launch behind a password prompt, // or many files at once) waits here, in order, and is submitted as opens finish. - if self.pending_opens.len() >= open_worker::MAX_QUEUED_OPENS || !self.deferred_opens.is_empty() { + if self.pending_opens.len() >= self.open_slots() || !self.deferred_opens.is_empty() { self.deferred_opens.push_back(DeferredOpen { path: path.to_string(), then, display_override }); return OpenOutcome::Pending; } diff --git a/crates/ui-egui/src/platform/open_worker/tests.rs b/crates/ui-egui/src/platform/open_worker/tests.rs index efe911bff6..bb44d9290b 100644 --- a/crates/ui-egui/src/platform/open_worker/tests.rs +++ b/crates/ui-egui/src/platform/open_worker/tests.rs @@ -921,3 +921,30 @@ fn a_repeated_waiting_open_keeps_the_tool_it_brings() { assert_eq!(app.open_path(&path), crate::OpenOutcome::Pending); assert_eq!(app.deferred_opens.front().and_then(|d| d.then), Some("export.text")); } + +/// A password prompt keeps a place in the worker's bounded queue for its retry: files named after +/// the protected one fill the rest and wait in order, and the retry is queued, not refused as +/// "too many documents are waiting to open" while the worker has yet to take the others. +#[test] +#[allow(clippy::expect_used)] +fn a_password_prompt_keeps_a_place_in_the_open_queue_for_its_retry() { + let mut harness = tool_open_harness(); + let held = HeldToolOpen::install(&mut harness); + let locked = tool_open_file("queue-locked", "pdf", &encrypted_tool_fixture()); + harness.state_mut().open_path(&locked.0.to_string_lossy()); + held.wait_started(); + let others: Vec = + (0..open_worker::MAX_QUEUED_OPENS + 4).map(|i| tool_open_file(&format!("queue-{i}"), "pdf", &fixture(1))).collect(); + for file in &others { + harness.state_mut().open_path(&file.0.to_string_lossy()); + } + held.complete(&mut harness); + let app = harness.state(); + assert!(app.password_prompt.is_some(), "the protected file asks for its password"); + assert!(app.pending_opens.len() < open_worker::MAX_QUEUED_OPENS, "one place stays free: {} waiting", app.pending_opens.len()); + assert!(!app.deferred_opens.is_empty(), "the rest wait their turn"); + harness.state_mut().submit_password(Some("tool-password".into())); + let app = harness.state(); + assert!(app.password_prompt.is_none(), "the retry is queued: {:?}", app.password_prompt.as_ref().and_then(|p| p.error.clone())); + assert!(app.password_retry.is_some()); +}