From ec60795575c29ee7fc2ea7507cfcc38958fe67bf Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 3 Feb 2020 15:05:34 +0100 Subject: =?UTF-8?q?Port=20tests=20=F0=9F=9A=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/coma.typ | 26 +++++ tests/layouter/coma.typ | 26 ----- tests/layouter/stack.typ | 71 ----------- tests/parser/tokens.rs | 77 ------------ tests/parser/trees.rs | 46 -------- tests/src/layouter.rs | 191 ------------------------------ tests/src/parser.rs | 297 ----------------------------------------------- tests/src/render.py | 7 +- tests/src/spanless.rs | 64 ---------- tests/src/typeset.rs | 196 +++++++++++++++++++++++++++++++ tests/stack.typ | 71 +++++++++++ 11 files changed, 295 insertions(+), 777 deletions(-) create mode 100644 tests/coma.typ delete mode 100644 tests/layouter/coma.typ delete mode 100644 tests/layouter/stack.typ delete mode 100644 tests/parser/tokens.rs delete mode 100644 tests/parser/trees.rs delete mode 100644 tests/src/layouter.rs delete mode 100644 tests/src/parser.rs delete mode 100644 tests/src/spanless.rs create mode 100644 tests/src/typeset.rs create mode 100644 tests/stack.typ (limited to 'tests') diff --git a/tests/coma.typ b/tests/coma.typ new file mode 100644 index 00000000..14f639b5 --- /dev/null +++ b/tests/coma.typ @@ -0,0 +1,26 @@ +[page.size: width=450pt, height=300pt] +[page.margins: 1cm] + +[box][ + *Technische Universität Berlin* [n] + *Fakultät II, Institut for Mathematik* [n] + Sekretariat MA [n] + Dr. Max Mustermann [n] + Ola Nordmann, John Doe +] +[align: right][*WiSe 2019/2020* [n] Woche 3] + +[v: 6mm] + +[align: center][ + *3. Ubungsblatt Computerorientierte Mathematik II* [v: 2mm] + *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) [v: 2mm] + *Alle Antworten sind zu beweisen.* +] + +*1. Aufgabe* [align: right][(1 + 1 + 2 Punkte)] + +Ein _Binärbaum_ ist ein Wurzelbaum, in dem jeder Knoten ≤ 2 Kinder hat. +Die Tiefe eines Knotens _v_ ist die Länge des eindeutigen Weges von der Wurzel +zu _v_, und die Höhe von _v_ ist die Länge eines längsten (absteigenden) Weges +von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel. diff --git a/tests/layouter/coma.typ b/tests/layouter/coma.typ deleted file mode 100644 index 14f639b5..00000000 --- a/tests/layouter/coma.typ +++ /dev/null @@ -1,26 +0,0 @@ -[page.size: width=450pt, height=300pt] -[page.margins: 1cm] - -[box][ - *Technische Universität Berlin* [n] - *Fakultät II, Institut for Mathematik* [n] - Sekretariat MA [n] - Dr. Max Mustermann [n] - Ola Nordmann, John Doe -] -[align: right][*WiSe 2019/2020* [n] Woche 3] - -[v: 6mm] - -[align: center][ - *3. Ubungsblatt Computerorientierte Mathematik II* [v: 2mm] - *Abgabe: 03.05.2019* (bis 10:10 Uhr in MA 001) [v: 2mm] - *Alle Antworten sind zu beweisen.* -] - -*1. Aufgabe* [align: right][(1 + 1 + 2 Punkte)] - -Ein _Binärbaum_ ist ein Wurzelbaum, in dem jeder Knoten ≤ 2 Kinder hat. -Die Tiefe eines Knotens _v_ ist die Länge des eindeutigen Weges von der Wurzel -zu _v_, und die Höhe von _v_ ist die Länge eines längsten (absteigenden) Weges -von _v_ zu einem Blatt. Die Höhe des Baumes ist die Höhe der Wurzel. diff --git a/tests/layouter/stack.typ b/tests/layouter/stack.typ deleted file mode 100644 index cbca41dc..00000000 --- a/tests/layouter/stack.typ +++ /dev/null @@ -1,71 +0,0 @@ -[page.size: w=5cm, h=5cm] -[page.margins: 0cm] - -// Test 1 -[box: w=1, h=1, debug=false][ - [box][ - [align: center] - [box: ps=3cm, ss=1cm] - [direction: p=ttb, s=ltr] - [box: ps=3cm, ss=1cm] - [box: ps=1cm, ss=1cm] - [box: ps=2cm, ss=1cm] - [box: ps=1cm, ss=1cm] - ] -] - -// Test 2 -[box: w=1, h=1, debug=false][ - [align: s=top] Top - [align: s=center] Center - [align: s=bottom] Bottom - [direction: p=ttb, s=ltr] - [align: p=bottom, s=origin] - [box: w=1cm, h=1cm] -] - -// Test 3 -[box: w=1, h=1, debug=false][ - [align: center][ - Some-long-spaceless-word! - [align: left] Some - [align: right] word! - ] -] - -// Test 4 -[box: w=1, h=1, debug=false][ - [direction: p=ltr, s=ttb] - [align: center] - [align: s=origin] [box: ps=1cm, ss=1cm] - [align: s=center] [box: ps=3cm, ss=1cm] [box: ps=4cm, ss=0.5cm] - [align: s=end] [box: ps=2cm, ss=1cm] -] - -// Test 5 -[box: w=1, h=1, debug=false][ - [direction: p=btt, s=ltr] - [align: p=center, s=left] - [box: h=2cm, w=1cm] - - [direction: p=rtl, s=btt] - [align: center] - [align: v=origin] ORIGIN - [align: v=center] CENTER - [align: v=end] END -] - -// Test 6 -[box: w=1, h=1, debug=false][ - [box: w=4cm, h=1cm] - - [align: p=right, s=center] CENTER - - [direction: p=btt, s=rtl] - [align: p=center, s=origin] - [box: w=0.5cm, h=0.5cm] - [box: w=0.5cm, h=1cm] - [box: w=0.5cm, h=0.5cm] - - [align: p=origin, s=end] END -] diff --git a/tests/parser/tokens.rs b/tests/parser/tokens.rs deleted file mode 100644 index 66e44ac5..00000000 --- a/tests/parser/tokens.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Whitespace. -t "" => [] -t " " => [S(0)] -t " " => [S(0)] -t "\t" => [S(0)] -t " \t" => [S(0)] -t "\n" => [S(1)] -t "\n " => [S(1)] -t " \n" => [S(1)] -t " \n " => [S(1)] -t " \n\t \n " => [S(2)] -t "\r\n" => [S(1)] -t " \r\r\n \x0D" => [S(3)] -t "\n\r" => [S(2)] - -// Comments. -t "a // bc\n " => [T("a"), S(0), LC(" bc"), S(1)] -t "a //a//b\n " => [T("a"), S(0), LC("a//b"), S(1)] -t "a //a//b\r\n" => [T("a"), S(0), LC("a//b"), S(1)] -t "a //a//b\n\nhello" => [T("a"), S(0), LC("a//b"), S(2), T("hello")] -t "/**/" => [BC("")] -t "_/*_/*a*/*/" => [Underscore, BC("_/*a*/")] -t "/*/*/" => [BC("/*/")] -t "abc*/" => [T("abc"), Invalid("*/")] - -// Header only tokens. -th "[" => [Func("", None, false)] -th "]" => [Invalid("]")] -th "(){}:=," => [LP, RP, LB, RB, Colon, Equals, Comma] -th "a:b" => [Id("a"), Colon, Id("b")] -th "=" => [Equals] -th "," => [Comma] -th r#""hello\"world""# => [Str(r#"hello\"world"#)] -th r#""hi", 12pt"# => [Str("hi"), Comma, S(0), Size(12.0)] -th "\"hi\"" => [T("\"hi"), T("\"")] -th "a: true, x=1" => [Id("a"), Colon, S(0), Bool(true), Comma, S(0), - Id("x"), Equals, Num(1.0)] -th "120%" => [Num(1.2)] -th "🌓, 🌍," => [T("🌓"), Comma, S(0), T("🌍"), Comma] -tb "a: b" => [T("a"), T(":"), S(0), T("b")] -tb "c=d, " => [T("c"), T("=d"), T(","), S(0)] - -// Body only tokens. -tb "_*`" => [Underscore, Star, Backtick] -tb "[func]*bold*" => [Func("func", None, true), Star, T("bold"), Star] -tb "hi_you_ there" => [T("hi"), Underscore, T("you"), Underscore, S(0), T("there")] -th "_*`" => [Invalid("_"), Invalid("*"), Invalid("`")] - -// Nested functions. -tb "[f: [=][*]]" => [Func("f: [=][*]", None, true)] -tb "[_][[,],]," => [Func("_", Some("[,],"), true), T(",")] -tb "[=][=][=]" => [Func("=", Some("="), true), Func("=", None, true)] -tb "[=][[=][=][=]]" => [Func("=", Some("[=][=][=]")), true] - -// Escapes. -tb r"\[" => [T("[")] -tb r"\]" => [T("]")] -tb r"\\" => [T(r"\")] -tb r"\/" => [T("/")] -tb r"\*" => [T("*")] -tb r"\_" => [T("_")] -tb r"\`" => [T("`")] - -// Unescapable special symbols. -th r"\:" => [T(r"\"), T(":")] -th r"\=" => [T(r"\"), T("=")] -th r"\:" => [T(r"\"), Colon] -th r"\=" => [T(r"\"), Equals] -th r"\," => [T(r"\"), Comma] - -// Spans. -tbs "hello" => [(0:0, 0:5, T("hello"))] -tbs "ab\r\nc" => [(0:0, 0:2, T("ab")), (0:2, 1:0, S(1)), (1:0, 1:1, T("c"))] -tbs "[x = \"(1)\"]*" => [(0:0, 0:11, Func("x = \"(1)\"", None, true)), (0:11, 0:12, Star)] -tbs "// ab\r\n\nf" => [(0:0, 0:5, LC(" ab")), (0:5, 2:0, S(2)), (2:0, 2:1, T("f"))] -tbs "/*b*/_" => [(0:0, 0:5, BC("b")), (0:5, 0:6, Underscore)] -ths "a=10" => [(0:0, 0:1, Id("a")), (0:1, 0:2, Equals), (0:2, 0:4, Num(10.0))] diff --git a/tests/parser/trees.rs b/tests/parser/trees.rs deleted file mode 100644 index d761fe24..00000000 --- a/tests/parser/trees.rs +++ /dev/null @@ -1,46 +0,0 @@ -// Basics. -p "" => [] -p "hi" => [T("hi")] -p "hi you" => [T("hi"), S, T("you")] -p "❤\n\n 🌍" => [T("❤"), N, T("🌍")] - -// Functions. -p "[func]" => [func!("func"; None)] -p "[tree][hi *you*]" => [func!("tree"; Some([T("hi"), S, B, T("you"), B]))] -p "from [align: left] to" => [ - T("from"), S, func!("align", pos: [ID("left")]; None), S, T("to"), -] -p "[f: left, 12pt, false]" => [ - func!("f", pos: [ID("left"), SIZE(Size::pt(12.0)), BOOL(false)]; None) -] -p "[f: , hi, * \"du\"]" => [func!("f", pos: [ID("hi"), STR("du")]; None)] -p "[box: x=1.2pt, false][a b c] bye" => [ - func!( - "box", - pos: [BOOL(false)], - key: ["x" => SIZE(Size::pt(1.2))]; - Some([T("a"), S, T("b"), S, T("c")]) - ), - S, T("bye"), -] - -// Errors. -e "[f: , hi, * \"du\"]" => [ - (0:4, 0:5, "expected value, found comma"), - (0:10, 0:11, "expected value, found invalid identifier"), -] -e "[f:, , ,]" => [ - (0:3, 0:4, "expected value, found comma"), - (0:5, 0:6, "expected value, found comma"), - (0:7, 0:8, "expected value, found comma"), -] -e "[f:" => [(0:3, 0:3, "expected closing bracket")] -e "[f: hi" => [(0:6, 0:6, "expected closing bracket")] -e "[f: hey 12pt]" => [(0:7, 0:7, "expected comma")] -e "[box: x=, false y=z=4" => [ - (0:8, 0:9, "expected value, found comma"), - (0:15, 0:15, "expected comma"), - (0:19, 0:19, "expected comma"), - (0:19, 0:20, "expected value, found equals sign"), - (0:21, 0:21, "expected closing bracket"), -] diff --git a/tests/src/layouter.rs b/tests/src/layouter.rs deleted file mode 100644 index 23110fe7..00000000 --- a/tests/src/layouter.rs +++ /dev/null @@ -1,191 +0,0 @@ -use std::collections::HashMap; -use std::error::Error; -use std::ffi::OsStr; -use std::fs::{File, create_dir_all, read_dir, read_to_string}; -use std::io::{BufWriter, Write}; -use std::panic; -use std::process::Command; - -use futures_executor::block_on; - -use typstc::{Typesetter, DynErrorProvider}; -use typstc::layout::{MultiLayout, Serialize}; -use typstc::size::{Size, Size2D, ValueBox}; -use typstc::style::{PageStyle, PaperClass}; -use typstc::export::pdf; -use typstc::toddle::query::fs::EagerFsProvider; - - -type DynResult = Result>; - -fn main() -> DynResult<()> { - let opts = Options::parse(); - - create_dir_all("tests/cache/serial")?; - create_dir_all("tests/cache/render")?; - create_dir_all("tests/cache/pdf")?; - - let tests: Vec<_> = read_dir("tests/layouter/")?.collect(); - let mut filtered = Vec::new(); - - for entry in tests { - let path = entry?.path(); - if path.extension() != Some(OsStr::new("typ")) { - continue; - } - - let name = path - .file_stem().ok_or("expected file stem")? - .to_string_lossy() - .to_string(); - - if opts.matches(&name) { - let src = read_to_string(&path)?; - filtered.push((name, src)); - } - } - - let len = filtered.len(); - println!(); - println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); - - for (name, src) in filtered { - panic::catch_unwind(|| { - if let Err(e) = test(&name, &src) { - println!("error: {:?}", e); - } - }).ok(); - } - - println!(); - - Ok(()) -} - -/// Create a _PDF_ and render with a name from the source code. -fn test(name: &str, src: &str) -> DynResult<()> { - println!("Testing: {}.", name); - - let (fs, entries) = EagerFsProvider::from_index("../fonts", "index.json")?; - let paths = fs.paths(); - let provider = DynErrorProvider::new(fs); - let mut typesetter = Typesetter::new((Box::new(provider), entries)); - - typesetter.set_page_style(PageStyle { - class: PaperClass::Custom, - dimensions: Size2D::with_all(Size::pt(250.0)), - margins: ValueBox::with_all(None), - }); - - let layouts = compile(&typesetter, src); - - // Compute the font's paths. - let mut fonts = HashMap::new(); - let loader = typesetter.loader().borrow(); - for layout in &layouts { - for index in layout.find_used_fonts() { - fonts.entry(index) - .or_insert_with(|| &paths[index.id][index.variant]); - } - } - drop(loader); - - // Write the serialized layout file. - let path = format!("tests/cache/serial/{}", name); - let mut file = BufWriter::new(File::create(path)?); - - // Write the font mapping into the serialization file. - writeln!(file, "{}", fonts.len())?; - for (index, path) in fonts.iter() { - writeln!(file, "{} {} {}", index.id, index.variant, path)?; - } - layouts.serialize(&mut file)?; - - // Render the layout into a PNG. - Command::new("python") - .arg("tests/src/render.py") - .arg(name) - .spawn() - .expect("failed to run python renderer"); - - // Write the PDF file. - let path = format!("tests/cache/pdf/{}.pdf", name); - let file = BufWriter::new(File::create(path)?); - pdf::export(&layouts, typesetter.loader(), file)?; - - Ok(()) -} - -/// Compile the source code with the typesetter. -fn compile(typesetter: &Typesetter, src: &str) -> MultiLayout { - #![allow(unused_variables)] - use std::time::Instant; - - // Warmup. - #[cfg(not(debug_assertions))] - let warmup = { - let warmup_start = Instant::now(); - block_on(typesetter.typeset(&src)); - Instant::now() - warmup_start - }; - - let start = Instant::now(); - let parsed = typesetter.parse(&src); - let parse = Instant::now() - start; - - if !parsed.errors.is_empty() { - println!("parse errors: {:#?}", parsed.errors); - } - - let start_layout = Instant::now(); - let layouted = block_on(typesetter.layout(&parsed.output)); - let layout = Instant::now() - start_layout; - let total = Instant::now() - start; - - if !layouted.errors.is_empty() { - println!("layout errors: {:#?}", layouted.errors); - } - - #[cfg(not(debug_assertions))] { - println!(" - cold start: {:?}", warmup); - println!(" - warmed up: {:?}", total); - println!(" - parsing: {:?}", parse); - println!(" - layouting: {:?}", layout); - println!(); - } - - layouted.output -} - -/// Command line options. -struct Options { - filter: Vec, - perfect: bool, -} - -impl Options { - /// Parse the options from the environment arguments. - fn parse() -> Options { - let mut perfect = false; - let mut filter = Vec::new(); - - for arg in std::env::args().skip(1) { - match arg.as_str() { - "--nocapture" => {}, - "=" => perfect = true, - _ => filter.push(arg), - } - } - - Options { filter, perfect } - } - - /// Whether a given test should be executed. - fn matches(&self, name: &str) -> bool { - match self.perfect { - true => self.filter.iter().any(|p| name == p), - false => self.filter.is_empty() - || self.filter.iter().any(|p| name.contains(p)) - } - } -} diff --git a/tests/src/parser.rs b/tests/src/parser.rs deleted file mode 100644 index 550090a8..00000000 --- a/tests/src/parser.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::fmt::Debug; - -use typstc::func::Scope; -use typstc::size::Size; -use typstc::syntax::*; -use typstc::{function, parse}; - -mod spanless; -use spanless::SpanlessEq; - - -/// The result of a single test case. -enum Case { - Okay, - Failed { - line: usize, - src: &'static str, - expected: String, - found: String, - } -} - -/// Test all tests. -fn test(tests: Vec<(&str, Vec)>) { - println!(); - - let mut errors = false; - - let len = tests.len(); - println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); - - for (file, cases) in tests { - print!("Testing: {}. ", file); - - let mut okay = 0; - let mut failed = 0; - - for case in cases { - match case { - Case::Okay => okay += 1, - Case::Failed { line, src, expected, found } => { - println!(); - println!(" ❌ Case failed in file {}.rs in line {}.", file, line); - println!(" - Source: {:?}", src); - println!(" - Expected: {}", expected); - println!(" - Found: {}", found); - - failed += 1; - } - } - } - - // Print a small summary. - print!("{} okay, {} failed.", okay, failed); - if failed == 0 { - print!(" ✔") - } else { - errors = true; - } - - println!(); - } - - println!(); - - if errors { - std::process::exit(-1); - } -} - -/// The main test macro. -macro_rules! tokens { - ($($task:ident $src:expr =>($line:expr)=> [$($e:tt)*])*) => ({ - vec![$({ - let (okay, expected, found) = case!($task $src, [$($e)*]); - if okay { - Case::Okay - } else { - Case::Failed { - line: $line, - src: $src, - expected: format(expected), - found: format(found), - } - } - }),*] - }); -} - -//// Indented formatting for failed cases. -fn format(thing: impl Debug) -> String { - format!("{:#?}", thing).replace('\n', "\n ") -} - -/// Evaluates a single test. -macro_rules! case { - (t $($rest:tt)*) => (case!(@tokenize SpanlessEq::spanless_eq, $($rest)*)); - (ts $($rest:tt)*) => (case!(@tokenize PartialEq::eq, $($rest)*)); - - (@tokenize $cmp:expr, $src:expr, [$($e:tt)*]) => ({ - let expected = list!(tokens [$($e)*]); - let found = tokenize($src).collect::>(); - ($cmp(&found, &expected), expected, found) - }); - - (p $($rest:tt)*) => (case!(@parse SpanlessEq::spanless_eq, $($rest)*)); - (ps $($rest:tt)*) => (case!(@parse PartialEq::eq, $($rest)*)); - - (@parse $cmp:expr, $src:expr, [$($e:tt)*]) => ({ - let expected = SyntaxModel { nodes: list!(nodes [$($e)*]) }; - let found = parse($src, ParseContext { scope: &scope() }).0; - ($cmp(&found, &expected), expected, found) - }); - - (c $src:expr, [$($e:tt)*]) => ({ - let expected = Colorization { tokens: list!(decorations [$($e)*]) }; - let found = parse($src, ParseContext { scope: &scope() }).1; - (expected == found, expected, found) - }); - - (e $src:expr, [$($e:tt)*]) => ({ - let expected = list!([$($e)*]).into_iter() - .map(|s| s.map(|m| m.to_string())) - .collect(); - - let found = parse($src, ParseContext { scope: &scope() }).2; - (expected == found, expected, found) - }); -} - -/// A scope containing the `DebugFn` as a fallback. -fn scope() -> Scope { - Scope::with_fallback::() -} - -/// Parses possibly-spanned lists of token or node expressions. -macro_rules! list { - (expr [$($item:expr),* $(,)?]) => ({ - #[allow(unused_imports)] - use cuts::expr::*; - Tuple { items: vec![$(zspan($item)),*] } - }); - - (expr [$($key:expr =>($_:expr)=> $value:expr),* $(,)?]) => ({ - #[allow(unused_imports)] - use cuts::expr::*; - Object { - pairs: vec![$(Pair { - key: zspan(Ident($key.to_string())), - value: zspan($value), - }),*] - } - }); - - ($cut:ident [$($e:tt)*]) => ({ - #[allow(unused_imports)] - use cuts::$cut::*; - list!([$($e)*]) - }); - - ([$(($sl:tt:$sc:tt, $el:tt:$ec:tt, $v:expr)),* $(,)?]) => ({ - vec![ - $(Spanned { v: $v, span: Span { - start: Position { line: $sl, column: $sc }, - end: Position { line: $el, column: $ec }, - }}),* - ] - }); - - ([$($e:tt)*]) => (vec![$($e)*].into_iter().map(zspan).collect::>()); -} - -/// Composes a function expression. -macro_rules! func { - ($name:expr $(,pos: [$($p:tt)*])? $(,key: [$($k:tt)*])?; $($b:tt)*) => ({ - #![allow(unused_mut, unused_assignments)] - - let mut positional = Tuple::new(); - let mut keyword = Object::new(); - - $(positional = list!(expr [$($p)*]);)? - $(keyword = list!(expr [$($k)*]);)? - - Node::Model(Box::new(DebugFn { - header: FuncHeader { - name: zspan(Ident($name.to_string())), - args: FuncArgs { - positional, - keyword, - }, - }, - body: func!(@body $($b)*), - })) - }); - - (@body Some($($b:tt)*)) => (Some(SyntaxModel{ nodes: list!(nodes $($b)*) })); - (@body None) => (None); -} - -function! { - /// Most functions in the tests are parsed into the debug function for easy - /// inspection of arguments and body. - #[derive(Debug, PartialEq)] - pub struct DebugFn { - header: FuncHeader, - body: Option, - } - - parse(header, body, ctx) { - let cloned = header.clone(); - header.args.clear(); - DebugFn { - header: cloned, - body: parse!(optional: body, ctx), - } - } - - layout() { vec![] } -} - -/// Span an element with a zero span. -fn zspan(v: T) -> Spanned { - Spanned { v, span: Span::ZERO } -} - -/// Abbreviations for tokens, nodes, colors and expressions. -#[allow(non_snake_case, dead_code)] -mod cuts { - pub mod tokens { - pub use typstc::syntax::Token::{ - Whitespace as W, - LineComment as LC, - BlockComment as BC, - StarSlash as SS, - LeftBracket as LB, - RightBracket as RB, - LeftParen as LP, - RightParen as RP, - LeftBrace as LBR, - RightBrace as RBR, - Colon as CL, - Comma as CM, - Equals as EQ, - ExprIdent as ID, - ExprStr as STR, - ExprSize as SIZE, - ExprNumber as NUM, - ExprBool as BOOL, - Star as S, - Underscore as U, - Backtick as B, - Text as T, - }; - } - - pub mod nodes { - use typstc::syntax::Node; - - pub use Node::{ - Space as S, - Newline as N, - ToggleItalic as I, - ToggleBolder as B, - ToggleMonospace as M, - }; - - pub fn T(text: &str) -> Node { - Node::Text(text.to_string()) - } - } - - pub mod decorations { - pub use typstc::syntax::Decoration::*; - } - - pub mod expr { - use typstc::syntax::{Expression, Ident}; - - pub use Expression::{ - Number as NUM, - Size as SIZE, - Bool as BOOL, - }; - - pub fn ID(text: &str) -> Expression { - Expression::Ident(Ident(text.to_string())) - } - - pub fn STR(text: &str) -> Expression { - Expression::Str(text.to_string()) - } - } -} - -fn main() { - test(include!("../cache/parser-tests.rs")) -} diff --git a/tests/src/render.py b/tests/src/render.py index d445e49c..ac94be20 100644 --- a/tests/src/render.py +++ b/tests/src/render.py @@ -8,15 +8,13 @@ from PIL import Image, ImageDraw, ImageFont BASE = os.path.dirname(__file__) CACHE = os.path.join(BASE, '../cache/') -SERIAL = os.path.join(CACHE, 'serial/') -RENDER = os.path.join(CACHE, 'render/') def main(): assert len(sys.argv) == 2, 'usage: python render.py ' name = sys.argv[1] - filename = os.path.join(SERIAL, name) + filename = os.path.join(CACHE, f'{name}.serialized') with open(filename, encoding='utf-8') as file: lines = [line[:-1] for line in file.readlines()] @@ -24,8 +22,7 @@ def main(): renderer.render() image = renderer.export() - pathlib.Path(RENDER).mkdir(parents=True, exist_ok=True) - image.save(os.path.join(RENDER, f'{name}.png')) + image.save(os.path.join(CACHE, f'{name}.png')) class MultiboxRenderer: diff --git a/tests/src/spanless.rs b/tests/src/spanless.rs deleted file mode 100644 index 87d3f39d..00000000 --- a/tests/src/spanless.rs +++ /dev/null @@ -1,64 +0,0 @@ -use super::*; - - -/// Compares elements by only looking at values and ignoring spans. -pub trait SpanlessEq { - fn spanless_eq(&self, other: &T) -> bool; -} - -impl SpanlessEq>>> for Vec>> { - fn spanless_eq(&self, other: &Vec>) -> bool { - self.len() == other.len() - && self.iter().zip(other).all(|(x, y)| x.v == y.v) - } -} - -impl SpanlessEq for SyntaxModel { - fn spanless_eq(&self, other: &SyntaxModel) -> bool { - fn downcast(func: &dyn Model) -> &DebugFn { - func.downcast::().expect("not a debug fn") - } - - self.nodes.len() == other.nodes.len() - && self.nodes.iter().zip(&other.nodes).all(|(x, y)| match (&x.v, &y.v) { - (Node::Model(a), Node::Model(b)) => { - downcast(a.as_ref()).spanless_eq(downcast(b.as_ref())) - } - (a, b) => a == b, - }) - } -} - -impl SpanlessEq for DebugFn { - fn spanless_eq(&self, other: &DebugFn) -> bool { - self.header.name.v == other.header.name.v - && self.header.args.positional.spanless_eq(&other.header.args.positional) - && self.header.args.keyword.spanless_eq(&other.header.args.keyword) - } -} - -impl SpanlessEq for Expression { - fn spanless_eq(&self, other: &Expression) -> bool { - match (self, other) { - (Expression::Tuple(a), Expression::Tuple(b)) => a.spanless_eq(b), - (Expression::Object(a), Expression::Object(b)) => a.spanless_eq(b), - (a, b) => a == b, - } - } -} - -impl SpanlessEq for Tuple { - fn spanless_eq(&self, other: &Tuple) -> bool { - self.items.len() == other.items.len() - && self.items.iter().zip(&other.items) - .all(|(x, y)| x.v.spanless_eq(&y.v)) - } -} - -impl SpanlessEq for Object { - fn spanless_eq(&self, other: &Object) -> bool { - self.pairs.len() == other.pairs.len() - && self.pairs.iter().zip(&other.pairs) - .all(|(x, y)| x.key.v == y.key.v && x.value.v.spanless_eq(&y.value.v)) - } -} diff --git a/tests/src/typeset.rs b/tests/src/typeset.rs new file mode 100644 index 00000000..6a5cdb9e --- /dev/null +++ b/tests/src/typeset.rs @@ -0,0 +1,196 @@ +use std::collections::HashMap; +use std::error::Error; +use std::ffi::OsStr; +use std::fs::{File, create_dir_all, read_dir, read_to_string}; +use std::io::{BufWriter, Write}; +use std::panic; +use std::process::Command; + +use futures_executor::block_on; + +use typstc::{Typesetter, DynErrorProvider}; +use typstc::layout::{MultiLayout, Serialize}; +use typstc::size::{Size, Size2D, ValueBox}; +use typstc::style::{PageStyle, PaperClass}; +use typstc::export::pdf; +use typstc::toddle::query::fs::EagerFsProvider; + + +type DynResult = Result>; + +fn main() -> DynResult<()> { + let opts = Options::parse(); + + create_dir_all("tests/cache")?; + + let tests: Vec<_> = read_dir("tests/")?.collect(); + let mut filtered = Vec::new(); + + for entry in tests { + let path = entry?.path(); + if path.extension() != Some(OsStr::new("typ")) { + continue; + } + + let name = path + .file_stem().ok_or("expected file stem")? + .to_string_lossy() + .to_string(); + + if opts.matches(&name) { + let src = read_to_string(&path)?; + filtered.push((name, src)); + } + } + + let len = filtered.len(); + println!(); + println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); + + for (name, src) in filtered { + panic::catch_unwind(|| { + if let Err(e) = test(&name, &src) { + println!("error: {:?}", e); + } + }).ok(); + } + + println!(); + + Ok(()) +} + +/// Create a _PDF_ and render with a name from the source code. +fn test(name: &str, src: &str) -> DynResult<()> { + println!("Testing: {}.", name); + + let (fs, entries) = EagerFsProvider::from_index("../fonts", "index.json")?; + let paths = fs.paths(); + let provider = DynErrorProvider::new(fs); + let mut typesetter = Typesetter::new((Box::new(provider), entries)); + + typesetter.set_page_style(PageStyle { + class: PaperClass::Custom, + dimensions: Size2D::with_all(Size::pt(250.0)), + margins: ValueBox::with_all(None), + }); + + let layouts = compile(&typesetter, src); + + // Write the PDF file. + let path = format!("tests/cache/{}.pdf", name); + let file = BufWriter::new(File::create(path)?); + pdf::export(&layouts, typesetter.loader(), file)?; + + // Compute the font's paths. + let mut fonts = HashMap::new(); + let loader = typesetter.loader().borrow(); + for layout in &layouts { + for index in layout.find_used_fonts() { + fonts.entry(index) + .or_insert_with(|| &paths[index.id][index.variant]); + } + } + drop(loader); + + // Write the serialized layout file. + let path = format!("tests/cache/{}.serialized", name); + let mut file = BufWriter::new(File::create(&path)?); + + // Write the font mapping into the serialization file. + writeln!(file, "{}", fonts.len())?; + for (index, path) in fonts.iter() { + writeln!(file, "{} {} {}", index.id, index.variant, path)?; + } + + layouts.serialize(&mut file)?; + file.flush()?; + drop(file); + + // Render the layout into a PNG. + Command::new("python") + .arg("tests/src/render.py") + .arg(name) + .spawn() + .expect("failed to run python renderer") + .wait() + .expect("command did not run"); + + std::fs::remove_file(path)?; + + Ok(()) +} + +/// Compile the source code with the typesetter. +fn compile(typesetter: &Typesetter, src: &str) -> MultiLayout { + #![allow(unused_variables)] + use std::time::Instant; + + // Warmup. + #[cfg(not(debug_assertions))] + let warmup = { + let warmup_start = Instant::now(); + block_on(typesetter.typeset(&src)); + Instant::now() - warmup_start + }; + + let start = Instant::now(); + let parsed = typesetter.parse(&src); + let parse = Instant::now() - start; + + if !parsed.errors.is_empty() { + println!("parse errors: {:#?}", parsed.errors); + } + + let start_layout = Instant::now(); + let layouted = block_on(typesetter.layout(&parsed.output)); + let layout = Instant::now() - start_layout; + let total = Instant::now() - start; + + if !layouted.errors.is_empty() { + println!("layout errors: {:#?}", layouted.errors); + } + + #[cfg(not(debug_assertions))] { + println!(" - cold start: {:?}", warmup); + println!(" - warmed up: {:?}", total); + println!(" - parsing: {:?}", parse); + println!(" - layouting: {:?}", layout); + println!(); + } + + layouted.output +} + +/// Command line options. +struct Options { + filter: Vec, + perfect: bool, +} + +impl Options { + /// Parse the options from the environment arguments. + fn parse() -> Options { + let mut perfect = false; + let mut filter = Vec::new(); + + for arg in std::env::args().skip(1) { + match arg.as_str() { + "--nocapture" => {}, + "=" => perfect = true, + _ => filter.push(arg), + } + } + + Options { filter, perfect } + } + + /// Whether a given test should be executed. + fn matches(&self, name: &str) -> bool { + match self.perfect { + true => self.filter.iter().any(|p| name == p), + false => self.filter.is_empty() + || self.filter.iter().any(|p| name.contains(p)) + } + } +} diff --git a/tests/stack.typ b/tests/stack.typ new file mode 100644 index 00000000..cbca41dc --- /dev/null +++ b/tests/stack.typ @@ -0,0 +1,71 @@ +[page.size: w=5cm, h=5cm] +[page.margins: 0cm] + +// Test 1 +[box: w=1, h=1, debug=false][ + [box][ + [align: center] + [box: ps=3cm, ss=1cm] + [direction: p=ttb, s=ltr] + [box: ps=3cm, ss=1cm] + [box: ps=1cm, ss=1cm] + [box: ps=2cm, ss=1cm] + [box: ps=1cm, ss=1cm] + ] +] + +// Test 2 +[box: w=1, h=1, debug=false][ + [align: s=top] Top + [align: s=center] Center + [align: s=bottom] Bottom + [direction: p=ttb, s=ltr] + [align: p=bottom, s=origin] + [box: w=1cm, h=1cm] +] + +// Test 3 +[box: w=1, h=1, debug=false][ + [align: center][ + Some-long-spaceless-word! + [align: left] Some + [align: right] word! + ] +] + +// Test 4 +[box: w=1, h=1, debug=false][ + [direction: p=ltr, s=ttb] + [align: center] + [align: s=origin] [box: ps=1cm, ss=1cm] + [align: s=center] [box: ps=3cm, ss=1cm] [box: ps=4cm, ss=0.5cm] + [align: s=end] [box: ps=2cm, ss=1cm] +] + +// Test 5 +[box: w=1, h=1, debug=false][ + [direction: p=btt, s=ltr] + [align: p=center, s=left] + [box: h=2cm, w=1cm] + + [direction: p=rtl, s=btt] + [align: center] + [align: v=origin] ORIGIN + [align: v=center] CENTER + [align: v=end] END +] + +// Test 6 +[box: w=1, h=1, debug=false][ + [box: w=4cm, h=1cm] + + [align: p=right, s=center] CENTER + + [direction: p=btt, s=rtl] + [align: p=center, s=origin] + [box: w=0.5cm, h=0.5cm] + [box: w=0.5cm, h=1cm] + [box: w=0.5cm, h=0.5cm] + + [align: p=origin, s=end] END +] -- cgit v1.2.3