mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 08:29:14 +00:00
EPS tests: the reader's long tests check counts and shared state instead of a 20 s limit
many_tiling_patterns_and_spot_inks_read_in_linear_time checked that a program setting 200,000 different tiling patterns and 200,001 spot inks reads within 20 s of wall-clock time. It reads in about 4 s alone; while the crate's full test suite ran in parallel it took more than 20 s and failed. In test builds, the tiling pattern lookup now counts on its thread how many patterns it compared with the one it looked for (tilings_compared, gated with cfg(test) like SELECTOR_TESTS in the SVG reader's CSS). The test, renamed many_tiling_patterns_are_found_again_through_a_hash, reads through import on a thread of its own and asserts that its 200,000 lookups compared at most 200,000 patterns in all. When every pattern hashes alike, as when each lookup went through every pattern made so far, the lookups compare 19,999,900,000 and the test fails. The test still reads the 200,001 spot inks and checks their swatches, but it does not count or time their lookups. The other tests that read through read_soon wait 300 s instead of 20 s, as a guard against a read that never ends; their budgets and limits decide what they check. work_on_the_longest_dash_pattern_ends_soon (4.5 s alone) now also checks that the states gsave, grestore, gstate, currentgstate and setgstate save, make or set hold the one 65,535-entry dash pattern, so a copy of the pattern in any of them fails it.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user