// rocie - An enterprise grocery management system // // Copyright (C) 2025 Benedikt Peetz // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use std::{ env, ffi::OsStr, fmt::Write, fs, io::{self, BufRead, BufReader}, mem, path::{Path, PathBuf}, process::{self, Stdio}, }; use rocie_client::{ apis::{api_set_no_auth_user_api::provision, configuration::Configuration}, models::{ProvisionInfo, UserStub}, }; use crate::{ _testenv::{Paths, log::request}, testenv::TestEnv, }; macro_rules! function_name { () => {{ fn f() {} fn type_name_of(_: T) -> &'static str { std::any::type_name::() } let name = type_name_of(f); name.strip_suffix("::{{closure}}::f").unwrap() }}; } pub(crate) use function_name; fn target_dir() -> PathBuf { // Tests exe is in target/debug/deps, the *rocie-server* exe is in target/debug env::current_exe() .expect("./target/debug/deps/rocie-server-*") .parent() .expect("./target/debug/deps") .parent() .expect("./target/debug") .parent() .expect("./target") .to_path_buf() } fn test_dir(name: &'static str) -> PathBuf { target_dir().join("tests").join(name) } fn prepare_files_and_dirs(test_dir: &Path) -> io::Result { fs::create_dir_all(test_dir)?; let db_path = test_dir.join("database.sqlite"); { // Remove all files, so that the test run stays pure for entry in fs::read_dir(test_dir).unwrap() { let entry = entry.unwrap(); let entry_ft = entry.file_type().unwrap(); if entry_ft.is_dir() { fs::remove_dir_all(entry.path())?; } else if entry_ft.is_file() { fs::remove_file(entry.path())?; } else { panic!("Unknown file: {} ({entry_ft:#?})", entry.path().display()); } } } Ok(Paths { db: db_path, test_dir: test_dir.to_owned(), }) } fn find_server_exe() -> PathBuf { let target = target_dir(); let prefixed_target = target.join(env!("TARGET")); let target = if prefixed_target.join("debug").exists() { prefixed_target.join("debug") } else if prefixed_target.join("release").exists() { prefixed_target.join("release") } else if target.join("debug").exists() { target.join("debug") } else if target.join("release").exists() { target.join("release") } else { panic!("Failed to find directory for rocie-server binary."); }; let exe_name = if cfg!(windows) { "rocie-server.exe" } else { "rocie-server" }; let final_path = target.join(exe_name); println!( "TESTENV: Assuming `rocie-server` binary is at: `{}`", final_path.display() ); final_path } fn rocie_base_path(port: &str) -> String { format!("http://127.0.0.1:{port}") } fn rocie_server_args(paths: &Paths) -> [&OsStr; 4] { [ OsStr::new("serve"), OsStr::new("--db-path"), paths.db.as_os_str(), OsStr::new("--print-port"), ] } impl TestEnv { pub(crate) async fn new(name: &'static str) -> TestEnv { let env = Self::new_no_login(name); request!( env, provision(ProvisionInfo { user: UserStub { description: Some("Test user, used during test runs".to_string()), name: "rocie".to_string(), password: "server".to_string() }, // Don't use any default units. // Otherwise we would need to update the tests every time we add new ones. use_defaults: false, }) ); env } pub(crate) fn new_no_login(name: &'static str) -> TestEnv { let test_dir = test_dir(name); let paths = prepare_files_and_dirs(&test_dir) .inspect_err(|err| panic!("Error during test dir preparation: {err}")) .unwrap(); let (server_process, port) = { let server_exe = find_server_exe(); let mut cmd = process::Command::new(&server_exe); cmd.current_dir(&paths.test_dir); cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); cmd.args(rocie_server_args(&paths)); let mut child = cmd.spawn().expect("server spawn"); let port: u16 = { let mut stdout = BufReader::new(child.stdout.as_mut().expect("Was captured")); let mut port = String::new(); assert_ne!( stdout.read_line(&mut port).expect("Works"), 0, "We should have been able to read the one line, the server printed" ); port.trim_end() .parse() .expect("The server should reply with a u16 port number") }; (child, port) }; let config = { let mut inner = Configuration::new(); inner.base_path = rocie_base_path(port.to_string().as_str()); inner.user_agent = Some(String::from("Rocie test driver")); inner }; let me = TestEnv { name, test_dir, paths, server_process: Some(server_process), config, port: port.to_string(), }; me.log(format!("Starting test `{name}` on port `{port}`")); me } } impl Drop for TestEnv { fn drop(&mut self) { /// Format an error message for when the server did not exit successfully. fn format_exit(args: &[&OsStr], output: &process::Output) -> String { let mut base = String::new(); { let args = args .iter() .map(|s| s.to_str().unwrap()) .collect::>() .join(" "); if output.status.success() { writeln!(base, "`rocie-server {args}` did exit successfully.") .expect("In-memory"); } else { writeln!(base, "`rocie-server {args}` did not exit successfully.") .expect("In-memory"); } } let stdout = String::from_utf8_lossy(&output.stdout); let stderr = String::from_utf8_lossy(&output.stderr); if !stdout.is_empty() { writeln!(base, "Stdout:\n---\n{stdout}\n---").expect("In-memory"); } if !stderr.is_empty() { writeln!(base, "Stderr:\n---\n{stderr}\n---").expect("In-memory"); } base } { // Stop the server process via SIGTERM. let mut kill = process::Command::new("kill") .args([ "-s", "TERM", &self.server_process.as_ref().unwrap().id().to_string(), ]) .spawn() .unwrap(); eprintln!("Killing the server process"); kill.wait().unwrap(); } let output = mem::take(&mut self.server_process) .expect("Is some at this point") .wait_with_output() .expect("server exit output"); eprintln!( "{}", format_exit(rocie_server_args(&self.paths).as_slice(), &output) ); } }