Merge pull request #6935 from dotCooCoo/fix/eps-linear-time-test-steps

EPS tests: the reader's long tests check counts and shared state instead of a 20 s limit
This commit is contained in:
BFlat
2026-11-13 03:27:59 +07:00
committed by GitHub
3 changed files with 82 additions and 16 deletions
+2
View File
@@ -24,6 +24,8 @@ mod state;
mod strips;
pub(crate) use paint::collapse;
#[cfg(test)]
pub(crate) use paint::tilings_compared;
pub(crate) use state::{GState, Out, Space, process};
pub(crate) use strips::merge_strips;
+20 -1
View File
@@ -5,6 +5,19 @@ use super::{
PsError, REGISTRATION, Rc, Rect, Res, SHADING_SAMPLES, Shared, UNKNOWN_FUNCTIONS, UNKNOWN_SHADINGS, Vec2, clamp_between, ps_err,
};
#[cfg(test)]
thread_local! {
/// How many patterns the lookups of tiling patterns on this thread compared with the pattern
/// they looked for.
static TILINGS_COMPARED: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
}
/// [`TILINGS_COMPARED`] of this thread.
#[cfg(test)]
pub(crate) fn tilings_compared() -> u64 {
TILINGS_COMPARED.get()
}
impl Interp<'_> {
// ---------- colour ----------
@@ -212,7 +225,13 @@ impl Interp<'_> {
let proc = get("PaintProc").ok_or(PsError::Ps("undefined", "PaintProc".into()))?;
let key = proc.items().cloned().ok_or(PsError::Ps("typecheck", "PaintProc".into()))?;
let hash = tiling_hash(&key, tile, under.as_ref());
let made = self.out.tilings.get(&hash).and_then(|list| list.iter().find(|(p, t, u, _)| p.same(&key) && *t == tile && *u == under));
let made = self.out.tilings.get(&hash).and_then(|list| {
list.iter().find(|(p, t, u, _)| {
#[cfg(test)]
TILINGS_COMPARED.set(TILINGS_COMPARED.get().saturating_add(1));
p.same(&key) && *t == tile && *u == under
})
});
if let Some((.., name)) = made {
return Ok(Paint::Pattern { pattern: name.clone(), xf });
}
+60 -15
View File
@@ -7,6 +7,9 @@ use vectorcraft_color::{Color, GradientKind, Paint};
use vectorcraft_doc::{AppearanceItem, ColorMode, Document, LineCap, LineJoin, Node, NodeKind};
use vectorcraft_geom::{Point, Rect};
use crate::import::graphics::{GState, Out, tilings_compared};
use crate::import::interp::Interp;
use crate::import::obj::Obj;
use crate::import::{Dsc, import};
use crate::{EpsOptions, Level, Preview, Raster};
@@ -369,12 +372,19 @@ fn dash_patterns_hold_at_most_65535_entries() {
/// Saved and copied graphics states share the dash pattern, and `setdash`, `currentdash` and
/// each stroke count the entries they read or copy against the budgets. Under a pattern of
/// 65,535 entries, 4,000,000 passes of `gsave grestore` and 2,000,000 passes of
/// `gstate currentgstate setgstate` finish, and loops of `setdash`, `currentdash` and `strokepath`
/// run out of a budget.
/// 65,535 entries, the states that `gsave`, `grestore`, `gstate`, `currentgstate` and `setgstate`
/// save, make or set hold that one pattern, 4,000,000 passes of `gsave grestore` and 2,000,000
/// passes of `gstate currentgstate setgstate` finish, and loops of `setdash`, `currentdash` and
/// `strokepath` run out of a budget.
#[test]
fn work_on_the_longest_dash_pattern_ends_soon() {
let dash = "0 0 10 10 rectfill /a 65535 array def 0 1 65534 { a exch 1 put } for a 0 setdash";
let shared = run_looking(&format!("{dash} gsave gsave grestore gstate gstate currentgstate dup setgstate"), |it| {
let Some(saved) = it.saved.last().map(|g| g.dash.clone()) else { return false };
let made: Vec<_> = it.stack.iter().filter_map(|o| if let Obj::GState(g) = o { Some(g.dash.clone()) } else { None }).collect();
saved.len() == 65_535 && made.len() == 2 && made.iter().chain([&it.g.dash]).all(|m| std::rc::Rc::ptr_eq(m, &saved))
});
assert!(shared);
for body in ["4000000 { gsave grestore } repeat", "2000000 { gstate currentgstate setgstate } repeat"] {
let r = read_soon(&format!("{dash} {body} 20 20 10 10 rectfill"));
assert_eq!(objects(&r.document).len(), 2, "{body}: {:?}", r.warnings);
@@ -530,13 +540,15 @@ fn non_finite_arithmetic_is_undefinedresult() {
assert!(!objects(&r.document).is_empty(), "{:?}", r.warnings);
}
/// Tiling patterns and spot inks are found again through a hash of what tells them apart, so a
/// program that makes 200,000 patterns and 200,001 inks reads within [`read_soon`]'s 20 seconds.
/// Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A pattern made
/// again from its prototype is the same pattern, and so is an uncolored pattern set again in a
/// color it was set in before.
/// Tiling patterns are found again through a hash of what tells them apart. A program that sets
/// 200,000 different patterns and also sets 200,001 spot inks reads them all, and its 200,000
/// pattern lookups compare at most 200,000 earlier patterns in all; going through every pattern
/// made so far would compare about 2 × 10^10. The test does not count or time the spot ink
/// lookups. Patterns are named Pattern 1, Pattern 2 and so on in the order they are made. A
/// pattern made again from its prototype is the same pattern, and so is an uncolored pattern set
/// again in a color it was set in before.
#[test]
fn many_tiling_patterns_and_spot_inks_read_in_linear_time() {
fn many_tiling_patterns_are_found_again_through_a_hash() {
let r = read(
"/pp { pop 0 0 1 1 rectfill } def \
/a << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep 2 /YStep 2 /PaintProc /pp load >> def \
@@ -560,7 +572,7 @@ fn many_tiling_patterns_and_spot_inks_read_in_linear_time() {
assert!(names.iter().all(|n| d.swatch(n).is_some_and(|s| matches!(s.paint, Paint::Pattern { .. }))));
// 100,000 patterns whose cells differ, an uncolored pattern in 100,000 tints of an ink that is
// white at every tint (only the tint tells those patterns apart), and 200,000 more inks.
let r = read_soon(
let (r, compared) = read_counting_tilings(
"0 1 99999 { /i exch def << /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 1 1] /XStep i 2 add /YStep 1 \
/PaintProc { pop 0 0 1 1 rectfill } >> matrix makepattern setpattern } for \
[/Separation (Spot) /DeviceGray { pop 1 }] setcolorspace \
@@ -568,6 +580,7 @@ fn many_tiling_patterns_and_spot_inks_read_in_linear_time() {
matrix makepattern def 0 1 99999 { 100000 div p setpattern } for \
0 1 199999 { /i exch def [/Separation i 8 string cvs /DeviceGray {}] setcolorspace } for 0 0 1 1 rectfill",
);
assert!(compared <= 200_000, "{compared} patterns compared");
let d = &r.document;
assert_eq!(d.patterns.len(), 200_000);
assert!(d.patterns.iter().enumerate().all(|(i, p)| p.name == format!("Pattern {}", i + 1)));
@@ -1112,19 +1125,51 @@ fn hostile_executable_files_and_type_loops_are_refused() {
blue("currentfile cvx exec currentfile flushfile 0 0 1 setrgbcolor");
}
/// Reads `body` as [`read`] does, on a thread of its own, and fails the test when reading takes
/// more than 20 s.
/// How long [`read_soon`] and [`read_counting_tilings`] wait, in seconds: a guard against a read
/// that never ends. The budgets, limits and counts each test names decide what it checks.
const NEVER_ENDS: u64 = 300;
/// Reads `body` as [`read`] does, on a thread of its own, and fails the test when reading has not
/// finished after [`NEVER_ENDS`] seconds.
fn read_soon(body: &str) -> crate::Imported {
read_within(body, 20)
read_within(body, NEVER_ENDS)
}
/// Reads `body` as [`read`] does, on a thread of its own, and fails the test when reading takes
/// more than `secs` seconds.
fn read_within(body: &str, secs: u64) -> crate::Imported {
read_counting(body, secs).0
}
/// [`read_soon`], with how many patterns the tiling pattern lookups compared while reading.
fn read_counting_tilings(body: &str) -> (crate::Imported, u64) {
read_counting(body, NEVER_ENDS)
}
/// [`read_within`], with how many patterns the tiling pattern lookups compared while reading.
fn read_counting(body: &str, secs: u64) -> (crate::Imported, u64) {
let (tx, rx) = std::sync::mpsc::channel();
let ps = eps(body);
std::thread::spawn(move || tx.send(import(&ps)).ok());
rx.recv_timeout(std::time::Duration::from_secs(secs)).unwrap_or_else(|_| panic!("reading {body} did not finish")).unwrap()
std::thread::spawn(move || tx.send((import(&ps), tilings_compared())).ok());
let (read, compared) = rx.recv_timeout(std::time::Duration::from_secs(secs)).unwrap_or_else(|_| panic!("reading {body} did not finish"));
(read.unwrap(), compared)
}
/// Runs the program of `body` in an interpreter of its own on the 100 × 100 page of [`eps`]'s
/// bounding box, and returns what `look` finds in the interpreter when the program has ended. It
/// does not read the file's comments or make a document. The test fails when the program stops on
/// an error.
fn run_looking<T>(body: &str, look: fn(&Interp) -> T) -> T {
let file = eps(body);
let ps = super::program(&file).unwrap();
let mut doc = Document::new(100.0, 100.0);
doc.layers.clear();
let page = vectorcraft_geom::Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, 100.0]);
let mut it = Interp::new(ps, GState::default(), Out::new(doc, page, Rect::new(0.0, 0.0, 100.0, 100.0)));
assert!(it.run().is_ok(), "running {body} failed");
let found = look(&it);
it.release();
found
}
/// Reads `body` after a square: the square stays, and the program runs out of the operation