use std::cell::RefCell;
use std::fs;
use std::path::PathBuf;
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};
use cli::build_executor::{BuildExecutionPlan, BuildWatchExecutionPlan, create_build_watch_state};
use cli::commands::{Input, InputValue};
use cli::configuration::Configuration;
use cli::error::CliError;
use cli::start_action::{StartActionPlanRequest, create_start_action_plan};
use cli::start_executor::{
StartChildProcess, StartExecutionPlan, StartProcessSpawner, StartWatchExecutionPlan,
create_start_execution_plan, execute_start_plan, execute_start_watch_plan_with, parse_env_file,
start_watch_tick,
};
use schematics::factories::ApplicationOptions;
use schematics::generators::write_application;
#[test]
fn start_executor_runs_generated_rust_project_with_cargo_run() {
let root = unique_temp_dir("nestrs-start-generated");
write_application(
ApplicationOptions {
name: "Cats API".to_string(),
author: None,
description: None,
directory: None,
strict: None,
version: None,
language: Some("rs".to_string()),
package_manager: None,
dependencies: None,
dev_dependencies: None,
spec: None,
spec_file_suffix: None,
},
&root,
)
.expect("application should be generated");
let project_root = root.join("cats-api");
let plan = create_start_action_plan(StartActionPlanRequest {
cwd: project_root,
configuration: Configuration::default(),
command_inputs: vec![],
command_options: vec![],
extra_flags: vec![],
ts_build_info_file: None,
})
.expect("start action plan should be created");
let mut execution_plan =
create_start_execution_plan(&plan).expect("start execution plan should be created");
assert_eq!(execution_plan.command.binary, "cargo");
assert_eq!(execution_plan.command.command, "run");
execution_plan.command.collect = true;
execute_start_plan(&execution_plan).expect("generated Rust project should cargo run");
}
#[test]
fn start_executor_loads_env_files_into_cargo_run_command() {
let root = unique_temp_dir("nestrs-start-env-file");
write_file(
&root.join(".env"),
"PORT=3000\nexport APP_NAME=\"Cats API\"\n# comment\nEMPTY=\n",
);
let plan = create_start_action_plan(StartActionPlanRequest {
cwd: root,
configuration: Configuration::default(),
command_inputs: vec![],
command_options: vec![Input::new(
"envFile",
Some(InputValue::StringList(vec![".env".to_string()])),
)],
extra_flags: vec![],
ts_build_info_file: None,
})
.expect("start action plan should be created");
let execution_plan =
create_start_execution_plan(&plan).expect("start execution plan should be created");
assert!(execution_plan.warnings.is_empty());
assert_eq!(
execution_plan.command.env,
vec![
("PORT".to_string(), "3000".to_string()),
("APP_NAME".to_string(), "Cats API".to_string()),
("EMPTY".to_string(), String::new()),
]
);
}
#[test]
fn env_parser_ignores_comments_and_strips_quotes() {
let values = parse_env_file(
r#"
# ignored
FOO=bar # inline
QUOTED="hello # still value"
SINGLE='value'
"#,
)
.expect("env file should parse");
assert_eq!(
values,
vec![
("FOO".to_string(), "bar".to_string()),
("QUOTED".to_string(), "hello # still value".to_string()),
("SINGLE".to_string(), "value".to_string()),
]
);
}
#[test]
fn start_executor_watch_rebuilds_and_restarts_running_process() {
let root = unique_temp_dir("nestrs-start-watch-restart");
write_file(
&root.join("Cargo.toml"),
r#"[package]
name = "watch-restart-fixture"
version = "0.1.0"
edition = "2024"
"#,
);
let source_file = root.join("src").join("main.rs");
write_file(&source_file, "fn main() {}\n");
let plan = create_start_action_plan(StartActionPlanRequest {
cwd: root,
configuration: Configuration::default(),
command_inputs: vec![],
command_options: vec![Input::new("watch", Some(InputValue::Bool(true)))],
extra_flags: vec![],
ts_build_info_file: None,
})
.expect("start action plan should be created");
let execution_plan =
create_start_execution_plan(&plan).expect("start execution plan should be created");
let watch = execution_plan.watch.as_ref().expect("watch plan");
let build_watch = watch.build_plan.watch.as_ref().expect("build watch plan");
let mut state = create_build_watch_state(build_watch).expect("watch state");
let kills = Rc::new(RefCell::new(Vec::new()));
let mut spawner = MockSpawner::new(kills.clone());
let mut child = spawner
.spawn(&execution_plan.command)
.expect("initial child should spawn");
write_file(&source_file, "fn main() { println!(\"changed\"); }\n");
let results = start_watch_tick(&execution_plan, &mut state, &mut child, &mut spawner)
.expect("watch tick should rebuild and restart");
assert_eq!(results.len(), 1);
assert!(results[0].changed);
assert_eq!(spawner.spawns.len(), 2);
assert_eq!(kills.borrow().as_slice(), &[1]);
assert_eq!(child.id, 2);
}
#[test]
fn start_executor_watch_spawn_failure_is_reported() {
let execution_plan = StartExecutionPlan {
command: cli::runners::RunnerCommand {
binary: "missing-cargo".to_string(),
prefix_args: Vec::new(),
command: "run".to_string(),
collect: false,
cwd: None,
shell: true,
env: Vec::new(),
},
warnings: Vec::new(),
watch: Some(StartWatchExecutionPlan {
build_plan: BuildExecutionPlan {
commands: Vec::new(),
warnings: Vec::new(),
projects: Vec::new(),
watch: Some(BuildWatchExecutionPlan {
poll_interval: std::time::Duration::from_millis(1),
projects: Vec::new(),
}),
},
kill_previous_process_on_success: true,
}),
};
let mut spawner = FailingSpawner;
let error = execute_start_watch_plan_with(&execution_plan, &mut spawner)
.unwrap_err()
.to_string();
assert!(error.contains("Failed to execute command"));
assert!(error.contains("failed to spawn process"));
}
#[test]
fn start_executor_watch_does_not_restart_when_rebuild_fails() {
let root = unique_temp_dir("nestrs-start-watch-build-fails");
write_file(
&root.join("Cargo.toml"),
r#"[package]
name = "watch-build-fails-fixture"
version = "0.1.0"
edition = "2024"
"#,
);
let source_file = root.join("src").join("main.rs");
write_file(&source_file, "fn main() {}\n");
let plan = create_start_action_plan(StartActionPlanRequest {
cwd: root,
configuration: Configuration::default(),
command_inputs: vec![],
command_options: vec![Input::new("watch", Some(InputValue::Bool(true)))],
extra_flags: vec![],
ts_build_info_file: None,
})
.expect("start action plan should be created");
let execution_plan =
create_start_execution_plan(&plan).expect("start execution plan should be created");
let watch = execution_plan.watch.as_ref().expect("watch plan");
let build_watch = watch.build_plan.watch.as_ref().expect("build watch plan");
let mut state = create_build_watch_state(build_watch).expect("watch state");
let kills = Rc::new(RefCell::new(Vec::new()));
let mut spawner = MockSpawner::new(kills.clone());
let mut child = spawner
.spawn(&execution_plan.command)
.expect("initial child should spawn");
write_file(&source_file, "fn main( {\n");
let error = start_watch_tick(&execution_plan, &mut state, &mut child, &mut spawner)
.expect_err("watch tick should report the failed rebuild")
.to_string();
assert!(error.contains("Failed to execute command"));
assert_eq!(spawner.spawns.len(), 1);
assert!(kills.borrow().is_empty());
assert_eq!(child.id, 1);
}
fn unique_temp_dir(name: &str) -> PathBuf {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let root = std::env::temp_dir().join(format!("{name}-{}-{timestamp}", std::process::id()));
if root.exists() {
fs::remove_dir_all(&root).expect("stale temp directory should be removable");
}
fs::create_dir_all(&root).expect("temp directory should be created");
root
}
fn write_file(path: &std::path::Path, content: &str) {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("parent directory should be created");
}
fs::write(path, content).expect("file should be written");
}
#[derive(Debug)]
struct MockChild {
id: usize,
kills: Rc<RefCell<Vec<usize>>>,
}
impl StartChildProcess for MockChild {
fn kill(&mut self) -> cli::Result<()> {
self.kills.borrow_mut().push(self.id);
Ok(())
}
}
#[derive(Debug)]
struct MockSpawner {
next_id: usize,
spawns: Vec<String>,
kills: Rc<RefCell<Vec<usize>>>,
}
impl MockSpawner {
fn new(kills: Rc<RefCell<Vec<usize>>>) -> Self {
Self {
next_id: 1,
spawns: Vec::new(),
kills,
}
}
}
impl StartProcessSpawner for MockSpawner {
type Child = MockChild;
fn spawn(&mut self, command: &cli::runners::RunnerCommand) -> cli::Result<Self::Child> {
self.spawns.push(command.raw_full_command());
let child = MockChild {
id: self.next_id,
kills: self.kills.clone(),
};
self.next_id += 1;
Ok(child)
}
}
#[derive(Debug)]
struct FailingSpawner;
impl StartProcessSpawner for FailingSpawner {
type Child = MockChild;
fn spawn(&mut self, command: &cli::runners::RunnerCommand) -> cli::Result<Self::Child> {
Err(CliError::RunnerFailed {
command: command.raw_full_command(),
reason: "failed to spawn process: test failure".to_string(),
})
}
}