diff --git a/crates/ui-egui/tests/content.rs b/crates/ui-egui/tests/content.rs index d75de6118..9294ec267 100644 --- a/crates/ui-egui/tests/content.rs +++ b/crates/ui-egui/tests/content.rs @@ -6,6 +6,17 @@ use egui_kittest::Harness; use egui_kittest::kittest::Queryable; use pdfcraft_ui_egui::{PdfCraftApp, QuickTool}; +/// Tests that render pixels (`h.render()`) take this first, so only one wgpu device exists at a +/// time. Lavapipe crashes when another test creates its Vulkan instance meanwhile (segfault in +/// `vkCreateSampler` during a concurrent `vkCreateInstance`). Declared before the harness, so it +/// is released after the device is dropped. +static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +fn gpu() -> std::sync::MutexGuard<'static, ()> { + // A test that panicked while holding it leaves nothing to clean up. + GPU.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + fn harness() -> Harness<'static, PdfCraftApp> { harness_with_step_dt(0.25) } @@ -78,6 +89,7 @@ fn texts(h: &Harness<'static, PdfCraftApp>) -> Vec { #[test] fn text_tool_drafts_grow_for_latin_and_arabic() { + let _gpu = gpu(); for text in ["Wide words ".repeat(8), "سلام عليكم ".repeat(8)] { let mut h = harness(); assert!(h.state_mut().execute("edit.text")); @@ -145,6 +157,7 @@ fn text_tool_single_click_retypes_added_text_without_another_item() { #[test] fn text_tool_deleting_a_draft_hides_the_old_added_glyphs_and_cancel_restores_them() { + let _gpu = gpu(); let mut h = harness(); assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::AddText { page: 0, @@ -343,6 +356,7 @@ fn a_picture_on_the_clipboard_is_pasted_onto_the_page() { /// written to the page as `?` (or lost). #[test] fn text_the_standard_fonts_cant_draw_stays_in_the_editor() { + let _gpu = gpu(); let mut h = harness(); assert!(h.state_mut().execute("edit.text")); h.run_steps(2); @@ -601,6 +615,7 @@ fn drag_added_text(h: &mut Harness<'static, PdfCraftApp>) -> ([f64; 4], egui::Re /// until the button is released. #[test] fn dragged_text_follows_the_pointer_before_release() { + let _gpu = gpu(); let mut h = harness(); let (rect, from, to, end) = drag_added_text(&mut h); if let Ok(dir) = std::env::var("PDFCRAFT_SHOTS") { @@ -619,6 +634,7 @@ fn dragged_text_follows_the_pointer_before_release() { /// instead of jumping back to where it was for those frames. #[test] fn released_text_does_not_jump_back_before_the_page_is_redrawn() { + let _gpu = gpu(); // A page slow enough to render that the frames before its new raster can be looked at: many // pale squares, too light to count as ink. let mut content = String::from("0.9 g\n"); @@ -805,6 +821,7 @@ fn settle(h: &mut Harness<'static, PdfCraftApp>) { /// the whole drag, and to show there again after the release. #[test] fn a_resized_added_picture_shows_only_at_its_new_size() { + let _gpu = gpu(); let mut h = harness(); assert!(h.state_mut().execute("edit.edit_text")); h.run_steps(2); diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs index b0136a2bd..e23ac966d 100644 --- a/crates/ui-egui/tests/editing.rs +++ b/crates/ui-egui/tests/editing.rs @@ -10,6 +10,17 @@ use egui_kittest::kittest::{NodeT, Queryable}; use pdfcraft_render::{PageRenderer, RenderRequest, RequestKind}; use pdfcraft_ui_egui::{CloseRequest, Dialog, OpenOutcome, PdfCraftApp}; +/// Tests that render pixels (`h.render()`) take this first, so only one wgpu device exists at a +/// time. Lavapipe crashes when another test creates its Vulkan instance meanwhile (segfault in +/// `vkCreateSampler` during a concurrent `vkCreateInstance`). Declared before the harness, so it +/// is released after the device is dropped. +static GPU: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +fn gpu() -> std::sync::MutexGuard<'static, ()> { + // A test that panicked while holding it leaves nothing to clean up. + GPU.lock().unwrap_or_else(std::sync::PoisonError::into_inner) +} + /// An `n`-page document with a proper xref table; page `i` shows "Page i+1". fn fixture(n: usize) -> Vec { let mut objs: Vec = vec!["<< /Type /Catalog /Pages 2 0 R >>".into()]; @@ -75,6 +86,7 @@ fn type_selection(h: &mut Harness<'static, PdfCraftApp>, text: &str) { #[test] fn organize_page_range_tracks_thumbnails_and_applies_typed_selection() { use egui::accesskit::Role; + let _gpu = gpu(); let mut h = organize(100); h.get_by_label("Select page 2").click(); h.run_steps(2); @@ -2810,6 +2822,7 @@ fn clicking_existing_text_selects_its_source_font_style() { #[test] fn deleting_a_draft_does_not_show_the_original_text_underneath() { + let _gpu = gpu(); let mut h = harness(1, |_| {}); assert!(h.state_mut().execute("edit.edit_text")); h.run_steps(2); @@ -2856,6 +2869,7 @@ fn deleting_a_draft_does_not_show_the_original_text_underneath() { #[test] fn switching_from_a_narrow_editor_keeps_one_frame_and_a_wide_heading_on_one_line() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text( "BT /F1 18 Tf 20 220 Td (Editable PDF heading) Tj ET BT /F2 18 Tf 20 120 Td (Mono) Tj ET BT /F2 18 Tf 20 98 Td (Text) Tj ET", ); @@ -2916,6 +2930,7 @@ fn switching_from_a_narrow_editor_keeps_one_frame_and_a_wide_heading_on_one_line /// dark for pale text) until its textless background was ready, and drew it again in its own fonts. #[test] fn opening_a_paragraph_leaves_the_page_as_it_is() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text("0 0 0.45 rg 10 180 280 70 re f 1 1 1 rg BT /F2 20 Tf 20 205 Td (White on a banner) Tj ET"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); @@ -2976,6 +2991,7 @@ fn opening_a_paragraph_leaves_the_page_as_it_is() { /// never spans the first one (it took the first paragraph's measured extent for a frame). #[test] fn a_quick_click_into_another_paragraph_keeps_both_in_place() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text("BT /F1 24 Tf 20 220 Td (Top) Tj ET BT /F2 24 Tf 20 120 Td (Bottom) Tj ET"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); @@ -3097,6 +3113,7 @@ fn ink(pixels: &image::RgbaImage, r: egui::Rect) -> usize { /// used to come back under the new outline for those frames and then jump to the new one. #[test] fn an_applied_paragraph_keeps_its_edited_look_until_the_page_is_redrawn() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text("BT /F1 24 Tf 20 220 Td (Topmost) Tj ET BT /F2 24 Tf 20 120 Td (Bottom) Tj ET"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); @@ -3339,6 +3356,7 @@ fn text_tool_clicking_source_text_edits_instead_of_adding_a_copy() { #[test] fn text_tool_mixed_style_row_grows_in_the_editor_and_saved_pdf() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text("BT /F1 12 Tf 20 220 Td (Small ) Tj /F2 12 Tf (row) Tj ET"); let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { let mut app = PdfCraftApp::new(); @@ -3980,6 +3998,7 @@ fn paragraph_drag_shot(h: &mut Harness<'static, PdfCraftApp>, name: &str) { } fn check_paragraph_overlap_drag(identical: bool, reopen_between: bool) { + let _gpu = gpu(); let (stationary, moving) = if identical { ("Repeated", "Repeated") } else { ("Stationary beta", "Moving alpha") }; let content = format!("BT /F1 12 Tf 60 220 Td ({stationary}) Tj ET BT /F1 12 Tf 250 100 Td (Keep here) Tj ET BT /F1 12 Tf 60 320 Td ({moving}) Tj ET"); @@ -4590,6 +4609,7 @@ fn host_dirty_hears_when_unsaved_work_appears_and_goes() { #[test] fn one_editor_contains_mixed_styles_and_visual_rows() { + let _gpu = gpu(); let bytes = source_font_fixture_with_text( "BT 0 0.2 0.5 rg /F1 1 Tf 18 0 0 18 20 240 Tm (Heading) Tj ET \ BT 0 g /F2 12 Tf 20 215 Td (Plain ) Tj /F1 12 Tf 0 0.2 0.5 rg (bold) Tj /F2 12 Tf 0 g ( words) Tj ET \ @@ -4820,6 +4840,7 @@ fn private_document_regions_open_as_one_editor() { fn inline_editor_preserves_source_font_and_previews_live_bounds() { use pdfcraft_cos::{Dict, Document, Object, Stream, write_incremental}; use skrifa::MetadataProvider as _; + let _gpu = gpu(); let mut cos = Document::open(std::sync::Arc::new(source_font_fixture_with_text( "BT /F1 18 Tf 20 220 Td (Stable source text) Tj ET BT /F1 12 Tf 20 194 Td (The second row stays here) Tj ET", ))) @@ -5212,6 +5233,7 @@ fn red(pixels: &image::RgbaImage, r: egui::Rect) -> usize { #[test] fn a_resized_image_keeps_its_new_size_until_the_page_is_redrawn() { use pdfcraft_cos::{Dict, Document, Object, Stream, write_incremental}; + let _gpu = gpu(); let mut doc = Document::open(std::sync::Arc::new(fixture(1))).unwrap(); let mut image = Dict::new(); image.set(b"Type".to_vec(), Object::name("XObject")); @@ -5757,6 +5779,7 @@ fn read_mode_shortcut_applies_the_open_paragraph() { /// page clip) opens in an editor that draws on its page only. It covered the next page. #[test] fn a_paragraph_running_off_its_page_edits_without_covering_the_next_page() { + let _gpu = gpu(); let text = |first: &str, count: usize, leading: usize| { let mut s = String::from("BT /F1 14 Tf\n"); for i in 0..count { diff --git a/crates/ui-egui/tests/print.rs b/crates/ui-egui/tests/print.rs index ddd8aac2e..1e2a94138 100644 --- a/crates/ui-egui/tests/print.rs +++ b/crates/ui-egui/tests/print.rs @@ -682,6 +682,13 @@ fn a_dead_options_read_wakes_an_idle_dialog() { d.driver_options_loading = Some("Office".into()); d.read_options_for = Some("Office".into()); d.show_driver_options = true; + // The preview's page rasters render on the pool and ask for frames until they arrive; on a + // loaded machine that outlasts `run`'s step budget, so the dialog is idle only after them. + let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10); + while h.state().render_pending() && std::time::Instant::now() < deadline { + h.run_steps(1); + std::thread::sleep(std::time::Duration::from_millis(5)); + } h.run(); assert!(!h.ctx.has_requested_repaint(), "the dialog is idle while the read waits"); drop(hold);