Parallelize tests (#900)

This commit is contained in:
Alex Saveau 2023-04-23 05:35:18 -07:00 committed by GitHub
parent 561ff979d5
commit b75cad2d3b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 154 additions and 95 deletions

9
Cargo.lock generated
View File

@ -1089,9 +1089,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.141" version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]] [[package]]
name = "libdeflate-sys" name = "libdeflate-sys"
@ -1704,9 +1704,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.12" version = "0.37.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "722529a737f5a942fdbac3a46cee213053196737c5eaa3386d52e85b786f2659" checksum = "f79bef90eb6d984c72722595b5b1348ab39275a5e5123faca6863bf07d75a4e0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno", "errno",
@ -2373,6 +2373,7 @@ dependencies = [
"iai", "iai",
"once_cell", "once_cell",
"oxipng", "oxipng",
"rayon",
"tiny-skia", "tiny-skia",
"ttf-parser 0.17.1", "ttf-parser 0.17.1",
"typst", "typst",

View File

@ -18,6 +18,7 @@ use crate::util::{PathExt, StrExt};
/// ///
/// All line and column indices start at zero, just like byte indices. Only for /// All line and column indices start at zero, just like byte indices. Only for
/// user-facing display, you should add 1 to them. /// user-facing display, you should add 1 to them.
#[derive(Clone)]
pub struct Source { pub struct Source {
id: SourceId, id: SourceId,
path: PathBuf, path: PathBuf,

View File

@ -13,6 +13,7 @@ elsa = "1.7"
iai = { git = "https://github.com/reknih/iai" } iai = { git = "https://github.com/reknih/iai" }
once_cell = "1" once_cell = "1"
oxipng = "8.0.0" oxipng = "8.0.0"
rayon = "1.7.0"
tiny-skia = "0.6.2" tiny-skia = "0.6.2"
ttf-parser = "0.17" ttf-parser = "0.17"
unscanny = "0.1" unscanny = "0.1"

View File

@ -3,15 +3,24 @@
use std::cell::{RefCell, RefMut}; use std::cell::{RefCell, RefMut};
use std::collections::HashMap; use std::collections::HashMap;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fmt::Write as FmtWrite;
use std::fs; use std::fs;
use std::io::Write;
use std::ops::Range; use std::ops::Range;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::{env, io};
use clap::Parser;
use comemo::{Prehashed, Track}; use comemo::{Prehashed, Track};
use elsa::FrozenVec; use elsa::FrozenVec;
use once_cell::unsync::OnceCell; use once_cell::unsync::OnceCell;
use oxipng::{InFile, Options, OutFile}; use oxipng::{InFile, Options, OutFile};
use rayon::iter::ParallelBridge;
use rayon::iter::ParallelIterator;
use tiny_skia as sk; use tiny_skia as sk;
use unscanny::Scanner;
use walkdir::WalkDir;
use typst::diag::{bail, FileError, FileResult}; use typst::diag::{bail, FileError, FileResult};
use typst::doc::{Document, Frame, FrameItem, Meta}; use typst::doc::{Document, Frame, FrameItem, Meta};
use typst::eval::{func, Library, Value}; use typst::eval::{func, Library, Value};
@ -22,8 +31,6 @@ use typst::util::{Buffer, PathExt};
use typst::World; use typst::World;
use typst_library::layout::PageElem; use typst_library::layout::PageElem;
use typst_library::text::{TextElem, TextSize}; use typst_library::text::{TextElem, TextSize};
use unscanny::Scanner;
use walkdir::WalkDir;
const TYP_DIR: &str = "typ"; const TYP_DIR: &str = "typ";
const REF_DIR: &str = "ref"; const REF_DIR: &str = "ref";
@ -32,8 +39,6 @@ const PDF_DIR: &str = "pdf";
const FONT_DIR: &str = "../assets/fonts"; const FONT_DIR: &str = "../assets/fonts";
const FILE_DIR: &str = "../assets/files"; const FILE_DIR: &str = "../assets/files";
use clap::Parser;
#[derive(Debug, Clone, Parser)] #[derive(Debug, Clone, Parser)]
#[clap(name = "typst-test", author)] #[clap(name = "typst-test", author)]
struct Args { struct Args {
@ -43,7 +48,7 @@ struct Args {
subtest: Option<usize>, subtest: Option<usize>,
#[arg(long)] #[arg(long)]
exact: bool, exact: bool,
#[arg(long)] #[arg(long, default_value_t = env::var_os("UPDATE_EXPECT").is_some())]
update: bool, update: bool,
#[arg(long)] #[arg(long)]
pdf: bool, pdf: bool,
@ -78,53 +83,49 @@ impl Args {
fn main() { fn main() {
let args = Args::parse(); let args = Args::parse();
let mut filtered = Vec::new();
// Since different tests can affect each other through the memoization // Create loader and context.
// cache, a deterministic order is important for reproducibility. let world = TestWorld::new(args.print);
for entry in WalkDir::new("typ").sort_by_file_name() {
println!("Running tests...");
let results = WalkDir::new("typ")
.into_iter()
.par_bridge()
.filter_map(|entry| {
let entry = entry.unwrap(); let entry = entry.unwrap();
if entry.depth() == 0 { if entry.depth() == 0 {
continue; return None;
} }
if entry.path().starts_with("typ/benches") { if entry.path().starts_with("typ/benches") {
continue; return None;
} }
let src_path = entry.into_path(); let src_path = entry.into_path();
if src_path.extension() != Some(OsStr::new("typ")) { if src_path.extension() != Some(OsStr::new("typ")) {
continue; return None;
} }
if args.matches(&src_path) { if args.matches(&src_path) {
filtered.push(src_path); Some(src_path)
} else {
None
} }
} })
.map_with(world, |world, src_path| {
let len = filtered.len();
if len == 1 {
println!("Running test ...");
} else if len > 1 {
println!("Running {len} tests");
}
// Create loader and context.
let mut world = TestWorld::new(args.print);
// Run all the tests.
let mut ok = 0;
for src_path in filtered {
let path = src_path.strip_prefix(TYP_DIR).unwrap(); let path = src_path.strip_prefix(TYP_DIR).unwrap();
let png_path = Path::new(PNG_DIR).join(path).with_extension("png"); 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 ref_path = Path::new(REF_DIR).join(path).with_extension("png");
let pdf_path = let pdf_path =
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf")); args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
ok += test(world, &src_path, &png_path, &ref_path, pdf_path.as_deref(), &args)
test(&mut world, &src_path, &png_path, &ref_path, pdf_path.as_deref(), &args) as usize
as usize; })
} .collect::<Vec<_>>();
let len = results.len();
let ok = results.iter().sum::<usize>();
if len > 1 { if len > 1 {
println!("{ok} / {len} tests passed."); println!("{ok} / {len} tests passed.");
} }
@ -158,14 +159,15 @@ fn library() -> Library {
/// Returns: /// Returns:
#[func] #[func]
fn print(#[variadic] values: Vec<Value>) -> Value { fn print(#[variadic] values: Vec<Value>) -> Value {
print!("> "); let mut stdout = io::stdout().lock();
write!(stdout, "> ").unwrap();
for (i, value) in values.into_iter().enumerate() { for (i, value) in values.into_iter().enumerate() {
if i > 0 { if i > 0 {
print!(", ") write!(stdout, ", ").unwrap();
} }
print!("{value:?}"); write!(stdout, "{value:?}").unwrap();
} }
println!(); writeln!(stdout).unwrap();
Value::None Value::None
} }
@ -206,7 +208,21 @@ struct TestWorld {
main: SourceId, main: SourceId,
} }
#[derive(Default)] impl Clone for TestWorld {
fn clone(&self) -> Self {
Self {
print: self.print,
library: self.library.clone(),
book: self.book.clone(),
fonts: self.fonts.clone(),
paths: self.paths.clone(),
sources: FrozenVec::from_iter(self.sources.iter().cloned().map(Box::new)),
main: self.main,
}
}
}
#[derive(Default, Clone)]
struct PathSlot { struct PathSlot {
source: OnceCell<FileResult<SourceId>>, source: OnceCell<FileResult<SourceId>>,
buffer: OnceCell<FileResult<Buffer>>, buffer: OnceCell<FileResult<Buffer>>,
@ -222,7 +238,7 @@ impl TestWorld {
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.filter(|entry| entry.file_type().is_file()) .filter(|entry| entry.file_type().is_file())
{ {
let data = std::fs::read(entry.path()).unwrap(); let data = fs::read(entry.path()).unwrap();
fonts.extend(Font::iter(data.into())); fonts.extend(Font::iter(data.into()));
} }
@ -337,10 +353,10 @@ fn test(
args: &Args, args: &Args,
) -> bool { ) -> bool {
let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path); let name = src_path.strip_prefix(TYP_DIR).unwrap_or(src_path);
println!("Testing {}", name.display());
let text = fs::read_to_string(src_path).unwrap(); let text = fs::read_to_string(src_path).unwrap();
let mut output = String::new();
let mut ok = true; let mut ok = true;
let mut updated = false; let mut updated = false;
let mut frames = vec![]; let mut frames = vec![];
@ -353,7 +369,7 @@ fn test(
for (i, &part) in parts.iter().enumerate() { for (i, &part) in parts.iter().enumerate() {
if let Some(x) = args.subtest { if let Some(x) = args.subtest {
if x != i { if x != i {
println!("skipped subtest {i}"); writeln!(output, " Skipped subtest {i}.").unwrap();
continue; continue;
} }
} }
@ -370,8 +386,16 @@ fn test(
} }
} }
} else { } else {
let (part_ok, compare_here, part_frames) = let (part_ok, compare_here, part_frames) = test_part(
test_part(world, src_path, part.into(), i, compare_ref, line, &mut rng); &mut output,
world,
src_path,
part.into(),
i,
compare_ref,
line,
&mut rng,
);
ok &= part_ok; ok &= part_ok;
compare_ever |= compare_here; compare_ever |= compare_here;
frames.extend(part_frames); frames.extend(part_frames);
@ -390,7 +414,7 @@ fn test(
if world.print.frames { if world.print.frames {
for frame in &document.pages { for frame in &document.pages {
println!("Frame:\n{:#?}\n", frame); writeln!(output, "{:#?}\n", frame).unwrap();
} }
} }
@ -411,7 +435,7 @@ fn test(
update_image(png_path, ref_path); update_image(png_path, ref_path);
updated = true; updated = true;
} else { } else {
println!(" Does not match reference image."); writeln!(output, " Does not match reference image.").unwrap();
ok = false; ok = false;
} }
} }
@ -420,24 +444,32 @@ fn test(
update_image(png_path, ref_path); update_image(png_path, ref_path);
updated = true; updated = true;
} else { } else {
println!(" Failed to open reference image."); writeln!(output, " Failed to open reference image.").unwrap();
ok = false; ok = false;
} }
} }
} }
if ok && !updated { {
if world.print == PrintConfig::default() { let mut stdout = io::stdout().lock();
print!("\x1b[1A"); stdout.write_all(name.to_string_lossy().as_bytes()).unwrap();
if ok {
writeln!(stdout, "").unwrap();
} else {
writeln!(stdout, "").unwrap();
}
if updated {
writeln!(stdout, " Updated reference image.").unwrap();
}
if !output.is_empty() {
stdout.write_all(output.as_bytes()).unwrap();
} }
println!("Testing {}", name.display());
} }
ok ok
} }
fn update_image(png_path: &Path, ref_path: &Path) { fn update_image(png_path: &Path, ref_path: &Path) {
println!(" Updated reference image. ✔");
oxipng::optimize( oxipng::optimize(
&InFile::Path(png_path.to_owned()), &InFile::Path(png_path.to_owned()),
&OutFile::Path(Some(ref_path.to_owned())), &OutFile::Path(Some(ref_path.to_owned())),
@ -446,7 +478,9 @@ fn update_image(png_path: &Path, ref_path: &Path) {
.unwrap(); .unwrap();
} }
#[allow(clippy::too_many_arguments)]
fn test_part( fn test_part(
output: &mut String,
world: &mut TestWorld, world: &mut TestWorld,
src_path: &Path, src_path: &Path,
text: String, text: String,
@ -460,14 +494,14 @@ fn test_part(
let id = world.set(src_path, text); let id = world.set(src_path, text);
let source = world.source(id); let source = world.source(id);
if world.print.syntax { if world.print.syntax {
println!("Syntax Tree:\n{:#?}\n", source.root()) writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
} }
let (local_compare_ref, mut ref_errors) = parse_metadata(source); let (local_compare_ref, mut ref_errors) = parse_metadata(source);
let compare_ref = local_compare_ref.unwrap_or(compare_ref); let compare_ref = local_compare_ref.unwrap_or(compare_ref);
ok &= test_spans(source.root()); ok &= test_spans(output, source.root());
ok &= test_reparse(world.source(id).text(), i, rng); ok &= test_reparse(output, world.source(id).text(), i, rng);
if world.print.model { if world.print.model {
let world = (world as &dyn World).track(); let world = (world as &dyn World).track();
@ -475,7 +509,7 @@ fn test_part(
let mut tracer = typst::eval::Tracer::default(); let mut tracer = typst::eval::Tracer::default();
let module = let module =
typst::eval::eval(world, route.track(), tracer.track_mut(), source).unwrap(); typst::eval::eval(world, route.track(), tracer.track_mut(), source).unwrap();
println!("Model:\n{:#?}\n", module.content()); writeln!(output, "Model:\n{:#?}\n", module.content()).unwrap();
} }
let (mut frames, errors) = match typst::compile(world) { let (mut frames, errors) = match typst::compile(world) {
@ -500,21 +534,21 @@ fn test_part(
ref_errors.sort_by_key(|error| error.0.start); ref_errors.sort_by_key(|error| error.0.start);
if errors != ref_errors { if errors != ref_errors {
println!(" Subtest {i} does not match expected errors. ❌"); writeln!(output, " Subtest {i} does not match expected errors.").unwrap();
ok = false; ok = false;
let source = world.source(id); let source = world.source(id);
for error in errors.iter() { for error in errors.iter() {
if !ref_errors.contains(error) { if !ref_errors.contains(error) {
print!(" Not annotated | "); write!(output, " Not annotated | ").unwrap();
print_error(source, line, error); print_error(output, source, line, error);
} }
} }
for error in ref_errors.iter() { for error in ref_errors.iter() {
if !errors.contains(error) { if !errors.contains(error) {
print!(" Not emitted | "); write!(output, " Not emitted | ").unwrap();
print_error(source, line, error); print_error(output, source, line, error);
} }
} }
} }
@ -551,7 +585,7 @@ fn parse_metadata(source: &Source) -> (Option<bool>, Vec<(Range<usize>, String)>
source.line_column_to_byte(line, column).unwrap() source.line_column_to_byte(line, column).unwrap()
}; };
let Some(rest) = line.strip_prefix("// Error: ") else { continue }; let Some(rest) = line.strip_prefix("// Error: ") else { continue; };
let mut s = Scanner::new(rest); let mut s = Scanner::new(rest);
let start = pos(&mut s); let start = pos(&mut s);
let end = if s.eat_if('-') { pos(&mut s) } else { start }; let end = if s.eat_if('-') { pos(&mut s) } else { start };
@ -563,12 +597,18 @@ fn parse_metadata(source: &Source) -> (Option<bool>, Vec<(Range<usize>, String)>
(compare_ref, errors) (compare_ref, errors)
} }
fn print_error(source: &Source, line: usize, (range, message): &(Range<usize>, String)) { fn print_error(
output: &mut String,
source: &Source,
line: usize,
(range, message): &(Range<usize>, String),
) {
let start_line = 1 + line + source.byte_to_line(range.start).unwrap(); let start_line = 1 + line + source.byte_to_line(range.start).unwrap();
let start_col = 1 + source.byte_to_column(range.start).unwrap(); let start_col = 1 + source.byte_to_column(range.start).unwrap();
let end_line = 1 + line + source.byte_to_line(range.end).unwrap(); let end_line = 1 + line + source.byte_to_line(range.end).unwrap();
let end_col = 1 + source.byte_to_column(range.end).unwrap(); let end_col = 1 + source.byte_to_column(range.end).unwrap();
println!("Error: {start_line}:{start_col}-{end_line}:{end_col}: {message}"); writeln!(output, "Error: {start_line}:{start_col}-{end_line}:{end_col}: {message}")
.unwrap();
} }
/// Pseudorandomly edit the source file and test whether a reparse produces the /// Pseudorandomly edit the source file and test whether a reparse produces the
@ -577,7 +617,12 @@ fn print_error(source: &Source, line: usize, (range, message): &(Range<usize>, S
/// The method will first inject 10 strings once every 400 source characters /// The method will first inject 10 strings once every 400 source characters
/// and then select 5 leaf node boundaries to inject an additional, randomly /// and then select 5 leaf node boundaries to inject an additional, randomly
/// chosen string from the injection list. /// chosen string from the injection list.
fn test_reparse(text: &str, i: usize, rng: &mut LinearShift) -> bool { fn test_reparse(
output: &mut String,
text: &str,
i: usize,
rng: &mut LinearShift,
) -> bool {
let supplements = [ let supplements = [
"[", "[",
"]", "]",
@ -608,7 +653,7 @@ fn test_reparse(text: &str, i: usize, rng: &mut LinearShift) -> bool {
let mut ok = true; let mut ok = true;
let apply = |replace: std::ops::Range<usize>, with| { let mut apply = |replace: Range<usize>, with| {
let mut incr_source = Source::detached(text); let mut incr_source = Source::detached(text);
if incr_source.root().len() != text.len() { if incr_source.root().len() != text.len() {
println!( println!(
@ -627,7 +672,7 @@ fn test_reparse(text: &str, i: usize, rng: &mut LinearShift) -> bool {
let mut incr_root = incr_source.root().clone(); let mut incr_root = incr_source.root().clone();
// Ensures that the span numbering invariants hold. // Ensures that the span numbering invariants hold.
let spans_ok = test_spans(&ref_root) && test_spans(&incr_root); let spans_ok = test_spans(output, &ref_root) && test_spans(output, &incr_root);
// Remove all spans so that the comparison works out. // Remove all spans so that the comparison works out.
let tree_ok = { let tree_ok = {
@ -637,13 +682,19 @@ fn test_reparse(text: &str, i: usize, rng: &mut LinearShift) -> bool {
}; };
if !tree_ok { if !tree_ok {
println!( writeln!(
output,
" Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n", " Subtest {i} reparse differs from clean parse when inserting '{with}' at {}-{} ❌\n",
replace.start, replace.end, replace.start, replace.end,
); ).unwrap();
println!(" Expected reference tree:\n{ref_root:#?}\n"); writeln!(output, " Expected reference tree:\n{ref_root:#?}\n").unwrap();
println!(" Found incremental tree:\n{incr_root:#?}"); writeln!(output, " Found incremental tree:\n{incr_root:#?}").unwrap();
println!(" Full source ({}):\n\"{edited_src:?}\"", edited_src.len()); writeln!(
output,
" Full source ({}):\n\"{edited_src:?}\"",
edited_src.len()
)
.unwrap();
} }
spans_ok && tree_ok spans_ok && tree_ok
@ -687,22 +738,27 @@ fn leafs(node: &SyntaxNode) -> Vec<SyntaxNode> {
/// Ensure that all spans are properly ordered (and therefore unique). /// Ensure that all spans are properly ordered (and therefore unique).
#[track_caller] #[track_caller]
fn test_spans(root: &SyntaxNode) -> bool { fn test_spans(output: &mut String, root: &SyntaxNode) -> bool {
test_spans_impl(root, 0..u64::MAX) test_spans_impl(output, root, 0..u64::MAX)
} }
#[track_caller] #[track_caller]
fn test_spans_impl(node: &SyntaxNode, within: Range<u64>) -> bool { fn test_spans_impl(output: &mut String, node: &SyntaxNode, within: Range<u64>) -> bool {
if !within.contains(&node.span().number()) { if !within.contains(&node.span().number()) {
eprintln!(" Node: {node:#?}"); writeln!(output, " Node: {node:#?}").unwrap();
eprintln!(" Wrong span order: {} not in {within:?}", node.span().number(),); writeln!(
output,
" Wrong span order: {} not in {within:?} ❌",
node.span().number()
)
.unwrap();
} }
let start = node.span().number() + 1; let start = node.span().number() + 1;
let mut children = node.children().peekable(); let mut children = node.children().peekable();
while let Some(child) = children.next() { while let Some(child) = children.next() {
let end = children.peek().map_or(within.end, |next| next.span().number()); let end = children.peek().map_or(within.end, |next| next.span().number());
if !test_spans_impl(child, start..end) { if !test_spans_impl(output, child, start..end) {
return false; return false;
} }
} }