tinymist_world/
mock.rs

1//! Mock world support for Tinymist tests.
2//!
3//! This module intentionally lives in `tinymist-world` so world-level tests can
4//! use deterministic compiler worlds without depending on higher-level project
5//! crates. Enable the `mock` feature from downstream test-support crates when
6//! this module is needed as a dependency.
7
8use std::{
9    path::{Path, PathBuf},
10    sync::{Arc, LazyLock},
11};
12
13use tinymist_vfs::{
14    RootResolver, Vfs,
15    mock::{MockChange, MockPathAccess, MockWorkspace},
16};
17use typst::{
18    Features,
19    diag::FileResult,
20    foundations::{Bytes, Dict},
21    syntax::VirtualPath,
22    utils::LazyHash,
23};
24
25use crate::{
26    CompilerFeat, CompilerUniverse, CompilerWorld, EntryState,
27    font::{FontResolverImpl, memory::MemoryFontSearcher},
28    package::{RegistryPathMapper, registry::DummyRegistry},
29};
30
31/// A compiler feature set for mock-backed Tinymist worlds.
32#[derive(Debug, Clone, Copy)]
33pub struct MockCompilerFeat;
34
35impl CompilerFeat for MockCompilerFeat {
36    type FontResolver = FontResolverImpl;
37    type AccessModel = MockPathAccess;
38    type Registry = DummyRegistry;
39}
40
41/// A compiler universe backed by [`MockWorkspace`].
42pub type MockUniverse = CompilerUniverse<MockCompilerFeat>;
43
44/// A compiler world backed by [`MockWorkspace`].
45pub type MockWorld = CompilerWorld<MockCompilerFeat>;
46
47/// Extension helpers for using a VFS mock workspace at world level.
48pub trait MockWorkspaceWorldExt {
49    /// Creates an entry state rooted at this workspace.
50    fn entry_state(&self, entry: impl AsRef<Path>) -> FileResult<EntryState>;
51
52    /// Creates a world builder for this workspace and entry file.
53    fn world(&self, entry: impl Into<PathBuf>) -> MockWorldBuilder;
54}
55
56impl MockWorkspaceWorldExt for MockWorkspace {
57    fn entry_state(&self, entry: impl AsRef<Path>) -> FileResult<EntryState> {
58        Ok(EntryState::new_rooted(
59            self.root_path(),
60            Some(VirtualPath::new(
61                self.path(entry)
62                    .strip_prefix(self.root())
63                    .map_err(|_| typst::diag::FileError::AccessDenied)?,
64            )),
65        ))
66    }
67
68    fn world(&self, entry: impl Into<PathBuf>) -> MockWorldBuilder {
69        MockWorldBuilder::new(self.clone(), entry)
70    }
71}
72
73/// Applies VFS mock changes to world-level runtime structures.
74pub trait MockWorldChangeExt {
75    /// Applies this change to a compiler universe through the VFS revision path.
76    fn apply_to_universe<F>(&self, universe: &mut CompilerUniverse<F>)
77    where
78        F: CompilerFeat;
79}
80
81impl MockWorldChangeExt for MockChange {
82    fn apply_to_universe<F>(&self, universe: &mut CompilerUniverse<F>)
83    where
84        F: CompilerFeat,
85    {
86        universe.increment_revision(|universe| {
87            universe.vfs().notify_fs_changes(self.changeset().clone());
88        });
89    }
90}
91
92/// Builder for mock-backed compiler worlds.
93#[derive(Debug, Clone)]
94pub struct MockWorldBuilder {
95    workspace: MockWorkspace,
96    entry: PathBuf,
97    features: Features,
98    inputs: Option<Arc<LazyHash<Dict>>>,
99    font_resolver: Option<Arc<FontResolverImpl>>,
100    creation_timestamp: Option<i64>,
101}
102
103impl MockWorldBuilder {
104    /// Creates a mock world builder.
105    pub fn new(workspace: MockWorkspace, entry: impl Into<PathBuf>) -> Self {
106        Self {
107            workspace,
108            entry: entry.into(),
109            features: Features::default(),
110            inputs: None,
111            font_resolver: None,
112            creation_timestamp: None,
113        }
114    }
115
116    /// Sets the Typst feature flags for the universe.
117    pub fn with_features(mut self, features: Features) -> Self {
118        self.features = features;
119        self
120    }
121
122    /// Sets Typst input values for the universe.
123    pub fn with_inputs(mut self, inputs: Dict) -> Self {
124        self.inputs = Some(Arc::new(LazyHash::new(inputs)));
125        self
126    }
127
128    /// Sets pre-hashed Typst input values for the universe.
129    pub fn with_lazy_inputs(mut self, inputs: Arc<LazyHash<Dict>>) -> Self {
130        self.inputs = Some(inputs);
131        self
132    }
133
134    /// Sets the font resolver for the universe.
135    pub fn with_font_resolver(mut self, resolver: Arc<FontResolverImpl>) -> Self {
136        self.font_resolver = Some(resolver);
137        self
138    }
139
140    /// Sets a deterministic creation timestamp for the universe.
141    pub fn with_creation_timestamp(mut self, timestamp: Option<i64>) -> Self {
142        self.creation_timestamp = timestamp;
143        self
144    }
145
146    /// Builds a compiler universe.
147    pub fn build_universe(&self) -> FileResult<MockUniverse> {
148        let registry = Arc::new(DummyRegistry);
149        let resolver: Arc<dyn RootResolver + Send + Sync> =
150            Arc::new(RegistryPathMapper::new(registry.clone()));
151
152        Ok(CompilerUniverse::new_raw(
153            self.workspace.entry_state(&self.entry)?,
154            self.features.clone(),
155            self.inputs.clone(),
156            Vfs::new(resolver, self.workspace.access_model()),
157            registry,
158            self.font_resolver
159                .clone()
160                .unwrap_or_else(embedded_font_resolver),
161            self.creation_timestamp,
162        ))
163    }
164
165    /// Builds a compiler world snapshot.
166    pub fn build_world(&self) -> FileResult<MockWorld> {
167        Ok(self.build_universe()?.snapshot())
168    }
169}
170
171/// Returns a deterministic font resolver using Typst's embedded fonts.
172pub fn embedded_font_resolver() -> Arc<FontResolverImpl> {
173    static FONT_RESOLVER: LazyLock<Arc<FontResolverImpl>> = LazyLock::new(|| {
174        let mut searcher = MemoryFontSearcher::new();
175        for font in typst_assets::fonts() {
176            searcher.add_memory_font(Bytes::new(font));
177        }
178        Arc::new(searcher.build())
179    });
180
181    FONT_RESOLVER.clone()
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn mock_world_reads_follow_up_updates() {
190        let workspace = MockWorkspace::default_builder()
191            .file("main.typ", "#import \"content.typ\": value\n#value")
192            .file("content.typ", "#let value = [before]")
193            .build();
194        let mut universe = workspace.world("main.typ").build_universe().unwrap();
195        let content_path = workspace.path("content.typ");
196
197        assert_eq!(
198            universe
199                .snapshot()
200                .source_by_path(&content_path)
201                .unwrap()
202                .text(),
203            "#let value = [before]"
204        );
205
206        workspace
207            .update_source("content.typ", "#let value = [after]")
208            .apply_to_universe(&mut universe);
209
210        assert_eq!(
211            universe
212                .snapshot()
213                .source_by_path(&content_path)
214                .unwrap()
215                .text(),
216            "#let value = [after]"
217        );
218    }
219
220    #[test]
221    fn mock_world_handles_rename_remove_sequence() {
222        let workspace = MockWorkspace::default_builder()
223            .file("main.typ", "#import \"content.typ\": value\n#value")
224            .file("content.typ", "#let value = [before]")
225            .build();
226        let mut universe = workspace.world("main.typ").build_universe().unwrap();
227        let content_path = workspace.path("content.typ");
228        let renamed_path = workspace.path("renamed.typ");
229        let main_path = workspace.path("main.typ");
230
231        workspace
232            .rename("content.typ", "renamed.typ")
233            .unwrap()
234            .apply_to_universe(&mut universe);
235        workspace
236            .update_source("main.typ", "#import \"renamed.typ\": value\n#value")
237            .apply_to_universe(&mut universe);
238
239        let world = universe.snapshot();
240        assert!(world.source_by_path(&content_path).is_err());
241        assert_eq!(
242            world.source_by_path(&renamed_path).unwrap().text(),
243            "#let value = [before]"
244        );
245
246        workspace
247            .remove("renamed.typ")
248            .unwrap()
249            .apply_to_universe(&mut universe);
250        workspace
251            .update_source("main.typ", "#let value = [inline]\n#value")
252            .apply_to_universe(&mut universe);
253
254        let world = universe.snapshot();
255        assert!(world.source_by_path(&renamed_path).is_err());
256        assert_eq!(
257            world.source_by_path(&main_path).unwrap().text(),
258            "#let value = [inline]\n#value"
259        );
260    }
261}