use crate::util::*;
use anyhow::{bail, Result};
use std::process::Command;
use std::sync::OnceLock;
pub const CONTAINER_IP: &str = "127.0.0.1";
pub const CONTAINER_SSH_PORT: &str = "54320";
pub const CONTAINER_TCP_PORT: &str = "12999";
pub const CONTAINER_DYNAMIC_PORTS_START: &str = "49152";
pub const CONTAINER_DYNAMIC_PORTS_END: &str = "49154";
pub const CONTAINER_USER: &str = "userfoo";
pub const CONTAINER_HOME_DOWNLOAD_DIR: &str = "/home/userfoo/downloads";
const JUST_CONTAINER_STOP_RECIPE: &str = "d-stop";
const TEST_TMP_DIR: &str = "docker_mounted_tmp/";
static SETUP_ONCE: OnceLock<()> = OnceLock::new();
fn setup_test_container(command: &str, args: &[&str], include_ssh_keys: bool) -> StdoutStderr {
SETUP_ONCE.get_or_init(|| {
run_stop_container_recipe().expect("Failed running stop container recipe");
cleanup_mounted_tmp_dir().expect("failed cleaning mounted tmp dir");
});
let output = Command::new(command).args(args).output().unwrap();
if include_ssh_keys {
let out = run_just_cmd("d-setup-ssh-login", [""]).unwrap();
eprintln!(
"Include ssh keys (d-setup-ssh-login)\n===> STDOUT\n{}\n===> STDERR\n{}\n",
out.stdout, out.stderr
);
}
process_output_to_stdio_if_success(output).unwrap()
}
fn run_stop_container_recipe() -> Result<()> {
let output = Command::new("just")
.args([JUST_CONTAINER_STOP_RECIPE])
.output()?;
let StdoutStderr { stdout, stderr } = process_output_to_stdio_if_success(output)?;
eprintln!("===> Cleanup ({JUST_CONTAINER_STOP_RECIPE}) STDOUT:\n{stdout}\n");
eprintln!("===> Cleanup ({JUST_CONTAINER_STOP_RECIPE}) STDERR:\n{stderr}\n");
Ok(())
}
fn cleanup_mounted_tmp_dir() -> Result<()> {
let tmp_path = PathBuf::from(TEST_TMP_DIR);
assert!(
tmp_path.exists(),
"The tmp directory {tmp_path:?} that docker mounts does not exist!"
);
for res in fs::read_dir(tmp_path)? {
match res {
Ok(dir_entry) => match dir_entry.file_type() {
Ok(t) => {
let entry_path = dir_entry.path();
if t.is_file() {
fs::remove_file(entry_path)?;
} else if t.is_dir() {
fs::remove_dir_all(entry_path)?;
}
}
Err(e) => eprintln!("{e}"),
},
Err(e) => eprintln!("{e}"),
}
}
Ok(())
}
fn perform_cleanup() -> Result<()> {
let output = Command::new("just")
.args([JUST_CONTAINER_STOP_RECIPE])
.output()?;
let StdoutStderr { stdout, stderr } = process_output_to_stdio_if_success(output)?;
eprintln!("===> Cleanup ({JUST_CONTAINER_STOP_RECIPE}) STDOUT:\n{stdout}\n");
eprintln!("===> Cleanup ({JUST_CONTAINER_STOP_RECIPE}) STDERR:\n{stderr}\n");
cleanup_mounted_tmp_dir()?;
Ok(())
}
pub struct TestContainer {
pub stdout_stderr: StdoutStderr,
}
impl TestContainer {
pub fn setup(args: &str, include_ssh_keys: bool) -> Self {
use std::env;
if env::var_os("NEXTEST").is_some() {
let nex_test_exec_mode = env::var("NEXTEST_EXECUTION_MODE")
.expect("Environment variable NEXTEST_EXECUTION_MODE not set");
assert_eq!(nex_test_exec_mode,
"process-per-test",
"Expected 'NEXTEST_EXECUTION_MODE=process-per-test' but 'NEXTEST_EXECUTION_MODE={nex_test_exec_mode}', \
make sure container tests are run in single threaded mode, \
either by specifying 'NEXTEST_TEST_THREADS=1' or adding the '--no-capture' flag to 'nextest run ...'");
} else if let Some(tt) = env::var_os("RUST_TEST_THREADS") {
assert_eq!(
tt, "1",
"Running tests ussing the test container requires setting RUST_TEST_THREADS=1"
)
} else {
panic!("Running tests using the test container requires setting RUST_TEST_THREADS=1 or NEXTEST_TEST_THREADS=1 if using nextest");
}
let stdout_stderr = setup_test_container("just", &["d-run-with", args], include_ssh_keys);
Self { stdout_stderr }
}
#[allow(unused)]
pub fn stdout(&self) -> &str {
&self.stdout_stderr.stdout
}
#[allow(unused)]
pub fn stderr(&self) -> &str {
&self.stdout_stderr.stderr
}
}
impl Drop for TestContainer {
fn drop(&mut self) {
perform_cleanup().expect("Test container cleanup failed!");
}
}
pub fn run_just_cmd<I, S>(recipe: &str, args: I) -> Result<StdoutStderr>
where
I: IntoIterator<Item = S> + Send + 'static + Debug,
S: ToOwned + AsRef<std::ffi::OsStr>,
String: FromIterator<S>,
{
let mut cmd = Command::new("just");
cmd.arg(recipe);
let args_str: String = args.into_iter().collect();
if !args_str.is_empty() {
cmd.arg(args_str);
}
let output = cmd.output()?;
process_output_to_stdio_if_success(output)
}
fn check_and_relocate_path(original_path: &Path) -> Result<PathBuf> {
let container_home_download_dir = Path::new(CONTAINER_HOME_DOWNLOAD_DIR);
let test_tmp_dir = Path::new(TEST_TMP_DIR);
if original_path.starts_with(container_home_download_dir) {
let remainder = original_path
.strip_prefix(container_home_download_dir)
.unwrap();
let new_path = test_tmp_dir.join(remainder);
assert!(new_path.exists(), "Path does not exist: {new_path:?}");
Ok(new_path)
} else {
bail!("Path does not start with {CONTAINER_HOME_DOWNLOAD_DIR}: {original_path:?}");
}
}
pub fn assert_file_exists_in_container(path: &str) -> Result<PathBuf> {
let tmp_dir = PathBuf::from(TEST_TMP_DIR);
assert!(tmp_dir.exists() && tmp_dir.is_dir());
let p = PathBuf::from(path);
check_and_relocate_path(&p)
}
pub fn get_docker_logs() -> Result<StdoutStderr> {
run_just_cmd("d-logs", [""])
}
pub fn eprint_docker_logs() -> Result<()> {
let StdoutStderr { stdout, stderr } = get_docker_logs()?;
eprintln!("====== DOCKER LOGS ======:\n===> STDOUT\n{stdout}\n===> STDERR\n{stderr}\n^^^^^^^^^^^^^^^^^^^^^^^^\n DOCKER LOGS\n\n",);
Ok(())
}
pub fn eprint_cmd_args_stderr_stdout_formatted(args: &[&str], stdout: &str, stderr: &str) {
eprintln!("=== COMMAND ARGUMENTS ===\n{args:?}\n");
eprintln!("=== COMMAND STDOUT ===\n{stdout}\n^^^COMMAND STDOUT^^^\n");
eprintln!("=== COMMAND STDERR ===\n{stderr}\n^^^COMMAND STDERR^^^\n");
}