// yt - A fully featured command line YouTube client // // Copyright (C) 2025 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Yt. // // 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, mem, path::{Path, PathBuf}, process::{self, Stdio}, thread::sleep, time::Duration, }; use rocie_client::apis::configuration::Configuration; use crate::{_testenv::Paths, testenv::TestEnv}; 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, port: u32) -> PathBuf { target_dir().join("tests").join(name).join(port.to_string()) } 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().join("debug"); let exe_name = if cfg!(windows) { "rocie-server.exe" } else { "rocie-server" }; target.join(exe_name) } fn rocie_base_path(port: &str) -> String { format!("http://127.0.0.1:{port}") } fn rocie_server_args<'a>(paths: &'a Paths, port: &'a str) -> [&'a OsStr; 5] { [ OsStr::new("serve"), OsStr::new("--db-path"), paths.db.as_os_str(), OsStr::new("--port"), OsStr::new(port), ] } impl TestEnv { pub(crate) fn new(name: &'static str, port: u32) -> TestEnv { let test_dir = test_dir(name, port); let paths = prepare_files_and_dirs(&test_dir) .inspect_err(|err| panic!("Error during test dir preparation: {err}")) .unwrap(); let server_process = { 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, port.to_string().as_str())); let child = cmd.spawn().expect("server spawn"); // Give the server time to start. // TODO(@bpeetz): Use a better synchronization primitive <2025-09-11> sleep(Duration::from_millis(240)); child }; 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, &self.port).as_slice(), &output ) ); } }