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 comemo::Tracked;
22use ecow::EcoString;
23use typst::diag::{bail, SourceResult};
24use typst::engine::Engine;
25use typst::foundations::{func, Context, Func, Module, Repr, Scope, Str, Value};
26use typst::{Library, LibraryBuilder};
27
28/// Defines prelude items for the given scope, this is a subset of
29/// [`define_test_module`].
30pub fn define_prelude(scope: &mut Scope) {
31    scope.define_func::<catch>();
32    scope.define_func::<assert_panic>();
33}
34
35/// Defines test module items for the given scope.
36pub fn define_test_module(scope: &mut Scope) {
37    define_prelude(scope)
38}
39
40/// Creates a new test module with the items defined by [`define_test_module`].
41pub fn test_module() -> Module {
42    let mut scope = Scope::new();
43    define_test_module(&mut scope);
44    Module::new("test", scope)
45}
46
47/// Creates a new augmented default standard library. See [`augmented_library`].
48pub fn augmented_default_library() -> Library {
49    augmented_library(|x| x)
50}
51
52/// Creates a new augmented standard library, applying the given closure to the
53/// builder.
54///
55/// The augmented standard library contains a new test module and a few items in
56/// the prelude for easier testing.
57pub fn augmented_library(builder: impl FnOnce(LibraryBuilder) -> LibraryBuilder) -> Library {
58    let mut lib = builder(LibraryBuilder::default()).build();
59    let scope = lib.global.scope_mut();
60
61    scope.define("test", test_module());
62    define_prelude(scope);
63
64    lib
65}
66
67#[func]
68fn catch(engine: &mut Engine, context: Tracked<Context>, func: Func) -> Value {
69    func.call::<[Value; 0]>(engine, context, [])
70        .map(|_| Value::None)
71        .unwrap_or_else(|errors| {
72            Value::Str(Str::from(
73                errors
74                    .first()
75                    .expect("should contain at least one diagnostic")
76                    .message
77                    .clone(),
78            ))
79        })
80}
81
82#[func]
83fn assert_panic(
84    engine: &mut Engine,
85    context: Tracked<Context>,
86    func: Func,
87    #[named] message: Option<EcoString>,
88) -> SourceResult<()> {
89    let result = func.call::<[Value; 0]>(engine, context, []);
90    let span = func.span();
91    if let Ok(val) = result {
92        match message {
93            Some(message) => bail!(span, "{}", message),
94            None => match val {
95                Value::None => bail!(
96                    span,
97                    "Expected panic, closure returned successfully with {}",
98                    val.repr(),
99                ),
100                _ => bail!(span, "Expected panic, closure returned successfully"),
101            },
102        }
103    }
104
105    Ok(())
106}
107
108#[cfg(test)]
109mod tests {
110    use typst::syntax::Source;
111
112    use super::*;
113    use crate::_dev::VirtualWorld;
114    use crate::doc::compile::{self, Warnings};
115
116    #[test]
117    fn test_catch() {
118        let world = VirtualWorld::new(augmented_default_library());
119        let source = Source::detached(
120            r#"
121            #let errors = catch(() => {
122                panic()
123            })
124            #assert.eq(errors, "panicked")
125        "#,
126        );
127
128        compile::compile(source, &world, Warnings::Emit)
129            .output
130            .unwrap();
131    }
132
133    #[test]
134    fn test_assert_panic() {
135        let world = VirtualWorld::new(augmented_default_library());
136        let source = Source::detached(
137            r#"
138            #assert-panic(() => {
139                panic()
140            })
141        "#,
142        );
143
144        compile::compile(source, &world, Warnings::Emit)
145            .output
146            .unwrap();
147    }
148}