perseus_cli/
test.rs

1use crate::cmd::{cfg_spinner, run_stage};
2use crate::install::Tools;
3use crate::parse::{Opts, ServeOpts, TestOpts};
4use crate::thread::spawn_thread;
5use crate::{errors::*, serve};
6use console::{style, Emoji};
7use indicatif::{MultiProgress, ProgressBar};
8use std::path::PathBuf;
9use std::process::{Command, Stdio};
10
11// Emoji for stages
12static TESTING: Emoji<'_, '_> = Emoji("🧪", "");
13
14/// Returns the exit code if it's non-zero.
15macro_rules! handle_exit_code {
16    ($code:expr) => {
17        let (_, _, code) = $code;
18        if code != 0 {
19            return ::std::result::Result::Ok(code);
20        }
21    };
22}
23
24/// Tests the user's app by creating a testing server and running `cargo test`
25/// against it, which will presumably use a WebDriver of some kind.
26pub fn test(
27    dir: PathBuf,
28    test_opts: &TestOpts,
29    tools: &Tools,
30    global_opts: &Opts,
31) -> Result<i32, ExecutionError> {
32    // We need to own this for the threads
33    let tools = tools.clone();
34    let Opts {
35        cargo_engine_path,
36        cargo_engine_args,
37        verbose,
38        ..
39    } = global_opts.clone();
40
41    let serve_opts = ServeOpts {
42        // We want to run the binary while we run `cargo test` at the same time
43        no_run: true,
44        no_build: test_opts.no_build,
45        release: false,
46        standalone: false,
47        watch: test_opts.watch,
48        custom_watch: test_opts.custom_watch.clone(),
49        host: test_opts.host.clone(),
50        port: test_opts.port,
51    };
52    let num_steps: u8 = if test_opts.no_build { 2 } else { 4 };
53    // This will do all sorts of things with spinners etc., but we've told it we're
54    // testing, so things will be neater
55    let spinners = MultiProgress::new();
56    let (exit_code, server_path) = serve(
57        dir.clone(),
58        &serve_opts,
59        &tools,
60        global_opts,
61        &spinners,
62        true,
63    )?;
64    if exit_code != 0 {
65        return Ok(exit_code);
66    }
67    if let Some(server_path) = server_path {
68        // Building is complete and we have a path to run the server, so we'll now do
69        // that with a child process (doesn't need to be in a separate thread, since
70        // it's long-running)
71        let mut server = Command::new(&server_path)
72            .envs([
73                ("PERSEUS_ENGINE_OPERATION", "serve"),
74                ("PERSEUS_TESTING", "true"),
75            ])
76            .current_dir(&dir)
77            .stdin(Stdio::piped())
78            .stdout(Stdio::piped())
79            .stderr(Stdio::piped())
80            .spawn()
81            .map_err(|err| ExecutionError::CmdExecFailed {
82                cmd: server_path,
83                source: err,
84            })?;
85
86        // Now run the Cargo tests against that
87        let test_msg = format!(
88            "{} {} Running tests",
89            style(format!("[{}/{}]", num_steps, num_steps)).bold().dim(),
90            TESTING,
91        );
92        let test_spinner = spinners.insert(num_steps.into(), ProgressBar::new_spinner());
93        let test_spinner = cfg_spinner(test_spinner, &test_msg);
94        let test_dir = dir;
95        let headless = !test_opts.show_browser;
96        let test_thread = spawn_thread(
97            move || {
98                handle_exit_code!(run_stage(
99                    vec![&format!(
100                        // We use single-threaded testing, because most webdrivers don't support
101                        // multithreaded testing yet
102                        "{} test {} -- --test-threads 1",
103                        cargo_engine_path, cargo_engine_args
104                    )],
105                    &test_dir,
106                    &test_spinner,
107                    &test_msg,
108                    if headless {
109                        vec![
110                            ("CARGO_TARGET_DIR", "dist/target_engine"),
111                            ("RUSTFLAGS", "--cfg=engine"),
112                            ("CARGO_TERM_COLOR", "always"),
113                            ("PERSEUS_RUN_WASM_TESTS", "true"),
114                            ("PERSEUS_RUN_WASM_TESTS_HEADLESS", "true"),
115                        ]
116                    } else {
117                        vec![
118                            ("CARGO_TARGET_DIR", "dist/target_engine"),
119                            ("RUSTFLAGS", "--cfg=engine"),
120                            ("CARGO_TERM_COLOR", "always"),
121                            ("PERSEUS_RUN_WASM_TESTS", "true"),
122                        ]
123                    },
124                    verbose,
125                )?);
126
127                Ok(0)
128            },
129            // See above
130            false,
131        );
132
133        let test_res = test_thread
134            .join()
135            .map_err(|_| ExecutionError::ThreadWaitFailed)??;
136
137        // If the server has already terminated, it had an error, and that would be
138        // reflected in the tests
139        let _ = server.kill();
140
141        if test_res != 0 {
142            return Ok(test_res);
143        }
144
145        // We've handled errors in the component threads, so the exit code is now zero
146        Ok(0)
147    } else {
148        Err(ExecutionError::GetServerExecutableFailedSimple)
149    }
150}