tytanic_core/doc/
compile.rs

1//! Test document compilation and diagnostics handling.
2
3use std::fmt::Debug;
4
5use ecow::{eco_vec, EcoVec};
6use thiserror::Error;
7use typst::diag::{FileResult, Severity, SourceDiagnostic, Warned};
8use typst::foundations::{Bytes, Datetime};
9use typst::layout::PagedDocument;
10use typst::syntax::{FileId, Source};
11use typst::text::{Font, FontBook};
12use typst::utils::LazyHash;
13use typst::{Library, World};
14use tytanic_utils::fmt::Term;
15
16/// How to handle warnings during compilation.
17#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Warnings {
19    /// Ignore all warnings.
20    Ignore,
21
22    /// Emit all warnings.
23    #[default]
24    Emit,
25
26    /// Promote all warnings to errors.
27    Promote,
28}
29
30/// An error which may occur during compilation. This struct only exists to
31/// implement [`Error`][trait@std::error::Error].
32#[derive(Debug, Clone, Error)]
33#[error("compilation failed with {} {}", .0.len(), Term::simple("error").with(.0.len()))]
34pub struct Error(pub EcoVec<SourceDiagnostic>);
35
36/// Compiles a source with the given global world.
37pub fn compile(
38    source: Source,
39    world: &dyn World,
40    warnings: Warnings,
41) -> Warned<Result<PagedDocument, Error>> {
42    struct TestWorldAdapter<'s, 'w> {
43        source: &'s Source,
44        global: &'w dyn World,
45    }
46
47    impl World for TestWorldAdapter<'_, '_> {
48        fn library(&self) -> &LazyHash<Library> {
49            self.global.library()
50        }
51
52        fn book(&self) -> &LazyHash<FontBook> {
53            self.global.book()
54        }
55
56        fn main(&self) -> FileId {
57            self.source.id()
58        }
59
60        fn source(&self, id: FileId) -> FileResult<Source> {
61            if id == self.source.id() {
62                Ok(self.source.clone())
63            } else {
64                self.global.source(id)
65            }
66        }
67
68        fn file(&self, id: FileId) -> FileResult<Bytes> {
69            self.global.file(id)
70        }
71
72        fn font(&self, index: usize) -> Option<Font> {
73            self.global.font(index)
74        }
75
76        fn today(&self, offset: Option<i64>) -> Option<Datetime> {
77            self.global.today(offset)
78        }
79    }
80
81    let Warned {
82        output,
83        warnings: mut emitted,
84    } = typst::compile(&TestWorldAdapter {
85        source: &source,
86        global: world,
87    });
88
89    match warnings {
90        Warnings::Ignore => Warned {
91            output: output.map_err(Error),
92            warnings: eco_vec![],
93        },
94        Warnings::Emit => Warned {
95            output: output.map_err(Error),
96            warnings: emitted,
97        },
98        Warnings::Promote => {
99            emitted = emitted
100                .into_iter()
101                .map(|mut warning| {
102                    warning.severity = Severity::Error;
103                    warning.with_hint("this warning was promoted to an error")
104                })
105                .collect();
106
107            match output {
108                Ok(doc) if emitted.is_empty() => Warned {
109                    output: Ok(doc),
110                    warnings: eco_vec![],
111                },
112                Ok(_) => Warned {
113                    output: Err(Error(emitted)),
114                    warnings: eco_vec![],
115                },
116                Err(errors) => {
117                    emitted.extend(errors);
118                    Warned {
119                        output: Err(Error(emitted)),
120                        warnings: eco_vec![],
121                    }
122                }
123            }
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use crate::_dev::VirtualWorld;
132
133    const TEST_PASS: &str = "Hello World";
134    const TEST_WARN: &str = "#set text(font: \"foo\"); Hello World";
135    const TEST_FAIL: &str = "#set text(font: \"foo\"); #panic()";
136
137    #[test]
138    fn test_compile_pass_ignore_warnings() {
139        let world = VirtualWorld::default();
140        let source = Source::detached(TEST_PASS);
141
142        let Warned { output, warnings } = compile(source, &world, Warnings::Ignore);
143        assert!(output.is_ok());
144        assert!(warnings.is_empty());
145    }
146
147    #[test]
148    fn test_compile_pass_emit_warnings() {
149        let world = VirtualWorld::default();
150        let source = Source::detached(TEST_PASS);
151
152        let Warned { output, warnings } = compile(source, &world, Warnings::Emit);
153        assert!(output.is_ok());
154        assert!(warnings.is_empty());
155    }
156
157    #[test]
158    fn test_compile_pass_promote_warnings() {
159        let world = VirtualWorld::default();
160        let source = Source::detached(TEST_PASS);
161
162        let Warned { output, warnings } = compile(source, &world, Warnings::Promote);
163        assert!(output.is_ok());
164        assert!(warnings.is_empty());
165    }
166
167    #[test]
168    fn test_compile_warn_ignore_warnings() {
169        let world = VirtualWorld::default();
170        let source = Source::detached(TEST_WARN);
171
172        let Warned { output, warnings } = compile(source, &world, Warnings::Ignore);
173        assert!(output.is_ok());
174        assert!(warnings.is_empty());
175    }
176
177    #[test]
178    fn test_compile_warn_emit_warnings() {
179        let world = VirtualWorld::default();
180        let source = Source::detached(TEST_WARN);
181
182        let Warned { output, warnings } = compile(source, &world, Warnings::Emit);
183        assert!(output.is_ok());
184        assert_eq!(warnings.len(), 1);
185    }
186
187    #[test]
188    fn test_compile_warn_promote_warnings() {
189        let world = VirtualWorld::default();
190        let source = Source::detached(TEST_WARN);
191
192        let Warned { output, warnings } = compile(source, &world, Warnings::Promote);
193        assert_eq!(output.unwrap_err().0.len(), 1);
194        assert!(warnings.is_empty());
195    }
196
197    #[test]
198    fn test_compile_fail_ignore_warnings() {
199        let world = VirtualWorld::default();
200        let source = Source::detached(TEST_FAIL);
201
202        let Warned { output, warnings } = compile(source, &world, Warnings::Ignore);
203        assert_eq!(output.unwrap_err().0.len(), 1);
204        assert!(warnings.is_empty());
205    }
206
207    #[test]
208    fn test_compile_fail_emit_warnings() {
209        let world = VirtualWorld::default();
210        let source = Source::detached(TEST_FAIL);
211
212        let Warned { output, warnings } = compile(source, &world, Warnings::Emit);
213        assert_eq!(output.unwrap_err().0.len(), 1);
214        assert_eq!(warnings.len(), 1);
215    }
216
217    #[test]
218    fn test_compile_fail_promote_warnings() {
219        let world = VirtualWorld::default();
220        let source = Source::detached(TEST_FAIL);
221
222        let Warned { output, warnings } = compile(source, &world, Warnings::Promote);
223        assert_eq!(output.unwrap_err().0.len(), 2);
224        assert!(warnings.is_empty());
225    }
226}