tytanic_core/test/
mod.rs

1//! Test loading and on-disk manipulation.
2
3use std::fmt::Debug;
4use std::fs::{self, File};
5use std::io::{self, Write};
6use std::time::{Duration, Instant};
7
8use ecow::{eco_vec, EcoString, EcoVec};
9use thiserror::Error;
10use typst::diag::SourceDiagnostic;
11use typst::syntax::{FileId, Source, VirtualPath};
12
13use crate::doc;
14use crate::doc::{compare, compile, Document, SaveError};
15use crate::project::{Project, Vcs};
16
17mod annotation;
18mod id;
19
20pub use self::annotation::{Annotation, ParseAnnotationError};
21pub use self::id::{Id, ParseIdError};
22
23// NOTE(tinger): the order of ignoring and deleting/creating documents is not
24// random, this is specifically for VCS like jj with active watchman triggers
25// and auto snapshotting.
26//
27// This is currently untested though.
28
29/// The default test input as source code.
30pub const DEFAULT_TEST_INPUT: &str = include_str!("default-test.typ");
31
32/// The default test output as an encouded PNG.
33pub const DEFAULT_TEST_OUTPUT: &[u8] = include_bytes!("default-test.png");
34
35/// References for a test.
36#[derive(Debug, Clone)]
37pub enum Reference {
38    /// An ephemeral reference script used to compile the reference document on
39    /// the fly.
40    Ephemeral(EcoString),
41
42    /// Persistent references which are stored on disk.
43    Persistent {
44        /// The reference document.
45        doc: Document,
46
47        /// The optimization options to use when storing the document, `None`
48        /// disabled optimization.
49        opt: Option<Box<oxipng::Options>>,
50    },
51}
52
53/// The kind of a unit test.
54#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
55pub enum Kind {
56    /// Test is compared to ephemeral references, these are compiled on the fly
57    /// from a reference script.
58    Ephemeral,
59
60    /// Test is compared to persistent references, these are pre-compiled and
61    /// loaded for comparison.
62    Persistent,
63
64    /// Test is only compiled.
65    CompileOnly,
66}
67
68impl Kind {
69    /// Whether this kind is is ephemeral.
70    pub fn is_ephemeral(self) -> bool {
71        matches!(self, Kind::Ephemeral)
72    }
73
74    /// Whether this kind is persistent.
75    pub fn is_persistent(self) -> bool {
76        matches!(self, Kind::Persistent)
77    }
78
79    /// Whether this kind is compile-only.
80    pub fn is_compile_only(self) -> bool {
81        matches!(self, Kind::CompileOnly)
82    }
83
84    /// Returns a kebab-case string representing this kind.
85    pub fn as_str(self) -> &'static str {
86        match self {
87            Kind::Ephemeral => "ephemeral",
88            Kind::Persistent => "persistent",
89            Kind::CompileOnly => "compile-only",
90        }
91    }
92}
93
94impl Reference {
95    /// The kind of this reference.
96    pub fn kind(&self) -> Kind {
97        match self {
98            Self::Ephemeral(_) => Kind::Ephemeral,
99            Self::Persistent { doc: _, opt: _ } => Kind::Persistent,
100        }
101    }
102}
103
104/// A standalone test script and its assocaited documents.
105#[derive(Debug, Clone, PartialEq)]
106pub struct Test {
107    id: Id,
108    kind: Kind,
109    annotations: EcoVec<Annotation>,
110}
111
112impl Test {
113    #[cfg(test)]
114    pub(crate) fn new_test(id: Id, kind: Kind) -> Self {
115        Self {
116            id,
117            kind,
118            annotations: eco_vec![],
119        }
120    }
121
122    /// Attempt to load a test, returns `None` if no test could be found.
123    pub fn load(project: &Project, id: Id) -> Result<Option<Test>, LoadError> {
124        let test_script = project.unit_test_script(&id);
125
126        if !test_script.try_exists()? {
127            return Ok(None);
128        }
129
130        let kind = if project.unit_test_ref_script(&id).try_exists()? {
131            Kind::Ephemeral
132        } else if project.unit_test_ref_dir(&id).try_exists()? {
133            Kind::Persistent
134        } else {
135            Kind::CompileOnly
136        };
137
138        let annotations = Annotation::collect(&fs::read_to_string(test_script)?)?;
139
140        Ok(Some(Test {
141            id,
142            kind,
143            annotations,
144        }))
145    }
146}
147
148impl Test {
149    /// The id of this test.
150    pub fn id(&self) -> &Id {
151        &self.id
152    }
153
154    /// The kind of this test.
155    pub fn kind(&self) -> Kind {
156        self.kind
157    }
158
159    /// This test's annotations.
160    pub fn annotations(&self) -> &[Annotation] {
161        &self.annotations
162    }
163
164    /// Whether this test has a skip annotation.
165    pub fn is_skip(&self) -> bool {
166        self.annotations.contains(&Annotation::Skip)
167    }
168}
169
170impl Test {
171    /// Creates a new test on disk, the kind is inferred from the passed
172    /// reference and annotations are parsed from the test script.
173    pub fn create(
174        project: &Project,
175        vcs: Option<&Vcs>,
176        id: Id,
177        source: &str,
178        reference: Option<Reference>,
179    ) -> Result<Test, CreateError> {
180        let test_dir = project.unit_test_dir(&id);
181        tytanic_utils::fs::create_dir(test_dir, true)?;
182
183        let mut file = File::options()
184            .write(true)
185            .create_new(true)
186            .open(project.unit_test_script(&id))?;
187
188        file.write_all(source.as_bytes())?;
189
190        let kind = reference
191            .as_ref()
192            .map(Reference::kind)
193            .unwrap_or(Kind::CompileOnly);
194
195        let annotations = Annotation::collect(source)?;
196
197        let this = Self {
198            id,
199            kind,
200            annotations,
201        };
202
203        // ingore temporaries before creating any
204        if let Some(vcs) = vcs {
205            vcs.ignore(project, &this)?;
206        }
207
208        match reference {
209            Some(Reference::Ephemeral(reference)) => {
210                this.create_reference_script(project, reference.as_str())?;
211            }
212            Some(Reference::Persistent {
213                doc: reference,
214                opt: options,
215            }) => {
216                this.create_reference_document(project, &reference, options.as_deref())?;
217            }
218            None => {}
219        }
220
221        Ok(this)
222    }
223
224    /// Creates the temporary directories of this test.
225    pub fn create_temporary_directories(&self, project: &Project) -> io::Result<()> {
226        if self.kind.is_ephemeral() {
227            tytanic_utils::fs::remove_dir(project.unit_test_ref_dir(&self.id), true)?;
228            tytanic_utils::fs::create_dir(project.unit_test_ref_dir(&self.id), true)?;
229        }
230
231        tytanic_utils::fs::create_dir(project.unit_test_out_dir(&self.id), true)?;
232        tytanic_utils::fs::create_dir(project.unit_test_diff_dir(&self.id), true)?;
233
234        Ok(())
235    }
236
237    /// Creates the test script of this test, this will truncate the file if it
238    /// already exists.
239    pub fn create_script(&self, project: &Project, source: &str) -> io::Result<()> {
240        std::fs::write(project.unit_test_script(&self.id), source)?;
241        Ok(())
242    }
243
244    /// Creates reference script of this test, this will truncate the file if it
245    /// already exists.
246    pub fn create_reference_script(&self, project: &Project, source: &str) -> io::Result<()> {
247        std::fs::write(project.unit_test_ref_script(&self.id), source)?;
248        Ok(())
249    }
250
251    /// Creates the persistent reference document of this test.
252    pub fn create_reference_document(
253        &self,
254        project: &Project,
255        reference: &Document,
256        optimize_options: Option<&oxipng::Options>,
257    ) -> Result<(), SaveError> {
258        // NOTE(tinger): if there are already more pages than we want to create,
259        // the surplus pages would persist and make every comparison fail due to
260        // a page count mismatch, so we clear them to be sure.
261        self.delete_reference_document(project)?;
262
263        let ref_dir = project.unit_test_ref_dir(&self.id);
264        tytanic_utils::fs::create_dir(&ref_dir, true)?;
265        reference.save(&ref_dir, optimize_options)?;
266
267        Ok(())
268    }
269
270    /// Deletes all directories and scripts of this test.
271    pub fn delete(&self, project: &Project) -> io::Result<()> {
272        self.delete_reference_document(project)?;
273        self.delete_reference_script(project)?;
274        self.delete_temporary_directories(project)?;
275
276        tytanic_utils::fs::remove_file(project.unit_test_script(&self.id))?;
277        tytanic_utils::fs::remove_dir(project.unit_test_dir(&self.id), true)?;
278
279        Ok(())
280    }
281
282    /// Deletes the temporary directories of this test.
283    pub fn delete_temporary_directories(&self, project: &Project) -> io::Result<()> {
284        if !self.kind.is_persistent() {
285            tytanic_utils::fs::remove_dir(project.unit_test_ref_dir(&self.id), true)?;
286        }
287
288        tytanic_utils::fs::remove_dir(project.unit_test_out_dir(&self.id), true)?;
289        tytanic_utils::fs::remove_dir(project.unit_test_diff_dir(&self.id), true)?;
290        Ok(())
291    }
292
293    /// Deletes the test script of of this test.
294    pub fn delete_script(&self, project: &Project) -> io::Result<()> {
295        tytanic_utils::fs::remove_file(project.unit_test_script(&self.id))?;
296        Ok(())
297    }
298
299    /// Deletes reference script of of this test.
300    pub fn delete_reference_script(&self, project: &Project) -> io::Result<()> {
301        tytanic_utils::fs::remove_file(project.unit_test_ref_script(&self.id))?;
302        Ok(())
303    }
304
305    /// Deletes persistent reference document of this test.
306    pub fn delete_reference_document(&self, project: &Project) -> io::Result<()> {
307        tytanic_utils::fs::remove_dir(project.unit_test_ref_dir(&self.id), true)?;
308        Ok(())
309    }
310
311    /// Removes any previous references, if they exist and creates a reference
312    /// script by copying the test script.
313    pub fn make_ephemeral(&mut self, project: &Project, vcs: Option<&Vcs>) -> io::Result<()> {
314        self.kind = Kind::Ephemeral;
315
316        // ensure deletion is recorded before ignore file is updated
317        self.delete_reference_script(project)?;
318        self.delete_reference_document(project)?;
319
320        if let Some(vcs) = vcs {
321            vcs.ignore(project, self)?;
322        }
323
324        // copy refernces after ignore file is updated
325        std::fs::copy(
326            project.unit_test_script(&self.id),
327            project.unit_test_ref_script(&self.id),
328        )?;
329
330        Ok(())
331    }
332
333    /// Removes any previous references, if they exist and creates persistent
334    /// references from the given pages.
335    pub fn make_persistent(
336        &mut self,
337        project: &Project,
338        vcs: Option<&Vcs>,
339        reference: &Document,
340        optimize_options: Option<&oxipng::Options>,
341    ) -> Result<(), SaveError> {
342        self.kind = Kind::Persistent;
343
344        // ensure deletion/creation is recorded before ignore file is updated
345        self.delete_reference_script(project)?;
346        self.create_reference_document(project, reference, optimize_options)?;
347
348        if let Some(vcs) = vcs {
349            vcs.ignore(project, self)?;
350        }
351
352        Ok(())
353    }
354
355    /// Removes any previous references, if they exist.
356    pub fn make_compile_only(&mut self, project: &Project, vcs: Option<&Vcs>) -> io::Result<()> {
357        self.kind = Kind::CompileOnly;
358
359        // ensure deletion is recorded before ignore file is updated
360        self.delete_reference_document(project)?;
361        self.delete_reference_script(project)?;
362
363        if let Some(vcs) = vcs {
364            vcs.ignore(project, self)?;
365        }
366
367        Ok(())
368    }
369
370    /// Loads the test script source of this test.
371    pub fn load_source(&self, project: &Project) -> io::Result<Source> {
372        let test_script = project.unit_test_script(&self.id);
373
374        Ok(Source::new(
375            FileId::new(
376                None,
377                VirtualPath::new(
378                    test_script
379                        .strip_prefix(project.root())
380                        .unwrap_or(&test_script),
381                ),
382            ),
383            std::fs::read_to_string(test_script)?,
384        ))
385    }
386
387    /// Loads the reference test script source of this test, if this test is
388    /// ephemeral.
389    pub fn load_reference_source(&self, project: &Project) -> io::Result<Option<Source>> {
390        if !self.kind().is_ephemeral() {
391            return Ok(None);
392        }
393
394        let ref_script = project.unit_test_ref_script(&self.id);
395        Ok(Some(Source::new(
396            FileId::new(
397                None,
398                VirtualPath::new(
399                    ref_script
400                        .strip_prefix(project.root())
401                        .unwrap_or(&ref_script),
402                ),
403            ),
404            std::fs::read_to_string(ref_script)?,
405        )))
406    }
407
408    /// Loads the test document of this test.
409    pub fn load_document(&self, project: &Project) -> Result<Document, doc::LoadError> {
410        Document::load(project.unit_test_out_dir(&self.id))
411    }
412
413    /// Loads the persistent reference document of this test.
414    pub fn load_reference_document(&self, project: &Project) -> Result<Document, doc::LoadError> {
415        Document::load(project.unit_test_ref_dir(&self.id))
416    }
417}
418
419/// Returned by [`Test::create`].
420#[derive(Debug, Error)]
421pub enum CreateError {
422    /// An error occurred while parsing a test annotation.
423    #[error("an error occurred while parsing a test annotation")]
424    Annotation(#[from] ParseAnnotationError),
425
426    /// An error occurred while saving test files.
427    #[error("an error occurred while saving test files")]
428    Save(#[from] doc::SaveError),
429
430    /// An io error occurred.
431    #[error("an io error occurred")]
432    Io(#[from] io::Error),
433}
434
435/// Returned by [`Test::load`].
436#[derive(Debug, Error)]
437pub enum LoadError {
438    /// An error occurred while parsing a test annotation.
439    #[error("an error occurred while parsing a test annotation")]
440    Annotation(#[from] ParseAnnotationError),
441
442    /// An io error occurred.
443    #[error("an io error occurred")]
444    Io(#[from] io::Error),
445}
446
447/// The stage of a single test run.
448#[derive(Debug, Clone, Default)]
449pub enum Stage {
450    /// The test was cancelled or not started in the first place.
451    #[default]
452    Skipped,
453
454    /// The test was filtered out by a [`Filter`].
455    ///
456    /// [`Filter`]: crate::suite::Filter
457    Filtered,
458
459    /// The test failed compilation.
460    FailedCompilation {
461        /// The inner error.
462        error: compile::Error,
463
464        /// Whether this was a compilation failure of the reference.
465        reference: bool,
466    },
467
468    /// The test passed compilation, but failed comparison.
469    FailedComparison(compare::Error),
470
471    /// The test passed compilation, but did not run comparison.
472    PassedCompilation,
473
474    /// The test passed compilation and comparison.
475    PassedComparison,
476
477    /// The test passed compilation and updated its references.
478    Updated {
479        /// Whether the references were optimized.
480        optimized: bool,
481    },
482}
483
484/// The result of a single test run.
485#[derive(Debug, Clone)]
486pub struct TestResult {
487    stage: Stage,
488    warnings: EcoVec<SourceDiagnostic>,
489    timestamp: Instant,
490    duration: Duration,
491}
492
493impl TestResult {
494    /// Create a result for a test for a skipped test. This will set the
495    /// starting time to now, the duration to zero and the result to `None`.
496    ///
497    /// This can be used for constructing test results in advance to ensure an
498    /// aborted test run contains a skip result for all yet-to-be-run tests.
499    pub fn skipped() -> Self {
500        Self {
501            stage: Stage::Skipped,
502            warnings: eco_vec![],
503            timestamp: Instant::now(),
504            duration: Duration::ZERO,
505        }
506    }
507
508    /// Create a result for a test for a filtered test. This will set the
509    /// starting time to now, the duration to zero and the result to filtered.
510    pub fn filtered() -> Self {
511        Self {
512            stage: Stage::Filtered,
513            warnings: eco_vec![],
514            timestamp: Instant::now(),
515            duration: Duration::ZERO,
516        }
517    }
518}
519
520impl TestResult {
521    /// The stage of this rest result, if it was started.
522    pub fn stage(&self) -> &Stage {
523        &self.stage
524    }
525
526    /// The warnings of the test emitted by the compiler.
527    pub fn warnings(&self) -> &[SourceDiagnostic] {
528        &self.warnings
529    }
530
531    /// The timestamp at which the suite run started.
532    pub fn timestamp(&self) -> Instant {
533        self.timestamp
534    }
535
536    /// The duration of the test, this a zero if this test wasn't started.
537    pub fn duration(&self) -> Duration {
538        self.duration
539    }
540
541    /// Whether the test was not started.
542    pub fn is_skipped(&self) -> bool {
543        matches!(&self.stage, Stage::Skipped)
544    }
545
546    /// Whether the test was filtered out.
547    pub fn is_filtered(&self) -> bool {
548        matches!(&self.stage, Stage::Filtered)
549    }
550
551    /// Whether the test passed compilation and/or comparison/update.
552    pub fn is_pass(&self) -> bool {
553        matches!(
554            &self.stage,
555            Stage::PassedCompilation | Stage::PassedComparison | Stage::Updated { .. }
556        )
557    }
558
559    /// Whether the test failed compilation or comparison.
560    pub fn is_fail(&self) -> bool {
561        matches!(
562            &self.stage,
563            Stage::FailedCompilation { .. } | Stage::FailedComparison(..),
564        )
565    }
566
567    /// The errors emitted by the compiler if compilation failed.
568    pub fn errors(&self) -> Option<&[SourceDiagnostic]> {
569        match &self.stage {
570            Stage::FailedCompilation { error, .. } => Some(&error.0),
571            _ => None,
572        }
573    }
574}
575
576impl TestResult {
577    /// Sets the timestamp to [`Instant::now`].
578    ///
579    /// See [`TestResult::end`].
580    pub fn start(&mut self) {
581        self.timestamp = Instant::now();
582    }
583
584    /// Sets the duration to the time elapsed since [`TestResult::start`] was
585    /// called.
586    pub fn end(&mut self) {
587        self.duration = self.timestamp.elapsed();
588    }
589
590    /// Sets the kind for this test to a compilation pass.
591    pub fn set_passed_compilation(&mut self) {
592        self.stage = Stage::PassedCompilation;
593    }
594
595    /// Sets the kind for this test to a reference compilation failure.
596    pub fn set_failed_reference_compilation(&mut self, error: compile::Error) {
597        self.stage = Stage::FailedCompilation {
598            error,
599            reference: true,
600        };
601    }
602
603    /// Sets the kind for this test to a test compilation failure.
604    pub fn set_failed_test_compilation(&mut self, error: compile::Error) {
605        self.stage = Stage::FailedCompilation {
606            error,
607            reference: false,
608        };
609    }
610
611    /// Sets the kind for this test to a test comparison pass.
612    pub fn set_passed_comparison(&mut self) {
613        self.stage = Stage::PassedComparison;
614    }
615
616    /// Sets the kind for this test to a comparison failure.
617    pub fn set_failed_comparison(&mut self, error: compare::Error) {
618        self.stage = Stage::FailedComparison(error);
619    }
620
621    /// Sets the kind for this test to a test update.
622    pub fn set_updated(&mut self, optimized: bool) {
623        self.stage = Stage::Updated { optimized };
624    }
625
626    /// Sets the warnings for this test.
627    pub fn set_warnings<I>(&mut self, warnings: I)
628    where
629        I: Into<EcoVec<SourceDiagnostic>>,
630    {
631        self.warnings = warnings.into();
632    }
633}
634
635impl Default for TestResult {
636    fn default() -> Self {
637        Self::skipped()
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use tytanic_utils::fs::{Setup, TempTestEnv};
644
645    use super::*;
646
647    fn id(id: &str) -> Id {
648        Id::new(id).unwrap()
649    }
650
651    fn test(test_id: &str, kind: Kind) -> Test {
652        Test::new_test(id(test_id), kind)
653    }
654
655    fn setup_all(root: &mut Setup) -> &mut Setup {
656        root.setup_file("tests/compile-only/test.typ", "Hello World")
657            .setup_file("tests/ephemeral/test.typ", "Hello World")
658            .setup_file("tests/ephemeral/ref.typ", "Hello\nWorld")
659            .setup_file("tests/persistent/test.typ", "Hello World")
660            .setup_dir("tests/persistent/ref")
661    }
662
663    #[test]
664    fn test_create() {
665        TempTestEnv::run(
666            |root| root.setup_dir("tests"),
667            |root| {
668                let project = Project::new(root);
669                Test::create(&project, None, id("compile-only"), "Hello World", None).unwrap();
670
671                Test::create(
672                    &project,
673                    None,
674                    id("ephemeral"),
675                    "Hello World",
676                    Some(Reference::Ephemeral("Hello\nWorld".into())),
677                )
678                .unwrap();
679
680                Test::create(
681                    &project,
682                    None,
683                    id("persistent"),
684                    "Hello World",
685                    Some(Reference::Persistent {
686                        doc: Document::new(vec![]),
687                        opt: None,
688                    }),
689                )
690                .unwrap();
691            },
692            |root| {
693                root.expect_file_content("tests/compile-only/test.typ", "Hello World")
694                    .expect_file_content("tests/ephemeral/test.typ", "Hello World")
695                    .expect_file_content("tests/ephemeral/ref.typ", "Hello\nWorld")
696                    .expect_file_content("tests/persistent/test.typ", "Hello World")
697                    .expect_dir("tests/persistent/ref")
698            },
699        );
700    }
701
702    #[test]
703    fn test_make_ephemeral() {
704        TempTestEnv::run(
705            setup_all,
706            |root| {
707                let project = Project::new(root);
708                test("compile-only", Kind::CompileOnly)
709                    .make_ephemeral(&project, None)
710                    .unwrap();
711                test("ephemeral", Kind::Ephemeral)
712                    .make_ephemeral(&project, None)
713                    .unwrap();
714                test("persistent", Kind::Persistent)
715                    .make_ephemeral(&project, None)
716                    .unwrap();
717            },
718            |root| {
719                root.expect_file_content("tests/compile-only/test.typ", "Hello World")
720                    .expect_file_content("tests/compile-only/ref.typ", "Hello World")
721                    .expect_file_content("tests/ephemeral/test.typ", "Hello World")
722                    .expect_file_content("tests/ephemeral/ref.typ", "Hello World")
723                    .expect_file_content("tests/persistent/test.typ", "Hello World")
724                    .expect_file_content("tests/persistent/ref.typ", "Hello World")
725            },
726        );
727    }
728
729    #[test]
730    fn test_make_persistent() {
731        TempTestEnv::run(
732            setup_all,
733            |root| {
734                let project = Project::new(root);
735                test("compile-only", Kind::CompileOnly)
736                    .make_persistent(&project, None, &Document::new([]), None)
737                    .unwrap();
738
739                test("ephemeral", Kind::Ephemeral)
740                    .make_persistent(&project, None, &Document::new([]), None)
741                    .unwrap();
742
743                test("persistent", Kind::Persistent)
744                    .make_persistent(&project, None, &Document::new([]), None)
745                    .unwrap();
746            },
747            |root| {
748                root.expect_file_content("tests/compile-only/test.typ", "Hello World")
749                    .expect_dir("tests/compile-only/ref")
750                    .expect_file_content("tests/ephemeral/test.typ", "Hello World")
751                    .expect_dir("tests/ephemeral/ref")
752                    .expect_file_content("tests/persistent/test.typ", "Hello World")
753                    .expect_dir("tests/persistent/ref")
754            },
755        );
756    }
757
758    #[test]
759    fn test_make_compile_only() {
760        TempTestEnv::run(
761            setup_all,
762            |root| {
763                let project = Project::new(root);
764                test("compile-only", Kind::CompileOnly)
765                    .make_compile_only(&project, None)
766                    .unwrap();
767
768                test("ephemeral", Kind::Ephemeral)
769                    .make_compile_only(&project, None)
770                    .unwrap();
771
772                test("persistent", Kind::Persistent)
773                    .make_compile_only(&project, None)
774                    .unwrap();
775            },
776            |root| {
777                root.expect_file_content("tests/compile-only/test.typ", "Hello World")
778                    .expect_file_content("tests/ephemeral/test.typ", "Hello World")
779                    .expect_file_content("tests/persistent/test.typ", "Hello World")
780            },
781        );
782    }
783
784    #[test]
785    fn test_load_sources() {
786        TempTestEnv::run_no_check(
787            |root| {
788                root.setup_file("tests/fancy/test.typ", "Hello World")
789                    .setup_file("tests/fancy/ref.typ", "Hello\nWorld")
790            },
791            |root| {
792                let project = Project::new(root);
793
794                let mut test = test("fancy", Kind::Ephemeral);
795                test.kind = Kind::Ephemeral;
796
797                test.load_source(&project).unwrap();
798                test.load_reference_source(&project).unwrap().unwrap();
799            },
800        );
801    }
802
803    #[test]
804    fn test_sources_virtual() {
805        TempTestEnv::run_no_check(
806            |root| root.setup_file_empty("tests/fancy/test.typ"),
807            |root| {
808                let project = Project::new(root);
809
810                let test = test("fancy", Kind::CompileOnly);
811
812                let source = test.load_source(&project).unwrap();
813                assert_eq!(
814                    source.id().vpath().resolve(root).unwrap(),
815                    root.join("tests/fancy/test.typ")
816                );
817            },
818        );
819    }
820}