summaryrefslogtreecommitdiff
path: root/tests/src/world.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2024-04-13 10:39:45 +0200
committerGitHub <noreply@github.com>2024-04-13 08:39:45 +0000
commit020294fca9a7065d4b9cf4e677f606ebaaa29b00 (patch)
treec0027ad22046e2726c22298461327823d6b88d53 /tests/src/world.rs
parent72dd79210602ecc799726fc096b078afbb47f299 (diff)
Better test runner (#3922)
Diffstat (limited to 'tests/src/world.rs')
-rw-r--r--tests/src/world.rs229
1 files changed, 229 insertions, 0 deletions
diff --git a/tests/src/world.rs b/tests/src/world.rs
new file mode 100644
index 00000000..86ee8da6
--- /dev/null
+++ b/tests/src/world.rs
@@ -0,0 +1,229 @@
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::fs;
+use std::io::Write;
+use std::path::{Path, PathBuf};
+use std::sync::OnceLock;
+
+use comemo::Prehashed;
+use once_cell::sync::Lazy;
+use parking_lot::Mutex;
+use typst::diag::{bail, FileError, FileResult, StrResult};
+use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
+use typst::layout::{Abs, Margin, PageElem};
+use typst::syntax::{FileId, Source};
+use typst::text::{Font, FontBook, TextElem, TextSize};
+use typst::visualize::Color;
+use typst::{Library, World};
+
+/// A world that provides access to the tests environment.
+#[derive(Clone)]
+pub struct TestWorld {
+ main: Source,
+ base: &'static TestBase,
+}
+
+impl TestWorld {
+ /// Create a new world for a single test.
+ ///
+ /// This is cheap because the shared base for all test runs is lazily
+ /// initialized just once.
+ pub fn new(source: Source) -> Self {
+ static BASE: Lazy<TestBase> = Lazy::new(TestBase::default);
+ Self { main: source, base: &*BASE }
+ }
+}
+
+impl World for TestWorld {
+ fn library(&self) -> &Prehashed<Library> {
+ &self.base.library
+ }
+
+ fn book(&self) -> &Prehashed<FontBook> {
+ &self.base.book
+ }
+
+ fn main(&self) -> Source {
+ self.main.clone()
+ }
+
+ fn source(&self, id: FileId) -> FileResult<Source> {
+ if id == self.main.id() {
+ Ok(self.main.clone())
+ } else {
+ self.slot(id, FileSlot::source)
+ }
+ }
+
+ fn file(&self, id: FileId) -> FileResult<Bytes> {
+ self.slot(id, FileSlot::file)
+ }
+
+ fn font(&self, index: usize) -> Option<Font> {
+ Some(self.base.fonts[index].clone())
+ }
+
+ fn today(&self, _: Option<i64>) -> Option<Datetime> {
+ Some(Datetime::from_ymd(1970, 1, 1).unwrap())
+ }
+}
+
+impl TestWorld {
+ /// Access the canonical slot for the given file id.
+ fn slot<F, T>(&self, id: FileId, f: F) -> T
+ where
+ F: FnOnce(&mut FileSlot) -> T,
+ {
+ let mut map = self.base.slots.lock();
+ f(map.entry(id).or_insert_with(|| FileSlot::new(id)))
+ }
+}
+
+/// Shared foundation of all test worlds.
+struct TestBase {
+ library: Prehashed<Library>,
+ book: Prehashed<FontBook>,
+ fonts: Vec<Font>,
+ slots: Mutex<HashMap<FileId, FileSlot>>,
+}
+
+impl Default for TestBase {
+ fn default() -> Self {
+ let fonts: Vec<_> = typst_assets::fonts()
+ .chain(typst_dev_assets::fonts())
+ .flat_map(|data| Font::iter(Bytes::from_static(data)))
+ .collect();
+
+ Self {
+ library: Prehashed::new(library()),
+ book: Prehashed::new(FontBook::from_fonts(&fonts)),
+ fonts,
+ slots: Mutex::new(HashMap::new()),
+ }
+ }
+}
+
+/// Holds the processed data for a file ID.
+#[derive(Clone)]
+struct FileSlot {
+ id: FileId,
+ source: OnceLock<FileResult<Source>>,
+ file: OnceLock<FileResult<Bytes>>,
+}
+
+impl FileSlot {
+ /// Create a new file slot.
+ fn new(id: FileId) -> Self {
+ Self { id, file: OnceLock::new(), source: OnceLock::new() }
+ }
+
+ /// Retrieve the source for this file.
+ fn source(&mut self) -> FileResult<Source> {
+ self.source
+ .get_or_init(|| {
+ let buf = read(&system_path(self.id)?)?;
+ let text = String::from_utf8(buf.into_owned())?;
+ Ok(Source::new(self.id, text))
+ })
+ .clone()
+ }
+
+ /// Retrieve the file's bytes.
+ fn file(&mut self) -> FileResult<Bytes> {
+ self.file
+ .get_or_init(|| {
+ read(&system_path(self.id)?).map(|cow| match cow {
+ Cow::Owned(buf) => buf.into(),
+ Cow::Borrowed(buf) => Bytes::from_static(buf),
+ })
+ })
+ .clone()
+ }
+}
+
+/// The file system path for a file ID.
+fn system_path(id: FileId) -> FileResult<PathBuf> {
+ let root: PathBuf = match id.package() {
+ Some(spec) => format!("tests/packages/{}-{}", spec.name, spec.version).into(),
+ None => PathBuf::new(),
+ };
+
+ id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
+}
+
+/// Read a file.
+fn read(path: &Path) -> FileResult<Cow<'static, [u8]>> {
+ // Resolve asset.
+ if let Ok(suffix) = path.strip_prefix("assets/") {
+ return typst_dev_assets::get(&suffix.to_string_lossy())
+ .map(Cow::Borrowed)
+ .ok_or_else(|| FileError::NotFound(path.into()));
+ }
+
+ let f = |e| FileError::from_io(e, path);
+ if fs::metadata(path).map_err(f)?.is_dir() {
+ Err(FileError::IsDirectory)
+ } else {
+ fs::read(path).map(Cow::Owned).map_err(f)
+ }
+}
+
+/// The extended standard library for testing.
+fn library() -> Library {
+ // 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 lib = Library::default();
+
+ #[func]
+ fn test(lhs: Value, rhs: Value) -> StrResult<NoneValue> {
+ if lhs != rhs {
+ bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
+ }
+ Ok(NoneValue)
+ }
+
+ #[func]
+ fn test_repr(lhs: Value, rhs: Value) -> StrResult<NoneValue> {
+ if lhs.repr() != rhs.repr() {
+ bail!("Assertion failed: {} != {}", lhs.repr(), rhs.repr());
+ }
+ Ok(NoneValue)
+ }
+
+ #[func]
+ fn print(#[variadic] values: Vec<Value>) -> NoneValue {
+ let mut out = std::io::stdout().lock();
+ write!(out, "> ").unwrap();
+ for (i, value) in values.into_iter().enumerate() {
+ if i > 0 {
+ write!(out, ", ").unwrap();
+ }
+ write!(out, "{value:?}").unwrap();
+ }
+ writeln!(out).unwrap();
+ NoneValue
+ }
+
+ // Hook up helpers into the global scope.
+ lib.global.scope_mut().define_func::<test>();
+ lib.global.scope_mut().define_func::<test_repr>();
+ lib.global.scope_mut().define_func::<print>();
+ lib.global
+ .scope_mut()
+ .define("conifer", Color::from_u8(0x9f, 0xEB, 0x52, 0xFF));
+ lib.global
+ .scope_mut()
+ .define("forest", Color::from_u8(0x43, 0xA1, 0x27, 0xFF));
+
+ // Hook up default styles.
+ lib.styles
+ .set(PageElem::set_width(Smart::Custom(Abs::pt(120.0).into())));
+ lib.styles.set(PageElem::set_height(Smart::Auto));
+ lib.styles.set(PageElem::set_margin(Margin::splat(Some(Smart::Custom(
+ Abs::pt(10.0).into(),
+ )))));
+ lib.styles.set(TextElem::set_size(TextSize(Abs::pt(10.0).into())));
+
+ lib
+}