diff --git a/crates/engine/src/tests/scripts.rs b/crates/engine/src/tests/scripts.rs index 5f620b251..8d2add2ea 100644 --- a/crates/engine/src/tests/scripts.rs +++ b/crates/engine/src/tests/scripts.rs @@ -140,7 +140,7 @@ fn pipes_and_folders_are_not_read() { let direct = s.execute("script.check", &json!({ "path": pipe })).map_err(|e| e.to_string()); let _ = tx.send((included.map_err(|e| e.to_string()), direct)); }); - let (included, direct) = rx.recv_timeout(std::time::Duration::from_secs(20)).expect("the checks return"); + let (included, direct) = rx.recv_timeout(std::time::Duration::from_secs(300)).expect("the checks return"); let statuses: Vec = included.unwrap().as_array().unwrap().iter().map(|i| i["status"].as_str().unwrap().to_string()).collect(); assert_eq!(statuses, ["missing", "missing"]); assert!(direct.is_err_and(|e| e.contains("not a file"))); diff --git a/crates/eps/src/import/ai.rs b/crates/eps/src/import/ai.rs index 57d240f9f..0b1481898 100644 --- a/crates/eps/src/import/ai.rs +++ b/crates/eps/src/import/ai.rs @@ -74,6 +74,19 @@ const MAX_ZSTD_WINDOW: u64 = 64 << 20; /// Most Zstandard frames read (the app writes one). const MAX_FRAMES: usize = 4096; +#[cfg(test)] +thread_local! { + /// How many bytes [`Codec::decode`] has looked at on this thread to find whether another + /// Zstandard frame follows or only zero padding is left. + static PADDING_LOOKS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// [`PADDING_LOOKS`] of this thread. +#[cfg(test)] +fn padding_looks() -> u64 { + PADDING_LOOKS.get() +} + const EPS_BEGIN: &[u8] = b"%AI9_PrivateDataBegin"; const EPS_END: &[u8] = b"%AI9_PrivateDataEnd"; @@ -171,7 +184,11 @@ impl Codec { // can itself end in a zero byte, so the padding is what is left after the last frame. let mut src = data; let mut frames = 0usize; - while src.iter().any(|b| *b != 0) { + while src.iter().any(|b| { + #[cfg(test)] + PADDING_LOOKS.set(PADDING_LOOKS.get().saturating_add(1)); + *b != 0 + }) { frames += 1; if frames > MAX_FRAMES { return Err(damaged("its Zstandard compression")); diff --git a/crates/eps/src/import/ai/tests.rs b/crates/eps/src/import/ai/tests.rs index e736fe6da..23b529c3d 100644 --- a/crates/eps/src/import/ai/tests.rs +++ b/crates/eps/src/import/ai/tests.rs @@ -5312,10 +5312,15 @@ fn editing_data_past_the_limit_is_refused() { fn many_zstandard_frames_are_read_in_one_pass_and_capped() { let frame = zstd(b" "); let frames = |n: usize| -> Vec { [&b"%AI24_ZStandard_Data"[..], &frame.repeat(n), &[0; 64]].concat() }; - let started = std::time::Instant::now(); - assert_eq!(decode_private(&frames(4000)).unwrap().len(), 4000); - assert!(decode_private(&frames(50_000)).unwrap_err().contains("Zstandard")); - assert!(started.elapsed() < std::time::Duration::from_secs(20), "{:?}", started.elapsed()); + let (few, many) = (frames(4000), frames(50_000)); + let before = padding_looks(); + assert_eq!(decode_private(&few).unwrap().len(), 4000); + assert!(decode_private(&many).unwrap_err().contains("Zstandard")); + // The first byte of each of the 4,000 frames and of the 4,097 read before the cap, and the + // 64 zeros of padding once: looking past the frames to the padding before each frame would + // look at 64 or more bytes per frame. + let looked = padding_looks() - before; + assert!(looked <= 2 * (4000 + 4097 + 64), "looked at {looked} bytes for padding"); } /// A Zstandard frame that ends in a zero byte keeps it: only the zeros after the last frame are diff --git a/crates/render/src/fx/renderer.rs b/crates/render/src/fx/renderer.rs index 1240e413b..6b7aa49f7 100644 --- a/crates/render/src/fx/renderer.rs +++ b/crates/render/src/fx/renderer.rs @@ -10,6 +10,26 @@ use super::{ effects, fill_rule, peniko, }; +#[cfg(test)] +thread_local! { + /// How many silhouettes of drop shadows and outer glows renderers on this thread have drawn: + /// one per shadow or glow raster made ([`Renderer::shadow_raster`]) and per one drawn through a + /// filter layer ([`Renderer::draw_shadow_filter`]). + static SILHOUETTES: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// [`SILHOUETTES`] of this thread. +#[cfg(test)] +pub(super) fn silhouettes() -> u64 { + SILHOUETTES.get() +} + +/// Adds one to [`SILHOUETTES`]. +#[cfg(test)] +fn note_silhouette() { + SILHOUETTES.set(SILHOUETTES.get().saturating_add(1)); +} + impl Renderer { /// Drop shadow / outer glow `fx` (shadow-cache slot `i` of `n`) as a cached raster: the /// silhouette `paint` draws is rendered once into a small crop, blurred and tinted on the CPU, @@ -125,6 +145,8 @@ impl Renderer { let decoded = crate::paint::decode_failures(); let outer_image_left_out = self.image_left_out.replace(false); let pattern_images_left_out = crate::pattern::images_left_out(); + #[cfg(test)] + note_silhouette(); self.inside_layer(|r| paint(r, &mut off, &shifted(&crop, dx, dy))); let failed = crate::paint::decode_failures() - decoded; let image_left_out = self.image_left_out.unwrap_or(false) || crate::pattern::images_left_out() > pattern_images_left_out; @@ -230,6 +252,8 @@ impl Renderer { // The offset moves the content rather than the filter: vello_cpu drops layer content // that lies entirely outside the viewport before filtering. let reach = reach + Vec2::new(dx, dy); + #[cfg(test)] + note_silhouette(); self.with_filters(ctx, f, reach, blur, Some((mode, opacity)), |r, c, fr, comp| { c.set_transform(fr.view); c.push_layer(None, comp.map(|m| blend_mode(m.0)), comp.map(|m| m.1), None, Some(filter)); diff --git a/crates/render/src/fx/tests.rs b/crates/render/src/fx/tests.rs index 74ff632d6..d964a5244 100644 --- a/crates/render/src/fx/tests.rs +++ b/crates/render/src/fx/tests.rs @@ -467,9 +467,10 @@ fn a_glow_too_large_for_a_full_size_raster_keeps_its_solid_silhouette() { } #[test] -fn thirty_stacked_glows_draw_quickly_cached_or_not() { +fn thirty_stacked_glows_draw_thirty_silhouettes_cached_or_not() { // GUARDS: each glow is of the content with the glows before it; drawing those again for every glow costs 2^n. // A shape draws through the shadow cache, a symbol instance without it (the filter layers). + // Each glow's silhouette is drawn once. let glows: Vec<(&str, serde_json::Value)> = (0..30).map(|k| ("stylize.outerGlow", json!({"color": "#ff0000", "mode": "normal", "opacity": 20, "blur": 1 + k % 4}))).collect(); let shape = doc_with(Rect::new(30.0, 30.0, 70.0, 70.0), Color::BLACK, glows.clone()); @@ -486,9 +487,10 @@ fn thirty_stacked_glows_draw_quickly_cached_or_not() { let l = instanced.layers[0].id; instanced.insert(Some(l), 0, instance).unwrap(); for d in [&shape, &instanced] { - let start = std::time::Instant::now(); + let before = super::renderer::silhouettes(); let p = render(d).pixel(25, 50); - assert!(start.elapsed() < std::time::Duration::from_secs(20), "{:?}", start.elapsed()); + let drawn = super::renderer::silhouettes() - before; + assert_eq!(drawn, 30, "silhouettes drawn for 30 glows"); assert!(p[1] < 250, "the glows show: {p:?}"); } } diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 82a1374da..f7799d02a 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1333,7 +1333,7 @@ fn invalid_dash_pattern_strokes_solid() { let d = line(Some(pattern.clone())); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || tx.send(render(&d)).ok()); - let img = rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("rendering dash {pattern:?} did not finish")); + let img = rx.recv_timeout(std::time::Duration::from_secs(300)).unwrap_or_else(|_| panic!("rendering dash {pattern:?} did not finish")); assert_eq!(img.pixels, solid.pixels, "dash {pattern:?}"); } } diff --git a/crates/svg/src/import/css.rs b/crates/svg/src/import/css.rs index 30432771a..3593a870b 100644 --- a/crates/svg/src/import/css.rs +++ b/crates/svg/src/import/css.rs @@ -120,6 +120,18 @@ fn is_ident(c: char) -> bool { /// How deep `:not()`s may nest (real style sheets nest one or two). const MAX_NOT_NESTING: usize = 8; +#[cfg(test)] +thread_local! { + /// How many times [`Selector::matches`] has tested a selector against an element on this thread. + static SELECTOR_TESTS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// [`SELECTOR_TESTS`] of this thread. +#[cfg(test)] +fn selector_tests() -> u64 { + SELECTOR_TESTS.get() +} + impl Selector { /// `None` for selectors this cascade doesn't support (they never match). fn parse(s: &str) -> Option { @@ -273,6 +285,8 @@ impl Selector { memo.insert(key, hit); hit } + #[cfg(test)] + SELECTOR_TESTS.set(SELECTOR_TESTS.get().saturating_add(1)); go(&self.parts, n, &mut HashMap::new()) } } diff --git a/crates/svg/src/import/css/tests.rs b/crates/svg/src/import/css/tests.rs index 89b519858..d4704192f 100644 --- a/crates/svg/src/import/css/tests.rs +++ b/crates/svg/src/import/css/tests.rs @@ -249,11 +249,11 @@ proptest! { } } -/// 50,000 class rules give 50,000 elements, each with one of the classes, their fill within -/// 20 s. +/// 5,000 class rules give 5,000 elements, each with one of the classes, their fill, testing +/// about one rule per element. #[test] -fn styling_50000_elements_with_50000_rules_finishes_within_20_s() { - const N: usize = 50_000; +fn styling_5000_elements_with_5000_rules_tests_about_one_rule_each() { + const N: usize = 5_000; let rules: String = (0..N).map(|i| format!(".c{i} {{ fill: #{i:06x} }}\n")).collect(); let rects: String = (0..N).map(|i| format!(r#""#)).collect(); let svg = format!(r#"{rects}"#); @@ -262,9 +262,12 @@ fn styling_50000_elements_with_50000_rules_finishes_within_20_s() { let doc = roxmltree::Document::parse(&svg).unwrap(); let st = Styles::new(&doc); let fills: Vec> = doc.descendants().filter(|n| n.tag_name().name() == "rect").map(|n| st.own(n, "fill")).collect(); - tx.send(fills).ok(); + tx.send((fills, selector_tests())).ok(); }); - let fills = rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|_| panic!("styling {N} elements did not finish within 20 s")); + let (fills, tests) = + rx.recv_timeout(std::time::Duration::from_secs(300)).unwrap_or_else(|_| panic!("styling {N} elements did not finish within 300 s")); + // Testing every rule against every element would test N × N selectors. + assert!(tests <= 2 * N as u64, "styling {N} elements tested {tests} selectors"); assert_eq!(fills.len(), N); for (i, fill) in fills.iter().enumerate() { assert_eq!(fill.as_deref(), Some(format!("#{i:06x}").as_str())); diff --git a/crates/text/src/tests/typo.rs b/crates/text/src/tests/typo.rs index 8f37e5905..4791347d0 100644 --- a/crates/text/src/tests/typo.rs +++ b/crates/text/src/tests/typo.rs @@ -704,12 +704,12 @@ fn area_flow_moves_a_word_down_to_a_line_wide_enough_for_it() { } /// Lays `t` out as [`layout`] does, on a thread of its own, and fails the test when that panics or -/// takes more than 20 s. +/// has not ended after 300 s. fn layout_soon(t: &TextObject) -> TextLayout { let (tx, rx) = std::sync::mpsc::channel(); let (t, text) = (t.clone(), t.plain_text()); std::thread::spawn(move || tx.send(layout(db(), &t)).ok()); - rx.recv_timeout(std::time::Duration::from_secs(20)).unwrap_or_else(|e| panic!("laying out {text:?}: {e}")) + rx.recv_timeout(std::time::Duration::from_secs(300)).unwrap_or_else(|e| panic!("laying out {text:?}: {e}")) } /// Each line's baseline, left and right edges, and byte range.