summaryrefslogtreecommitdiff
path: root/tests/src/tests.rs
blob: 58bd7cf7e1083dcb4d6158978bb1d9cef03946bc (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
//! Typst's test runner.

mod args;
mod collect;
mod custom;
mod logger;
mod run;
mod world;

use std::path::Path;
use std::time::Duration;

use clap::Parser;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use rayon::iter::{ParallelBridge, ParallelIterator};

use crate::args::{CliArguments, Command};
use crate::logger::Logger;

/// The parsed command line arguments.
static ARGS: Lazy<CliArguments> = Lazy::new(CliArguments::parse);

/// The directory where the test suite is located.
const SUITE_PATH: &str = "tests/suite";

/// The directory where the full test results are stored.
const STORE_PATH: &str = "tests/store";

/// The directory where the reference images are stored.
const REF_PATH: &str = "tests/ref";

/// The file where the skipped tests are stored.
const SKIP_PATH: &str = "tests/skip.txt";

/// The maximum size of reference images that aren't marked as `// LARGE`.
const REF_LIMIT: usize = 20 * 1024;

fn main() {
    setup();

    match &ARGS.command {
        None => test(),
        Some(Command::Clean) => clean(),
        Some(Command::Undangle) => undangle(),
    }
}

fn setup() {
    // Make all paths relative to the workspace. That's nicer for IDEs when
    // clicking on paths printed to the terminal.
    std::env::set_current_dir("..").unwrap();

    // Create the storage.
    for ext in ["render", "pdf", "svg"] {
        std::fs::create_dir_all(Path::new(STORE_PATH).join(ext)).unwrap();
    }

    // Set up the thread pool.
    if let Some(num_threads) = ARGS.num_threads {
        rayon::ThreadPoolBuilder::new()
            .num_threads(num_threads)
            .build_global()
            .unwrap();
    }
}

fn test() {
    let (tests, skipped) = match crate::collect::collect() {
        Ok(output) => output,
        Err(errors) => {
            eprintln!("failed to collect tests");
            for error in errors {
                eprintln!("❌ {error}");
            }
            std::process::exit(1);
        }
    };

    let selected = tests.len();
    if ARGS.list {
        for test in tests.iter() {
            println!("{test}");
        }
        eprintln!("{selected} selected, {skipped} skipped");
        return;
    } else if selected == 0 {
        eprintln!("no test selected");
        return;
    }

    // Run the tests.
    let logger = Mutex::new(Logger::new(selected, skipped));
    std::thread::scope(|scope| {
        let logger = &logger;
        let (sender, receiver) = std::sync::mpsc::channel();

        // Regularly refresh the logger in case we make no progress.
        scope.spawn(move || {
            while receiver.recv_timeout(Duration::from_millis(500)).is_err() {
                if !logger.lock().refresh() {
                    eprintln!("tests seem to be stuck");
                    std::process::exit(1);
                }
            }
        });

        // Run the tests.
        //
        // We use `par_bridge` instead of `par_iter` because the former
        // results in a stack overflow during PDF export. Probably related
        // to `typst::utils::Deferred` yielding.
        tests.iter().par_bridge().for_each(|test| {
            logger.lock().start(test);
            let result = std::panic::catch_unwind(|| run::run(test));
            logger.lock().end(test, result);
        });

        sender.send(()).unwrap();
    });

    let passed = logger.into_inner().finish();
    if !passed {
        std::process::exit(1);
    }
}

fn clean() {
    std::fs::remove_dir_all(STORE_PATH).unwrap();
}

fn undangle() {
    match crate::collect::collect() {
        Ok(_) => eprintln!("no danging reference images"),
        Err(errors) => {
            for error in errors {
                if error.message == "dangling reference image" {
                    std::fs::remove_file(&error.pos.path).unwrap();
                    eprintln!("✅ deleted {}", error.pos.path.display());
                }
            }
        }
    }
}