tytanic_core/
library.rs

1//! Standard library augmentation, i.e. additional functions and values for the
2//! typst standard library.
3//!
4//! # Functions
5//! ## `catch`
6//! Provides a mechanism to catch panics inside test scripts. Returns an array
7//! of strings for each panic.
8//! ```typst
9//! #let (msg,) = catch(() => {
10//!   panic()
11//! })
12//! ```
13//!
14//! ## `assert-panic`
15//! Provides an assertion that tests if a given closure panicked, panicking if
16//! it did not. Takes an optional `message` similar to other `assert` functions.
17//! ```typst
18//! #assert-panic(() => {}, message: "Did not panic")
19//! ```
20
21use ecow::EcoString;
22use typst::Library;
23use typst::LibraryBuilder;
24use typst::LibraryExt;
25use typst::comemo::Tracked;
26use typst::diag::SourceResult;
27use typst::diag::bail;
28use typst::engine::Engine;
29use typst::foundations::Context;
30use typst::foundations::Func;
31use typst::foundations::Module;
32use typst::foundations::Repr;
33use typst::foundations::Scope;
34use typst::foundations::Str;
35use typst::foundations::Value;
36use typst::foundations::func;
37
38/// Defines prelude items for the given scope, this is a subset of
39/// [`define_test_module`].
40pub fn define_prelude(scope: &mut Scope) {
41    scope.define_func::<catch>();
42    scope.define_func::<assert_panic>();
43}
44
45/// Defines test module items for the given scope.
46pub fn define_test_module(scope: &mut Scope) {
47    define_prelude(scope)
48}
49
50/// Creates a new test module with the items defined by [`define_test_module`].
51pub fn test_module() -> Module {
52    let mut scope = Scope::new();
53    define_test_module(&mut scope);
54    Module::new("test", scope)
55}
56
57/// Creates a new augmented default standard library. See [`augmented_library`].
58pub fn augmented_default_library() -> Library {
59    augmented_library(|x| x)
60}
61
62/// Creates a new augmented standard library, applying the given closure to the
63/// builder.
64///
65/// The augmented standard library contains a new test module and a few items in
66/// the prelude for easier testing.
67pub fn augmented_library(builder: impl FnOnce(LibraryBuilder) -> LibraryBuilder) -> Library {
68    let mut lib = builder(Library::builder()).build();
69    let scope = lib.global.scope_mut();
70
71    scope.define("test", test_module());
72    define_prelude(scope);
73
74    lib
75}
76
77#[func]
78fn catch(engine: &mut Engine, context: Tracked<Context>, func: Func) -> Value {
79    func.call::<[Value; 0]>(engine, context, [])
80        .map(|_| Value::None)
81        .unwrap_or_else(|errors| {
82            Value::Str(Str::from(
83                errors
84                    .first()
85                    .expect("should contain at least one diagnostic")
86                    .message
87                    .clone(),
88            ))
89        })
90}
91
92#[func]
93fn assert_panic(
94    engine: &mut Engine,
95    context: Tracked<Context>,
96    func: Func,
97    #[named] message: Option<EcoString>,
98) -> SourceResult<()> {
99    let result = func.call::<[Value; 0]>(engine, context, []);
100    let span = func.span();
101    if let Ok(val) = result {
102        match message {
103            Some(message) => bail!(span, "{}", message),
104            None => match val {
105                Value::None => bail!(span, "Expected panic, closure returned successfully"),
106                _ => bail!(
107                    span,
108                    "Expected panic, closure returned successfully with {}",
109                    val.repr(),
110                ),
111            },
112        }
113    }
114
115    Ok(())
116}
117
118#[cfg(test)]
119mod tests {
120    use typst::syntax::Source;
121
122    use super::*;
123    use crate::doc::compile;
124    use crate::doc::compile::Warnings;
125    use crate::world_builder::file::VirtualFileProvider;
126    use crate::world_builder::library::LibraryProvider;
127    use crate::world_builder::test_utils;
128
129    #[test]
130    fn test_catch() {
131        let mut files = VirtualFileProvider::new();
132        let library = LibraryProvider::with_library(augmented_default_library());
133
134        let source = Source::detached(
135            r#"
136            #let errors = catch(() => {
137                panic()
138            })
139            #assert.eq(errors, "panicked")
140        "#,
141        );
142
143        let world = test_utils::virtual_world(source, &mut files, &library);
144
145        compile::compile(&world, Warnings::Emit).output.unwrap();
146    }
147
148    #[test]
149    fn test_assert_panic() {
150        let mut files = VirtualFileProvider::new();
151        let library = LibraryProvider::with_library(augmented_default_library());
152
153        let source = Source::detached(
154            r#"
155            #assert-panic(() => {
156                panic()
157            })
158        "#,
159        );
160
161        let world = test_utils::virtual_world(source, &mut files, &library);
162
163        compile::compile(&world, Warnings::Emit).output.unwrap();
164    }
165}