typst/tests/typeset.rs

635 lines
18 KiB
Rust
Raw Normal View History

use std::env;
use std::ffi::OsStr;
2022-01-01 14:12:50 +03:00
use std::fs;
use std::ops::Range;
use std::path::Path;
2022-01-31 18:06:44 +03:00
use std::sync::Arc;
2021-07-08 23:33:44 +03:00
use tiny_skia as sk;
use walkdir::WalkDir;
use typst::diag::Error;
2021-12-30 14:12:19 +03:00
use typst::eval::{Smart, StyleMap, Value};
2022-01-24 18:48:24 +03:00
use typst::frame::{Element, Frame};
use typst::geom::{Length, RgbaColor};
use typst::library::{PageNode, TextNode};
2021-08-09 12:06:37 +03:00
use typst::loading::FsLoader;
use typst::parse::Scanner;
use typst::source::SourceFile;
use typst::syntax::Span;
2021-07-20 21:21:56 +03:00
use typst::Context;
2022-01-01 14:12:50 +03:00
#[cfg(feature = "layout-cache")]
use {
filedescriptor::{FileDescriptor, StdioDescriptor::*},
std::fs::File,
typst::layout::RootNode,
};
2021-02-20 19:53:40 +03:00
const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref";
const PNG_DIR: &str = "./png";
const PDF_DIR: &str = "./pdf";
const FONT_DIR: &str = "../fonts";
fn main() {
env::set_current_dir(env::current_dir().unwrap().join("tests")).unwrap();
let args = Args::new(env::args().skip(1));
let mut filtered = Vec::new();
2022-01-01 14:12:50 +03:00
// Since differents tests can affect each other through the layout cache, a
// deterministic order is very important for reproducibility.
for entry in WalkDir::new(".").sort_by_file_name() {
let entry = entry.unwrap();
if entry.depth() <= 1 {
continue;
}
let src_path = entry.into_path();
if src_path.extension() != Some(OsStr::new("typ")) {
continue;
}
if args.matches(&src_path) {
filtered.push(src_path);
}
}
let len = filtered.len();
2021-01-13 23:33:22 +03:00
if len == 1 {
println!("Running test ...");
2021-01-13 23:33:22 +03:00
} else if len > 1 {
println!("Running {len} tests");
}
// Set page width to 120pt with 10pt margins, so that the inner page is
// exactly 100pt wide. Page height is unbounded and font size is 10pt so
// that it multiplies to nice round numbers.
2021-12-30 14:12:19 +03:00
let mut styles = StyleMap::new();
styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0)));
styles.set(PageNode::HEIGHT, Smart::Auto);
styles.set(PageNode::LEFT, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::TOP, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::RIGHT, Smart::Custom(Length::pt(10.0).into()));
styles.set(PageNode::BOTTOM, Smart::Custom(Length::pt(10.0).into()));
2021-12-15 13:11:57 +03:00
styles.set(TextNode::SIZE, Length::pt(10.0).into());
2021-07-20 21:21:56 +03:00
// Hook up an assert function into the global scope.
2021-07-21 12:25:49 +03:00
let mut std = typst::library::new();
std.def_const("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
std.def_const("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
std.def_func("test", move |_, args| {
let lhs = args.expect::<Value>("left-hand side")?;
let rhs = args.expect::<Value>("right-hand side")?;
if lhs != rhs {
2021-08-12 14:39:33 +03:00
return Err(Error::boxed(
args.span,
2021-08-12 14:39:33 +03:00
format!("Assertion failed: {:?} != {:?}", lhs, rhs),
));
}
Ok(Value::None)
});
2021-07-21 12:25:49 +03:00
// Create loader and context.
let loader = FsLoader::new().with_path(FONT_DIR).wrap();
let mut ctx = Context::builder().std(std).styles(styles).build(loader);
2021-07-21 12:25:49 +03:00
// Run all the tests.
2021-12-15 13:11:57 +03:00
let mut ok = 0;
for src_path in filtered {
let path = src_path.strip_prefix(TYP_DIR).unwrap();
let png_path = Path::new(PNG_DIR).join(path).with_extension("png");
let ref_path = Path::new(REF_DIR).join(path).with_extension("png");
let pdf_path =
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
2021-12-15 13:11:57 +03:00
ok += test(
2021-07-20 21:21:56 +03:00
&mut ctx,
&src_path,
&png_path,
&ref_path,
pdf_path.as_deref(),
args.debug,
2021-12-15 13:11:57 +03:00
) as usize;
}
if len > 1 {
println!("{ok} / {len} tests passed.");
2021-01-13 16:07:38 +03:00
}
2021-12-15 13:11:57 +03:00
if ok < len {
std::process::exit(1);
}
}
struct Args {
filter: Vec<String>,
exact: bool,
debug: bool,
pdf: bool,
}
impl Args {
fn new(args: impl Iterator<Item = String>) -> Self {
let mut filter = Vec::new();
let mut exact = false;
let mut debug = false;
let mut pdf = false;
for arg in args {
match arg.as_str() {
// Ignore this, its for cargo.
"--nocapture" => {}
// Match only the exact filename.
"--exact" => exact = true,
// Generate PDFs.
"--pdf" => pdf = true,
// Debug print the layout trees.
"--debug" | "-d" => debug = true,
// Everything else is a file filter.
_ => filter.push(arg),
}
}
Self { filter, pdf, debug, exact }
}
fn matches(&self, path: &Path) -> bool {
if self.exact {
let name = path.file_name().unwrap().to_string_lossy();
self.filter.iter().any(|v| v == &name)
} else {
let path = path.to_string_lossy();
self.filter.is_empty() || self.filter.iter().any(|v| path.contains(v))
}
}
}
fn test(
2021-07-20 21:21:56 +03:00
ctx: &mut Context,
src_path: &Path,
png_path: &Path,
2021-02-20 19:53:40 +03:00
ref_path: &Path,
pdf_path: Option<&Path>,
debug: bool,
) -> bool {
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display());
let src = fs::read_to_string(src_path).unwrap();
2021-01-13 16:07:38 +03:00
let mut ok = true;
let mut frames = vec![];
let mut line = 0;
2021-02-01 00:43:11 +03:00
let mut compare_ref = true;
let mut compare_ever = false;
let mut rng = LinearShift::new();
2021-02-01 00:43:11 +03:00
2021-06-29 17:05:05 +03:00
let parts: Vec<_> = src.split("\n---").collect();
for (i, &part) in parts.iter().enumerate() {
2021-02-01 00:43:11 +03:00
let is_header = i == 0
&& parts.len() > 1
&& part
.lines()
.all(|s| s.starts_with("//") || s.chars().all(|c| c.is_whitespace()));
if is_header {
for line in part.lines() {
if line.starts_with("// Ref: false") {
compare_ref = false;
}
}
} else {
let (part_ok, compare_here, part_frames) = test_part(
ctx,
src_path,
part.into(),
i,
compare_ref,
line,
debug,
&mut rng,
);
2021-02-01 00:43:11 +03:00
ok &= part_ok;
compare_ever |= compare_here;
2021-02-01 00:43:11 +03:00
frames.extend(part_frames);
}
line += part.lines().count() + 1;
2021-01-13 16:07:38 +03:00
}
if compare_ever {
if let Some(pdf_path) = pdf_path {
2021-07-20 21:21:56 +03:00
let pdf_data = typst::export::pdf(ctx, &frames);
fs::create_dir_all(&pdf_path.parent().unwrap()).unwrap();
fs::write(pdf_path, pdf_data).unwrap();
}
2021-01-13 16:07:38 +03:00
2022-01-24 18:48:24 +03:00
let canvas = render(ctx, &frames);
fs::create_dir_all(&png_path.parent().unwrap()).unwrap();
2021-03-23 15:17:00 +03:00
canvas.save_png(png_path).unwrap();
2021-01-14 01:19:44 +03:00
2021-07-08 23:33:44 +03:00
if let Ok(ref_pixmap) = sk::Pixmap::load_png(ref_path) {
2021-03-23 15:17:00 +03:00
if canvas != ref_pixmap {
2021-02-20 19:53:40 +03:00
println!(" Does not match reference image. ❌");
2021-01-13 16:07:38 +03:00
ok = false;
}
} else if !frames.is_empty() {
2021-02-20 19:53:40 +03:00
println!(" Failed to open reference image. ❌");
ok = false;
2021-01-13 16:07:38 +03:00
}
}
if ok {
if !debug {
print!("\x1b[1A");
}
println!("Testing {}", name.display());
2021-01-13 16:07:38 +03:00
}
ok
}
2021-02-01 00:43:11 +03:00
fn test_part(
2021-07-20 21:21:56 +03:00
ctx: &mut Context,
2021-08-09 12:06:37 +03:00
src_path: &Path,
src: String,
2021-02-01 00:43:11 +03:00
i: usize,
compare_ref: bool,
line: usize,
debug: bool,
rng: &mut LinearShift,
2022-01-31 18:06:44 +03:00
) -> (bool, bool, Vec<Arc<Frame>>) {
let mut ok = true;
2021-08-09 12:06:37 +03:00
let id = ctx.sources.provide(src_path, src);
let source = ctx.sources.get(id);
2022-01-30 13:16:57 +03:00
if debug {
println!("Syntax: {:#?}", source.root())
}
2021-08-09 12:06:37 +03:00
let (local_compare_ref, mut ref_errors) = parse_metadata(&source);
2021-02-01 00:43:11 +03:00
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
ok &= test_reparse(ctx.sources.get(id).src(), i, rng);
let (frames, mut errors) = match ctx.evaluate(id) {
Ok(module) => {
let tree = module.into_root();
if debug {
2022-01-30 13:16:57 +03:00
println!("Layout: {tree:#?}");
}
let mut frames = tree.layout(ctx);
2021-06-18 14:00:36 +03:00
#[cfg(feature = "layout-cache")]
(ok &= test_incremental(ctx, i, &tree, &frames));
2021-06-27 19:06:39 +03:00
if !compare_ref {
frames.clear();
}
(frames, vec![])
}
Err(errors) => (vec![], *errors),
};
// TODO: Also handle errors from other files.
2021-08-13 13:21:14 +03:00
errors.retain(|error| error.span.source == id);
for error in &mut errors {
error.trace.clear();
}
2021-08-21 17:38:51 +03:00
// The comparison never fails since all spans are from the same source file.
ref_errors.sort_by(|a, b| a.span.partial_cmp(&b.span).unwrap());
errors.sort_by(|a, b| a.span.partial_cmp(&b.span).unwrap());
2021-07-21 12:25:49 +03:00
if errors != ref_errors {
println!(" Subtest {i} does not match expected errors. ❌");
ok = false;
2021-08-09 12:06:37 +03:00
let source = ctx.sources.get(id);
for error in errors.iter() {
2021-08-13 13:21:14 +03:00
if error.span.source == id && !ref_errors.contains(error) {
print!(" Not annotated | ");
print_error(&source, line, error);
}
}
for error in ref_errors.iter() {
if !errors.contains(error) {
print!(" Not emitted | ");
print_error(&source, line, error);
}
}
}
(ok, compare_ref, frames)
}
2022-01-24 18:48:24 +03:00
fn parse_metadata(source: &SourceFile) -> (Option<bool>, Vec<Error>) {
let mut compare_ref = None;
let mut errors = vec![];
2022-01-24 18:48:24 +03:00
let lines: Vec<_> = source.src().lines().map(str::trim).collect();
for (i, line) in lines.iter().enumerate() {
if line.starts_with("// Ref: false") {
compare_ref = Some(false);
}
2022-01-24 18:48:24 +03:00
if line.starts_with("// Ref: true") {
compare_ref = Some(true);
}
2022-01-24 18:48:24 +03:00
let rest = if let Some(rest) = line.strip_prefix("// Error: ") {
rest
} else {
continue;
};
2022-01-24 18:48:24 +03:00
fn num(s: &mut Scanner) -> usize {
s.eat_while(|c| c.is_numeric()).parse().unwrap()
}
2022-01-24 18:48:24 +03:00
let comments =
lines[i ..].iter().take_while(|line| line.starts_with("//")).count();
let pos = |s: &mut Scanner| -> usize {
let first = num(s) - 1;
let (delta, column) =
if s.eat_if(':') { (first, num(s) - 1) } else { (0, first) };
let line = (i + comments) + delta;
source.line_column_to_byte(line, column).unwrap()
};
let mut s = Scanner::new(rest);
let start = pos(&mut s);
let end = if s.eat_if('-') { pos(&mut s) } else { start };
let span = Span::new(source.id(), start, end);
errors.push(Error::new(span, s.rest().trim()));
2021-06-27 19:06:39 +03:00
}
2022-01-24 18:48:24 +03:00
(compare_ref, errors)
}
2022-01-24 18:48:24 +03:00
fn print_error(source: &SourceFile, line: usize, error: &Error) {
let start_line = 1 + line + source.byte_to_line(error.span.start).unwrap();
let start_col = 1 + source.byte_to_column(error.span.start).unwrap();
let end_line = 1 + line + source.byte_to_line(error.span.end).unwrap();
let end_col = 1 + source.byte_to_column(error.span.end).unwrap();
println!(
"Error: {start_line}:{start_col}-{end_line}:{end_col}: {}",
error.message,
);
}
/// Pseudorandomly edit the source file and test whether a reparse produces the
/// same result as a clean parse.
///
/// The method will first inject 10 strings once every 400 source characters
/// and then select 5 leaf node boundries to inject an additional, randomly
/// chosen string from the injection list.
fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
let supplements = [
"[",
2022-01-31 15:26:40 +03:00
"]",
"{",
"}",
"(",
")",
"#rect()",
"a word",
", a: 1",
"10.0",
":",
"if i == 0 {true}",
"for",
"* hello *",
"//",
"/*",
"\\u{12e4}",
"```typst",
" ",
"trees",
"\\",
"$ a $",
"2.",
"-",
"5",
];
let mut ok = true;
let apply = |replace: std::ops::Range<usize>, with| {
let mut incr_source = SourceFile::detached(src);
2022-01-02 16:46:08 +03:00
if incr_source.root().len() != src.len() {
println!(
" Subtest {i} tree length {} does not match string length {} ❌",
2022-01-02 16:46:08 +03:00
incr_source.root().len(),
src.len(),
);
return false;
}
incr_source.edit(replace.clone(), with);
2022-01-24 18:48:24 +03:00
let edited_src = incr_source.src();
let ref_source = SourceFile::detached(edited_src);
let incr_root = incr_source.root();
let ref_root = ref_source.root();
2022-01-24 18:48:24 +03:00
let same = incr_root == ref_root;
if !same {
println!(
" Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n",
replace.start, replace.end,
);
println!(" Expected reference tree:\n{ref_root:#?}\n");
println!(" Found incremental tree:\n{incr_root:#?}");
println!("Full source ({}):\n\"{edited_src:?}\"", edited_src.len());
}
2022-01-24 18:48:24 +03:00
same
};
let mut pick = |range: Range<usize>| {
let ratio = rng.next();
(range.start as f64 + ratio * (range.end - range.start) as f64).floor() as usize
};
let insertions = (src.len() as f64 / 400.0).ceil() as usize;
for _ in 0 .. insertions {
let supplement = supplements[pick(0 .. supplements.len())];
let start = pick(0 .. src.len());
let end = pick(start .. src.len());
if !src.is_char_boundary(start) || !src.is_char_boundary(end) {
continue;
}
ok &= apply(start .. end, supplement);
}
let red = SourceFile::detached(src).red();
let leafs = red.as_ref().leafs();
let leaf_start = leafs[pick(0 .. leafs.len())].span().start;
let supplement = supplements[pick(0 .. supplements.len())];
ok &= apply(leaf_start .. leaf_start, supplement);
ok
}
2022-01-24 18:48:24 +03:00
#[cfg(feature = "layout-cache")]
fn test_incremental(
ctx: &mut Context,
i: usize,
tree: &RootNode,
2022-01-31 18:06:44 +03:00
frames: &[Arc<Frame>],
2022-01-24 18:48:24 +03:00
) -> bool {
let mut ok = true;
2021-01-13 16:07:38 +03:00
2022-01-24 18:48:24 +03:00
let reference = ctx.layout_cache.clone();
for level in 0 .. reference.levels() {
ctx.layout_cache = reference.clone();
ctx.layout_cache.retain(|x| x == level);
if ctx.layout_cache.is_empty() {
continue;
}
2022-01-24 18:48:24 +03:00
ctx.layout_cache.turnaround();
2022-01-24 18:48:24 +03:00
let cached = silenced(|| tree.layout(ctx));
let total = reference.levels() - 1;
let misses = ctx
.layout_cache
.entries()
.filter(|e| e.level() == level && !e.hit() && e.age() == 2)
.count();
2021-01-13 16:07:38 +03:00
2022-01-24 18:48:24 +03:00
if misses > 0 {
println!(
" Subtest {i} relayout had {misses} cache misses on level {level} of {total} ❌",
);
ok = false;
}
2021-02-17 23:30:20 +03:00
2022-01-24 18:48:24 +03:00
if cached != frames {
println!(
" Subtest {i} relayout differs from clean pass on level {level} ❌",
);
ok = false;
}
}
2022-01-24 18:48:24 +03:00
ctx.layout_cache = reference;
ctx.layout_cache.turnaround();
2022-01-24 18:48:24 +03:00
ok
}
2022-01-24 18:48:24 +03:00
/// Draw all frames into one image with padding in between.
2022-01-31 18:06:44 +03:00
fn render(ctx: &mut Context, frames: &[Arc<Frame>]) -> sk::Pixmap {
2022-01-24 18:48:24 +03:00
let pixel_per_pt = 2.0;
let pixmaps: Vec<_> = frames
.iter()
.map(|frame| {
let limit = Length::cm(100.0);
if frame.size.x > limit || frame.size.y > limit {
panic!("overlarge frame: {:?}", frame.size);
}
typst::export::render(ctx, frame, pixel_per_pt)
})
.collect();
2020-08-30 23:18:55 +03:00
2022-01-24 18:48:24 +03:00
let pad = (5.0 * pixel_per_pt).round() as u32;
let pxw = 2 * pad + pixmaps.iter().map(sk::Pixmap::width).max().unwrap_or_default();
let pxh = pad + pixmaps.iter().map(|pixmap| pixmap.height() + pad).sum::<u32>();
2021-01-14 01:19:44 +03:00
2021-11-16 23:32:29 +03:00
let mut canvas = sk::Pixmap::new(pxw, pxh).unwrap();
2021-07-08 23:33:44 +03:00
canvas.fill(sk::Color::BLACK);
2022-01-24 18:48:24 +03:00
let [x, mut y] = [pad; 2];
for (frame, mut pixmap) in frames.iter().zip(pixmaps) {
let ts = sk::Transform::from_scale(pixel_per_pt, pixel_per_pt);
render_links(&mut pixmap, ts, ctx, frame);
canvas.draw_pixmap(
x as i32,
y as i32,
pixmap.as_ref(),
&sk::PixmapPaint::default(),
sk::Transform::identity(),
None,
);
y += pixmap.height() + pad;
}
2020-11-25 20:46:47 +03:00
canvas
}
2022-01-24 18:48:24 +03:00
/// Draw extra boxes for links so we can see whether they are there.
fn render_links(
2021-11-16 23:32:29 +03:00
canvas: &mut sk::Pixmap,
ts: sk::Transform,
ctx: &Context,
frame: &Frame,
) {
for (pos, element) in &frame.elements {
2022-01-24 18:48:24 +03:00
let ts = ts.pre_translate(pos.x.to_pt() as f32, pos.y.to_pt() as f32);
2021-11-16 23:32:29 +03:00
match *element {
2021-11-22 17:26:56 +03:00
Element::Group(ref group) => {
2022-01-24 18:48:24 +03:00
let ts = ts.pre_concat(group.transform.into());
render_links(canvas, ts, ctx, &group.frame);
2021-03-23 17:32:36 +03:00
}
2022-01-24 18:48:24 +03:00
Element::Link(_, size) => {
let w = size.x.to_pt() as f32;
let h = size.y.to_pt() as f32;
let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap();
let mut paint = sk::Paint::default();
paint.set_color_rgba8(40, 54, 99, 40);
canvas.fill_rect(rect, &paint, ts, None);
}
2022-01-24 18:48:24 +03:00
_ => {}
2021-03-23 17:32:36 +03:00
}
2021-11-20 17:51:07 +03:00
}
}
/// Disable stdout and stderr during execution of `f`.
2022-01-01 14:12:50 +03:00
#[cfg(feature = "layout-cache")]
fn silenced<F, T>(f: F) -> T
where
F: FnOnce() -> T,
{
let path = if cfg!(windows) { "NUL" } else { "/dev/null" };
let null = File::create(path).unwrap();
let stderr = FileDescriptor::redirect_stdio(&null, Stderr).unwrap();
let stdout = FileDescriptor::redirect_stdio(&null, Stdout).unwrap();
let result = f();
FileDescriptor::redirect_stdio(&stderr, Stderr).unwrap();
FileDescriptor::redirect_stdio(&stdout, Stdout).unwrap();
result
}
/// This is an Linear-feedback shift register using XOR as its shifting
/// function. It can be used as PRNG.
struct LinearShift(u64);
impl LinearShift {
/// Initialize the shift register with a pre-set seed.
pub fn new() -> Self {
Self(0xACE5)
}
/// Return a pseudo-random number between `0.0` and `1.0`.
pub fn next(&mut self) -> f64 {
self.0 ^= self.0 >> 3;
self.0 ^= self.0 << 14;
self.0 ^= self.0 >> 28;
self.0 ^= self.0 << 36;
self.0 ^= self.0 >> 52;
self.0 as f64 / u64::MAX as f64
}
}