Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
[alias]
xtask = "run --package xtask --"
# The UI tests read pixels back from one offscreen GPU renderer each. Run in parallel, the
# readbacks come back blank (see tests/ui.rs, the highlight and ink tests), so the test threads
# are one. Set RUST_TEST_THREADS yourself to override it.
[env]
RUST_TEST_THREADS = "1"
8 changes: 8 additions & 0 deletions .github/workflows/website-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ jobs:
- name: Install the wasm32 target
run: rustup target add wasm32-unknown-unknown

# The demo's dependencies are the same from one build to the next, so they come from the
# cache rather than being compiled again. Only main saves it (the nightly release builds the
# site from main every night); pull requests restore it, as in ci.yml.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: website-wasm
save-if: ${{ github.ref == 'refs/heads/main' }}

- name: Install trunk
run: |
set -eu
Expand Down
4 changes: 4 additions & 0 deletions crates/ui-egui/tests/icons_visual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,10 @@ fn render(scale: f32, bg: Color32, draw: impl Fn(&Painter) + 'static) -> RgbaIma
draw(ui.painter());
});
h.run();
// One readback at a time on the shared device, as in `support/gpu.rs`: two in flight at once
// can come back blank. Everything before this line still runs on every test thread.
static READBACK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _turn = READBACK.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
h.render().expect("an offscreen renderer is required for the visual suite")
}

Expand Down
16 changes: 15 additions & 1 deletion crates/ui-egui/tests/support/gpu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,16 @@
//! on Linux. The same fix the icon tests use (`icons_visual.rs`) applies here: the adapter,
//! device and queue are shared, and each window still gets its own `Renderer` with its own
//! textures and pipelines, so nothing drawn in one test can leak into another.
//!
//! Reading a picture back from that shared device is done one window at a time: two readbacks in
//! flight on the same queue at once came back blank (the highlight and ink tests). Take a picture
//! with [`render`], never `Harness::render` directly. Only the readback waits its turn; building
//! windows, clicking and typing still run on every test thread, which is most of the suite's time.

use egui_kittest::Harness;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use egui_wgpu::{RenderState, Renderer, RendererOptions};
use std::sync::{Arc, OnceLock};
use std::sync::{Arc, Mutex, OnceLock, PoisonError};

fn shared() -> &'static RenderState {
static STATE: OnceLock<RenderState> = OnceLock::new();
Expand All @@ -23,3 +29,11 @@ pub fn renderer() -> WgpuTestRenderer {
let renderer = Renderer::new(&base.device, base.target_format, RendererOptions::PREDICTABLE);
WgpuTestRenderer::from_render_state(RenderState { renderer: Arc::new(egui::mutex::RwLock::new(renderer)), ..base.clone() })
}

/// The window's picture, read back while no other test is reading one from the shared device.
pub fn render<State>(h: &mut Harness<'_, State>) -> Result<image::RgbaImage, String> {
static READBACK: Mutex<()> = Mutex::new(());
// A test that panicked mid-readback leaves the device as usable as before; the next one goes on.
let _turn = READBACK.lock().unwrap_or_else(PoisonError::into_inner);
h.render()
}
34 changes: 17 additions & 17 deletions crates/ui-egui/tests/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ fn review(h: &mut Harness<'_, NoteCraftApp>, name: &str) {
if let Some(dir) = std::env::var_os("NOTECRAFT_UI_VISUAL_DIR") {
let dir = PathBuf::from(dir);
let _ = std::fs::create_dir_all(&dir);
if let Ok(img) = h.render() {
if let Ok(img) = support::gpu::render(h) {
let _ = img.save(dir.join(format!("{name}.png")));
}
}
Expand Down Expand Up @@ -341,7 +341,7 @@ fn header_language_picker_labels_render_light_text_on_the_purple_band() {
h.get_by_label("Interface language: English").click();
settle(&mut h);
let label_rect = h.get_by_label("Fran\u{00e7}ais").rect();
let image = h.render().expect("render open language picker");
let image = support::gpu::render(&mut h).expect("render open language picker");
let scale = image.width() as f32 / 1200.0;
let min_x = (label_rect.min.x * scale).floor().max(0.0) as u32;
let max_x = (label_rect.max.x * scale).ceil().min(image.width() as f32) as u32;
Expand Down Expand Up @@ -490,7 +490,7 @@ fn middle_drag_pans_the_canvas_for_html_and_cmgfm_pages() {
assert_eq!(drawn.pointer("/ink/strokes").and_then(Value::as_u64), Some(1), "open canvas accepts ink for {label}: {drawn}");
let ink_file = path.join("ink.svg");
let svg = std::fs::read_to_string(&ink_file).unwrap_or_else(|error| panic!("saved canvas ink: {error}; state={drawn}"));
let ink_image = h.render().expect("render outside-page stroke");
let ink_image = support::gpu::render(&mut h).expect("render outside-page stroke");
review(&mut h, if markdown { "canvas_open_ink_cmgfm" } else { "canvas_open_ink_html" });
let red_canvas_pixels = ink_image
.pixels()
Expand Down Expand Up @@ -570,7 +570,7 @@ fn drawing_over_text_keeps_ink_above_the_page_without_editing_words() {
);
assert_eq!(state.pointer("/page/caret").cloned().unwrap_or(Value::Null), caret_before, "drawing over words leaves the text caret alone: {state}");

let image = h.render().expect("render ink over words");
let image = support::gpu::render(&mut h).expect("render ink over words");
let region = egui::Rect::from_min_max(egui::pos2(x as f32 - 4.0, y as f32 - 4.0), egui::pos2(x as f32 + 42.0, y as f32 + 12.0));
let red_pixels = image
.pixels()
Expand Down Expand Up @@ -619,14 +619,14 @@ fn pen_palette_owns_pointer_input_over_the_page_canvas() {
let more_colours = h.get_by_label("More Colours").rect();
ask(&mut h, "ui.move", json!({"x": more_colours.center().x, "y": more_colours.center().y})).expect("hover the popup button");
settle(&mut h);
let before = h.render().expect("render the open palette before drawing");
let before = support::gpu::render(&mut h).expect("render the open palette before drawing");
let start = egui::pos2(page.max.x - 40.0, more_colours.center().y);
assert!(page.contains(start) && !more_colours.contains(start), "the stroke begins on page canvas outside the palette");
ask(&mut h, "ui.press", json!({"x": start.x, "y": start.y})).expect("start a page stroke");
pump(&mut h);
ask(&mut h, "ui.move", json!({"x": more_colours.center().x, "y": more_colours.center().y})).expect("carry the stroke across the palette");
pump(&mut h);
let during = h.render().expect("render the stroke beneath the open palette");
let during = support::gpu::render(&mut h).expect("render the stroke beneath the open palette");
let (sx, sy) = (before.width() as f32 / 1200.0, before.height() as f32 / 760.0);
let (x0, y0, x1, y1) = (
(more_colours.min.x * sx).floor().max(0.0) as u32,
Expand Down Expand Up @@ -1342,7 +1342,7 @@ fn the_plus_between_two_pages_is_round() {
for _ in 0..4 {
pump(&mut h);
}
let shot = h.render().expect("a picture");
let shot = support::gpu::render(&mut h).expect("a picture");
// The harness is 1200 points wide; the picture may be rendered larger than that.
let scale = f64::from(shot.width()) / 1200.0;

Expand Down Expand Up @@ -1958,7 +1958,7 @@ fn display_math_paints_ink_inside_its_box() {
notecraft_ui_egui::mathview::display(ui, "x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}", 28.0, egui::Color32::BLACK);
});
h.run();
let img = h.render().expect("render");
let img = support::gpu::render(&mut h).expect("render");
let dark: Vec<(u32, u32)> =
img.enumerate_pixels().filter(|(_, _, p)| p.0[0] < 100 && p.0[1] < 100 && p.0[2] < 100).map(|(x, y, _)| (x, y)).collect();
assert!(dark.len() > 300, "the formula should leave ink, got {} dark pixels", dark.len());
Expand Down Expand Up @@ -2035,7 +2035,7 @@ fn a_swatch_overrides_the_system_mode() {
assert_eq!(h.state().theme(), Theme { mode: other, accent: Accent::Default });
assert_eq!(h.state().mode_pref(), if other == Mode::Dark { ModePref::Dark } else { ModePref::Light }, "the swatch stops following the system");
// And the window really is painted in that mode.
let img = h.render().expect("render");
let img = support::gpu::render(&mut h).expect("render");
let total = img.pixels().count();
let matching = img
.pixels()
Expand Down Expand Up @@ -2180,7 +2180,7 @@ fn page_ink_is_visible_across_container_borders() {

let ink = ask(&mut h, "ui.inspect", json!({})).expect("inspect")["ink"].clone();
assert_eq!(ink["strokes"].as_u64(), Some(1), "the pen made one page stroke: {ink}");
let image = h.render().expect("render the page with the stroke");
let image = support::gpu::render(&mut h).expect("render the page with the stroke");
review(&mut h, "ink_across_boxes");
let scale = image.width() as f64 / 1200.0;
let visible_red_near = |x: f64| {
Expand Down Expand Up @@ -2257,7 +2257,7 @@ fn page_ink_crosses_a_diagram_container_edge() {
settle(&mut h);
let ink = ask(&mut h, "ui.inspect", json!({})).expect("inspect strokes")["ink"].clone();
assert_eq!(ink["strokes"].as_u64(), Some(2), "both strokes remain on the page: {ink}");
let image = h.render().expect("render diagram and ink");
let image = support::gpu::render(&mut h).expect("render diagram and ink");
review(&mut h, "ink_across_diagram_container");
let scale = image.width() as f64 / 1200.0;
let visible_red = |x: f64, at_y: f64| {
Expand Down Expand Up @@ -5468,7 +5468,7 @@ fn all_fourteen_themes_render_with_their_own_colours() {
app.open_path(&s.md_page.to_string_lossy());
let mut h = harness(app, theme);
settle(&mut h);
let img = h.render().expect("render");
let img = support::gpu::render(&mut h).expect("render");
let tokens = theme.tokens();
let near = |p: &image::Rgba<u8>, c: egui::Color32| {
(i32::from(p.0[0]) - i32::from(c.r())).abs() <= 3
Expand Down Expand Up @@ -6779,15 +6779,15 @@ fn a_drag_across_an_arabic_line_selects_it_and_the_words_stay_readable() {
let (at, anchor) = (caret.get("at").and_then(Value::as_u64).unwrap_or(0), caret.get("anchor").and_then(Value::as_u64).unwrap_or(0));
assert_ne!(at, anchor, "and the drag selected something: at {at}, anchor {anchor}");

let selected = h.render().expect("a picture of the window");
let selected = support::gpu::render(&mut h).expect("a picture of the window");
review(&mut h, "rtl-drag-selection");

// The same page with nothing selected, to compare against.
ask(&mut h, "ui.key", json!({"key": "ArrowRight"})).expect("drop the selection");
settle(&mut h);
let caret = ask(&mut h, "ui.inspect", json!({})).expect("inspect").pointer("/page/caret").cloned().unwrap_or(Value::Null);
assert_eq!(caret.get("at").and_then(Value::as_u64), caret.get("anchor").and_then(Value::as_u64), "an arrow without Shift drops the selection");
let plain = h.render().expect("a picture of the window");
let plain = support::gpu::render(&mut h).expect("a picture of the window");

// The band the drag covered, in the rendered picture's own pixels.
let scale = selected.width() as f32 / 1200.0;
Expand Down Expand Up @@ -6851,15 +6851,15 @@ fn a_drag_can_select_across_paragraphs() {
let open = across(&mut h);
assert_eq!(open.pointer("/from/0").and_then(Value::as_u64), Some(0), "it begins in the paragraph the press fell on: {open}");
assert_eq!(open.pointer("/to/0").and_then(Value::as_u64), Some(1), "and reaches the one the drag ended in: {open}");
let selected = h.render().expect("picture");
let selected = support::gpu::render(&mut h).expect("picture");
review(&mut h, "cross-block");

// Both paragraphs are drawn with a band behind them. Compared against the same page
// with nothing selected, which a press on the empty room below the page gives.
ask(&mut h, "ui.click", json!({"x": 600.0, "y": first.min.y - 40.0})).expect("let go");
settle(&mut h);
assert_eq!(across(&mut h), Value::Null, "a press somewhere else lets the selection go");
let plain = h.render().expect("picture");
let plain = support::gpu::render(&mut h).expect("picture");
let scale = selected.width() as f32 / 1200.0;
let band = |rect: egui::Rect| -> u32 {
let (x0, x1) = ((rect.min.x * scale) as u32, (rect.max.x * scale) as u32);
Expand Down Expand Up @@ -6922,7 +6922,7 @@ fn a_drag_highlights_the_words_past_the_marks_in_a_paragraph() {
let open = ask(&mut h, "ui.inspect", json!({})).expect("inspect").pointer("/page/across").cloned().unwrap_or(Value::Null);
assert_eq!(open.pointer("/from/0").and_then(Value::as_u64), Some(0), "the drag spans both paragraphs: {open}");
assert_eq!(open.pointer("/to/0").and_then(Value::as_u64), Some(1), "the drag spans both paragraphs: {open}");
let selected = h.render().expect("picture");
let selected = support::gpu::render(&mut h).expect("picture");
review(&mut h, "cross-block-marks");

// The band is the selection wash painted behind the words; counting its pixels says
Expand Down
Loading