Skip to content

Commit bc7582a

Browse files
committed
feat(cli): made perseus test actually run tests
This massively improves the ease of testing Perseus apps.
1 parent be9dbc9 commit bc7582a

File tree

7 files changed

+242
-35
lines changed

7 files changed

+242
-35
lines changed

packages/perseus-cli/src/bin/main.rs

+15-9
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,36 @@ use clap::Parser;
22
use command_group::stdlib::CommandGroup;
33
use directories::ProjectDirs;
44
use fmterr::fmt_err;
5+
use indicatif::MultiProgress;
56
use notify::{recommended_watcher, RecursiveMode, Watcher};
67
use perseus_cli::parse::{
7-
BuildOpts, CheckOpts, ExportOpts, ServeOpts, SnoopServeOpts, SnoopSubcommand,
8+
BuildOpts, CheckOpts, ExportOpts, ServeOpts, SnoopServeOpts, SnoopSubcommand, TestOpts,
89
};
910
use perseus_cli::{
1011
build, check_env, delete_artifacts, deploy, export, init, new,
1112
parse::{Opts, Subcommand},
12-
serve, serve_exported, tinker,
13+
serve, serve_exported, test, tinker,
1314
};
1415
use perseus_cli::{
1516
check, create_dist, delete_dist, errors::*, export_error_page, order_reload, run_reload_server,
1617
snoop_build, snoop_server, snoop_wasm_build, Tools,
1718
};
1819
use std::env;
1920
use std::path::{Path, PathBuf};
20-
use std::process::Command;
21+
use std::process::{Command, ExitCode};
2122
use std::sync::mpsc::channel;
2223

2324
// All this does is run the program and terminate with the acquired exit code
2425
#[tokio::main]
25-
async fn main() {
26+
async fn main() -> ExitCode {
2627
// In development, we'll test in the `basic` example
2728
if cfg!(debug_assertions) && env::var("TEST_EXAMPLE").is_ok() {
2829
let example_to_test = env::var("TEST_EXAMPLE").unwrap();
2930
env::set_current_dir(example_to_test).unwrap();
3031
}
3132
let exit_code = real_main().await;
32-
std::process::exit(exit_code)
33+
let u8_exit_code: u8 = exit_code.try_into().unwrap_or(1);
34+
ExitCode::from(u8_exit_code)
3335
}
3436

3537
// This manages error handling and returns a definite exit code to terminate
@@ -129,6 +131,11 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
129131
custom_watch,
130132
..
131133
})
134+
| Subcommand::Test(TestOpts {
135+
watch,
136+
custom_watch,
137+
..
138+
})
132139
| Subcommand::Snoop(SnoopSubcommand::Build {
133140
watch,
134141
custom_watch,
@@ -327,20 +334,19 @@ async fn core_watch(dir: PathBuf, opts: Opts) -> Result<i32, Error> {
327334
delete_artifacts(dir.clone(), "mutable")?;
328335
}
329336
// This orders reloads internally
330-
let (exit_code, _server_path) = serve(dir, serve_opts, &tools, &opts)?;
337+
let (exit_code, _server_path) =
338+
serve(dir, serve_opts, &tools, &opts, &MultiProgress::new(), false)?;
331339
exit_code
332340
}
333341
Subcommand::Test(ref test_opts) => {
334342
create_dist(&dir)?;
335343
let tools = Tools::new(&dir, &opts).await?;
336-
// This will be used by the subcrates
337-
env::set_var("PERSEUS_TESTING", "true");
338344
// Delete old build artifacts if `--no-build` wasn't specified
339345
if !test_opts.no_build {
340346
delete_artifacts(dir.clone(), "static")?;
341347
delete_artifacts(dir.clone(), "mutable")?;
342348
}
343-
let (exit_code, _server_path) = serve(dir, test_opts, &tools, &opts)?;
349+
let exit_code = test(dir, &test_opts, &tools, &opts)?;
344350
exit_code
345351
}
346352
Subcommand::Clean => {

packages/perseus-cli/src/deploy.rs

+5-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::parse::{DeployOpts, ExportOpts, ServeOpts};
66
use crate::serve;
77
use fs_extra::copy_items;
88
use fs_extra::dir::{copy as copy_dir, CopyOptions};
9+
use indicatif::MultiProgress;
910
use std::fs;
1011
use std::path::PathBuf;
1112

@@ -59,6 +60,9 @@ fn deploy_full(
5960
},
6061
tools,
6162
global_opts,
63+
&MultiProgress::new(),
64+
// We're not testing
65+
false,
6266
)?;
6367
if serve_exit_code != 0 {
6468
return Ok(serve_exit_code);
@@ -160,7 +164,7 @@ fn deploy_full(
160164
Ok(0)
161165
} else {
162166
// If we don't have the executable, throw an error
163-
Err(DeployError::GetServerExecutableFailed.into())
167+
Err(ExecutionError::GetServerExecutableFailedSimple.into())
164168
}
165169
}
166170

packages/perseus-cli/src/errors.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub enum ExecutionError {
5252
#[source]
5353
source: serde_json::Error,
5454
},
55+
#[error("couldn't get path to server executable (if this persists, try `perseus clean`)")]
56+
GetServerExecutableFailedSimple,
5557
#[error("expected second-last message from Cargo to contain server executable path, none existed (too few messages) (report this as a bug if it persists)")]
5658
ServerExecutableMsgNotFound,
5759
#[error("couldn't parse server executable path from Cargo (report this as a bug if it persists): {err}")]
@@ -126,8 +128,6 @@ pub enum DeployError {
126128
#[source]
127129
source: std::io::Error,
128130
},
129-
#[error("couldn't get path to server executable (if this persists, try `perseus clean`)")]
130-
GetServerExecutableFailed,
131131
#[error("couldn't copy file from '{from}' to '{to}' for deployment packaging")]
132132
MoveAssetFailed {
133133
to: String,

packages/perseus-cli/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ mod reload_server;
3131
mod serve;
3232
mod serve_exported;
3333
mod snoop;
34+
mod test;
3435
mod thread;
3536
mod tinker;
3637

@@ -52,6 +53,7 @@ pub use reload_server::{order_reload, run_reload_server};
5253
pub use serve::serve;
5354
pub use serve_exported::serve_exported;
5455
pub use snoop::{snoop_build, snoop_server, snoop_wasm_build};
56+
pub use test::test;
5557
pub use tinker::tinker;
5658

5759
/// Creates the `dist/` directory in the project root, which is necessary

packages/perseus-cli/src/parse.rs

+29-4
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ pub enum Subcommand {
9090
ExportErrorPage(ExportErrorPageOpts),
9191
Export(ExportOpts),
9292
Serve(ServeOpts),
93-
/// Serves your app as `perseus serve` does, but puts it in testing mode
94-
Test(ServeOpts),
93+
Test(TestOpts),
9594
/// Removes build artifacts in the `dist/` directory
9695
Clean,
9796
Deploy(DeployOpts),
@@ -153,8 +152,7 @@ pub struct ExportErrorPageOpts {
153152
#[clap(short, long)]
154153
pub output: String,
155154
}
156-
/// Serves your app (set the `$HOST` and `$PORT` environment variables to change
157-
/// the location it's served at)
155+
/// Serves your app
158156
#[derive(Parser, Clone)]
159157
pub struct ServeOpts {
160158
/// Don't run the final binary, but print its location instead as the last
@@ -186,6 +184,33 @@ pub struct ServeOpts {
186184
#[clap(long, default_value = "8080")]
187185
pub port: u16,
188186
}
187+
/// Serves your app as `perseus serve` does, but puts it in testing mode
188+
#[derive(Parser, Clone)]
189+
pub struct TestOpts {
190+
/// Only build the testing server, and use the results of a previous
191+
/// `perseus build`
192+
#[clap(long)]
193+
pub no_build: bool,
194+
/// Show the browser window when testing (by default, the browser is used in
195+
/// 'headless' mode); this can be useful for debugging failing tests in
196+
/// some cases
197+
#[clap(long)]
198+
pub show_browser: bool,
199+
/// Watch the files in your working directory for changes (excluding
200+
/// `target/` and `dist/`)
201+
#[clap(short, long)]
202+
pub watch: bool,
203+
/// Marks a specific file/directory to be watched (directories will be
204+
/// recursively watched)
205+
#[clap(long)]
206+
pub custom_watch: Vec<String>,
207+
/// Where to host your exported app
208+
#[clap(long, default_value = "127.0.0.1")]
209+
pub host: String,
210+
/// The port to host your exported app on
211+
#[clap(long, default_value = "8080")]
212+
pub port: u16,
213+
}
189214
/// Packages your app for deployment
190215
#[derive(Parser, Clone)]
191216
pub struct DeployOpts {

packages/perseus-cli/src/serve.rs

+37-19
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ macro_rules! handle_exit_code {
3636
fn build_server(
3737
dir: PathBuf,
3838
spinners: &MultiProgress,
39-
did_build: bool,
39+
num_steps: u8,
40+
step_offset: u8,
4041
exec: Arc<Mutex<String>>,
4142
is_release: bool,
4243
tools: &Tools,
@@ -49,15 +50,11 @@ fn build_server(
4950
let Opts {
5051
cargo_engine_args, ..
5152
} = global_opts.clone();
52-
let num_steps = match did_build {
53-
true => 4,
54-
false => 2,
55-
};
5653

5754
// Server building message
5855
let sb_msg = format!(
5956
"{} {} Building server",
60-
style(format!("[{}/{}]", num_steps - 1, num_steps))
57+
style(format!("[{}/{}]", num_steps - step_offset, num_steps))
6158
.bold()
6259
.dim(),
6360
BUILDING_SERVER
@@ -66,7 +63,7 @@ fn build_server(
6663
// We'll parallelize the building of the server with any build commands that are
6764
// currently running We deliberately insert the spinner at the end of the
6865
// list
69-
let sb_spinner = spinners.insert(num_steps - 1, ProgressBar::new_spinner());
66+
let sb_spinner = spinners.insert((num_steps - step_offset).into(), ProgressBar::new_spinner());
7067
let sb_spinner = cfg_spinner(sb_spinner, &sb_msg);
7168
let sb_target = dir;
7269
let sb_thread = spawn_thread(
@@ -133,16 +130,13 @@ fn build_server(
133130

134131
/// Runs the server at the given path, handling any errors therewith. This will
135132
/// likely be a black hole until the user manually terminates the process.
133+
///
134+
/// This function is not used by the testing process.
136135
fn run_server(
137136
exec: Arc<Mutex<String>>,
138137
dir: PathBuf,
139-
did_build: bool,
138+
num_steps: u8,
140139
) -> Result<i32, ExecutionError> {
141-
let num_steps = match did_build {
142-
true => 4,
143-
false => 2,
144-
};
145-
146140
// First off, handle any issues with the executable path
147141
let exec_val = exec.lock().unwrap();
148142
if exec_val.is_empty() {
@@ -214,32 +208,52 @@ pub fn serve(
214208
opts: &ServeOpts,
215209
tools: &Tools,
216210
global_opts: &Opts,
211+
spinners: &MultiProgress,
212+
testing: bool,
217213
) -> Result<(i32, Option<String>), ExecutionError> {
218214
// Set the environment variables for the host and port
219215
// NOTE Another part of this code depends on setting these in this way
220216
env::set_var("PERSEUS_HOST", &opts.host);
221217
env::set_var("PERSEUS_PORT", opts.port.to_string());
222218

223-
let spinners = MultiProgress::new();
224219
let did_build = !opts.no_build;
225220
let should_run = !opts.no_run;
221+
222+
// Weird naming here, but this is right
223+
let num_steps = if testing && did_build {
224+
4
225+
} else if testing && !did_build {
226+
3
227+
} else if !testing && did_build {
228+
4
229+
} else {
230+
2
231+
};
232+
226233
// We need to have a way of knowing what the executable path to the server is
227234
let exec = Arc::new(Mutex::new(String::new()));
228235
// We can begin building the server in a thread without having to deal with the
229236
// rest of the build stage yet
230237
let sb_thread = build_server(
231238
dir.clone(),
232239
&spinners,
233-
did_build,
240+
num_steps,
241+
if testing { 2 } else { 1 },
234242
Arc::clone(&exec),
235243
opts.release,
236244
tools,
237245
global_opts,
238246
)?;
239247
// Only build if the user hasn't set `--no-build`, handling non-zero exit codes
240248
if did_build {
241-
let (sg_thread, wb_thread) =
242-
build_internal(dir.clone(), &spinners, 4, opts.release, tools, global_opts)?;
249+
let (sg_thread, wb_thread) = build_internal(
250+
dir.clone(),
251+
&spinners,
252+
num_steps,
253+
opts.release,
254+
tools,
255+
global_opts,
256+
)?;
243257
let sg_res = sg_thread
244258
.join()
245259
.map_err(|_| ExecutionError::ThreadWaitFailed)??;
@@ -268,13 +282,17 @@ pub fn serve(
268282

269283
// Now actually run that executable path if we should
270284
if should_run {
271-
let exit_code = run_server(Arc::clone(&exec), dir, did_build)?;
285+
let exit_code = run_server(Arc::clone(&exec), dir, num_steps)?;
272286
Ok((exit_code, None))
273287
} else {
274288
// The user doesn't want to run the server, so we'll give them the executable
275289
// path instead
276290
let exec_str = (*exec.lock().unwrap()).to_string();
277-
println!("Not running server because `--no-run` was provided. You can run it manually by running the following executable from the root of the project.\n{}", &exec_str);
291+
// Only tell the user about this if we're not testing (which is a whole separate
292+
// workflow)
293+
if !testing {
294+
println!("Not running server because `--no-run` was provided. You can run it manually by running the following executable from the root of the project.\n{}", &exec_str);
295+
}
278296
Ok((0, Some(exec_str)))
279297
}
280298
}

0 commit comments

Comments
 (0)