summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-09-20 13:05:55 +0200
committerLaurenz <laurmaedje@gmail.com>2022-09-20 16:37:15 +0200
commit757a701c1aa2a6fb80033c7e75666661818da6f9 (patch)
tree0415fec94d3856f4ebc97a1744cf2ba75fe8e7aa /tests
parente29f55bb294cc298daad97accf6d8a76976b409c (diff)
A New World
Diffstat (limited to 'tests')
-rw-r--r--tests/typ/coma.typ2
-rw-r--r--tests/typeset.rs275
2 files changed, 192 insertions, 85 deletions
diff --git a/tests/typ/coma.typ b/tests/typ/coma.typ
index e312fc78..0e228d14 100644
--- a/tests/typ/coma.typ
+++ b/tests/typ/coma.typ
@@ -21,4 +21,4 @@ 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.
-#align(center, image("/res/graph.png", width: 75%))
+#align(center, image("../res/graph.png", width: 75%))
diff --git a/tests/typeset.rs b/tests/typeset.rs
index 4b25e176..3ac353db 100644
--- a/tests/typeset.rs
+++ b/tests/typeset.rs
@@ -1,24 +1,31 @@
+use std::cell::RefCell;
+use std::collections::{hash_map::Entry, HashMap};
use std::env;
use std::ffi::OsStr;
-use std::fs;
+use std::fs::{self, File};
+use std::hash::Hash;
+use std::io;
use std::ops::Range;
-use std::path::Path;
-use std::sync::Arc;
+use std::path::{Path, PathBuf};
+use elsa::FrozenVec;
+use same_file::Handle;
+use siphasher::sip128::{Hasher128, SipHasher};
use tiny_skia as sk;
use unscanny::Scanner;
use walkdir::WalkDir;
use typst::eval::{Smart, Value};
+use typst::font::{Font, FontBook};
use typst::frame::{Element, Frame};
use typst::geom::{Length, RgbaColor, Sides};
use typst::library::layout::PageNode;
use typst::library::text::{TextNode, TextSize};
-use typst::loading::FsLoader;
use typst::model::StyleMap;
-use typst::source::Source;
+use typst::source::{Source, SourceId};
use typst::syntax::SyntaxNode;
-use typst::{bail, Config, Context};
+use typst::util::Buffer;
+use typst::{bail, Config, World};
const TYP_DIR: &str = "./typ";
const REF_DIR: &str = "./ref";
@@ -57,46 +64,8 @@ fn main() {
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.
- let mut styles = StyleMap::new();
- styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into()));
- styles.set(PageNode::HEIGHT, Smart::Auto);
- styles.set(
- PageNode::MARGINS,
- Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))),
- );
- styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into()));
-
- // Hook up helpers into the global scope.
- let mut std = typst::library::new();
- std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
- std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
- std.def_fn("test", move |_, args| {
- let lhs = args.expect::<Value>("left-hand side")?;
- let rhs = args.expect::<Value>("right-hand side")?;
- if lhs != rhs {
- bail!(args.span, "Assertion failed: {:?} != {:?}", lhs, rhs,);
- }
- Ok(Value::None)
- });
- std.def_fn("print", move |_, args| {
- print!("> ");
- for (i, value) in args.all::<Value>()?.into_iter().enumerate() {
- if i > 0 {
- print!(", ")
- }
- print!("{value:?}");
- }
- println!();
- Ok(Value::None)
- });
-
// Create loader and context.
- let loader = FsLoader::new().with_path(FONT_DIR);
- let config = Config::builder().std(std).styles(styles).build();
- let mut ctx = Context::new(Arc::new(loader), config);
+ let mut world = TestWorld::new(args.print);
// Run all the tests.
let mut ok = 0;
@@ -108,12 +77,11 @@ fn main() {
args.pdf.then(|| Path::new(PDF_DIR).join(path).with_extension("pdf"));
ok += test(
- &mut ctx,
+ &mut world,
&src_path,
&png_path,
&ref_path,
pdf_path.as_deref(),
- &args.print,
) as usize;
}
@@ -179,18 +147,166 @@ impl Args {
}
}
+struct TestWorld {
+ config: Config,
+ print: PrintConfig,
+ sources: FrozenVec<Box<Source>>,
+ nav: RefCell<HashMap<PathHash, SourceId>>,
+ book: FontBook,
+ fonts: Vec<Font>,
+ files: RefCell<HashMap<PathHash, Buffer>>,
+}
+
+impl TestWorld {
+ fn new(print: PrintConfig) -> Self {
+ // 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.
+ let mut styles = StyleMap::new();
+ styles.set(PageNode::WIDTH, Smart::Custom(Length::pt(120.0).into()));
+ styles.set(PageNode::HEIGHT, Smart::Auto);
+ styles.set(
+ PageNode::MARGINS,
+ Sides::splat(Some(Smart::Custom(Length::pt(10.0).into()))),
+ );
+ styles.set(TextNode::SIZE, TextSize(Length::pt(10.0).into()));
+
+ // Hook up helpers into the global scope.
+ let mut std = typst::library::new();
+ std.define("conifer", RgbaColor::new(0x9f, 0xEB, 0x52, 0xFF));
+ std.define("forest", RgbaColor::new(0x43, 0xA1, 0x27, 0xFF));
+ std.def_fn("test", move |_, args| {
+ let lhs = args.expect::<Value>("left-hand side")?;
+ let rhs = args.expect::<Value>("right-hand side")?;
+ if lhs != rhs {
+ bail!(args.span, "Assertion failed: {:?} != {:?}", lhs, rhs,);
+ }
+ Ok(Value::None)
+ });
+ std.def_fn("print", move |_, args| {
+ print!("> ");
+ for (i, value) in args.all::<Value>()?.into_iter().enumerate() {
+ if i > 0 {
+ print!(", ")
+ }
+ print!("{value:?}");
+ }
+ println!();
+ Ok(Value::None)
+ });
+
+ let mut fonts = vec![];
+ for entry in WalkDir::new(FONT_DIR)
+ .into_iter()
+ .filter_map(|e| e.ok())
+ .filter(|entry| entry.file_type().is_file())
+ {
+ let buffer: Buffer = fs::read(entry.path()).unwrap().into();
+ for index in 0 .. ttf_parser::fonts_in_collection(&buffer).unwrap_or(1) {
+ fonts.push(Font::new(buffer.clone(), index).unwrap())
+ }
+ }
+
+ Self {
+ config: Config { root: PathBuf::new(), std, styles },
+ print,
+ sources: FrozenVec::new(),
+ nav: RefCell::new(HashMap::new()),
+ book: FontBook::from_fonts(&fonts),
+ fonts,
+ files: RefCell::new(HashMap::new()),
+ }
+ }
+
+ fn provide(&mut self, path: &Path, text: String) -> SourceId {
+ let hash = PathHash::new(path).unwrap();
+ if let Some(&id) = self.nav.borrow().get(&hash) {
+ self.sources.as_mut()[id.into_raw() as usize].replace(text);
+ return id;
+ }
+
+ let id = SourceId::from_raw(self.sources.len() as u16);
+ let source = Source::new(id, path, text);
+ self.sources.push(Box::new(source));
+ self.nav.borrow_mut().insert(hash, id);
+ id
+ }
+}
+
+impl World for TestWorld {
+ fn config(&self) -> &Config {
+ &self.config
+ }
+
+ fn resolve(&self, path: &Path) -> io::Result<SourceId> {
+ let hash = PathHash::new(path)?;
+ if let Some(&id) = self.nav.borrow().get(&hash) {
+ return Ok(id);
+ }
+
+ let data = fs::read(path)?;
+ let text = String::from_utf8(data).map_err(|_| {
+ io::Error::new(io::ErrorKind::InvalidData, "file is not valid utf-8")
+ })?;
+
+ let id = SourceId::from_raw(self.sources.len() as u16);
+ let source = Source::new(id, path, text);
+ self.sources.push(Box::new(source));
+ self.nav.borrow_mut().insert(hash, id);
+
+ Ok(id)
+ }
+
+ fn source(&self, id: SourceId) -> &Source {
+ &self.sources[id.into_raw() as usize]
+ }
+
+ fn book(&self) -> &FontBook {
+ &self.book
+ }
+
+ fn font(&self, id: usize) -> io::Result<Font> {
+ Ok(self.fonts[id].clone())
+ }
+
+ fn file(&self, path: &Path) -> io::Result<Buffer> {
+ let hash = PathHash::new(path)?;
+ Ok(match self.files.borrow_mut().entry(hash) {
+ Entry::Occupied(entry) => entry.get().clone(),
+ Entry::Vacant(entry) => entry.insert(fs::read(path)?.into()).clone(),
+ })
+ }
+}
+
+/// A hash that is the same for all paths pointing to the same file.
+#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
+struct PathHash(u128);
+
+impl PathHash {
+ fn new(path: &Path) -> io::Result<Self> {
+ let file = File::open(path)?;
+ if file.metadata()?.is_file() {
+ let handle = Handle::from_file(file)?;
+ let mut state = SipHasher::new();
+ handle.hash(&mut state);
+ Ok(Self(state.finish128().as_u128()))
+ } else {
+ Err(io::ErrorKind::NotFound.into())
+ }
+ }
+}
+
fn test(
- ctx: &mut Context,
+ world: &mut TestWorld,
src_path: &Path,
png_path: &Path,
ref_path: &Path,
pdf_path: Option<&Path>,
- print: &PrintConfig,
) -> 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();
+ let text = fs::read_to_string(src_path).unwrap();
let mut ok = true;
let mut frames = vec![];
@@ -199,7 +315,7 @@ fn test(
let mut compare_ever = false;
let mut rng = LinearShift::new();
- let parts: Vec<_> = src.split("\n---").collect();
+ let parts: Vec<_> = text.split("\n---").collect();
for (i, &part) in parts.iter().enumerate() {
let is_header = i == 0
&& parts.len() > 1
@@ -214,16 +330,8 @@ fn test(
}
}
} else {
- let (part_ok, compare_here, part_frames) = test_part(
- ctx,
- src_path,
- part.into(),
- i,
- compare_ref,
- line,
- print,
- &mut rng,
- );
+ let (part_ok, compare_here, part_frames) =
+ test_part(world, src_path, part.into(), i, compare_ref, line, &mut rng);
ok &= part_ok;
compare_ever |= compare_here;
frames.extend(part_frames);
@@ -239,7 +347,7 @@ fn test(
fs::write(pdf_path, pdf_data).unwrap();
}
- if print.frames {
+ if world.print.frames {
for frame in &frames {
println!("Frame: {:#?}", frame);
}
@@ -268,7 +376,7 @@ fn test(
}
if ok {
- if *print == PrintConfig::default() {
+ if world.print == PrintConfig::default() {
print!("\x1b[1A");
}
println!("Testing {} ✔", name.display());
@@ -278,20 +386,19 @@ fn test(
}
fn test_part(
- ctx: &mut Context,
+ world: &mut TestWorld,
src_path: &Path,
- src: String,
+ text: String,
i: usize,
compare_ref: bool,
line: usize,
- print: &PrintConfig,
rng: &mut LinearShift,
) -> (bool, bool, Vec<Frame>) {
let mut ok = true;
- let id = ctx.sources.provide(src_path, src);
- let source = ctx.sources.get(id);
- if print.syntax {
+ let id = world.provide(src_path, text);
+ let source = world.source(id);
+ if world.print.syntax {
println!("Syntax Tree: {:#?}", source.root())
}
@@ -299,9 +406,9 @@ fn test_part(
let compare_ref = local_compare_ref.unwrap_or(compare_ref);
ok &= test_spans(source.root());
- ok &= test_reparse(ctx.sources.get(id).src(), i, rng);
+ ok &= test_reparse(world.source(id).text(), i, rng);
- let (mut frames, errors) = match typst::typeset(ctx, id) {
+ let (mut frames, errors) = match typst::typeset(world, id) {
Ok(frames) => (frames, vec![]),
Err(errors) => (vec![], *errors),
};
@@ -317,7 +424,7 @@ fn test_part(
.into_iter()
.filter(|error| error.span.source() == id)
.map(|error| {
- let range = ctx.sources.range(error.span);
+ let range = world.source(error.span.source()).range(error.span);
let msg = error.message.replace("\\", "/");
(range, msg)
})
@@ -330,7 +437,7 @@ fn test_part(
println!(" Subtest {i} does not match expected errors. ❌");
ok = false;
- let source = ctx.sources.get(id);
+ let source = world.source(id);
for error in errors.iter() {
if !ref_errors.contains(error) {
print!(" Not annotated | ");
@@ -353,7 +460,7 @@ fn parse_metadata(source: &Source) -> (Option<bool>, Vec<(Range<usize>, String)>
let mut compare_ref = None;
let mut errors = vec![];
- let lines: Vec<_> = source.src().lines().map(str::trim).collect();
+ let lines: Vec<_> = source.text().lines().map(str::trim).collect();
for (i, line) in lines.iter().enumerate() {
if line.starts_with("// Ref: false") {
compare_ref = Some(false);
@@ -409,7 +516,7 @@ fn print_error(source: &Source, line: usize, (range, message): &(Range<usize>, S
/// 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 {
+fn test_reparse(text: &str, i: usize, rng: &mut LinearShift) -> bool {
let supplements = [
"[",
"]",
@@ -441,19 +548,19 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
let mut ok = true;
let apply = |replace: std::ops::Range<usize>, with| {
- let mut incr_source = Source::detached(src);
- if incr_source.root().len() != src.len() {
+ let mut incr_source = Source::detached(text);
+ if incr_source.root().len() != text.len() {
println!(
" Subtest {i} tree length {} does not match string length {} ❌",
incr_source.root().len(),
- src.len(),
+ text.len(),
);
return false;
}
incr_source.edit(replace.clone(), with);
- let edited_src = incr_source.src();
+ let edited_src = incr_source.text();
let incr_root = incr_source.root();
let ref_source = Source::detached(edited_src);
let ref_root = ref_source.root();
@@ -481,20 +588,20 @@ fn test_reparse(src: &str, i: usize, rng: &mut LinearShift) -> bool {
(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;
+ let insertions = (text.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());
+ let start = pick(0 .. text.len());
+ let end = pick(start .. text.len());
- if !src.is_char_boundary(start) || !src.is_char_boundary(end) {
+ if !text.is_char_boundary(start) || !text.is_char_boundary(end) {
continue;
}
ok &= apply(start .. end, supplement);
}
- let source = Source::detached(src);
+ let source = Source::detached(text);
let leafs = source.root().leafs();
let start = source.range(leafs[pick(0 .. leafs.len())].span()).start;
let supplement = supplements[pick(0 .. supplements.len())];