#![allow(dead_code)]
use std::ffi::OsStr;
use std::fmt::Display;
use std::path::Path;
use std::path::PathBuf;
use std::process;
use std::process::ExitStatus;
use assert_cmd::Command;
#[expect(
deprecated,
reason = "cargo_bin is deprecated, cargo_bin! is not, see https://github.com/rust-lang/rust/issues/148426"
)]
use assert_cmd::cargo::cargo_bin;
use temp_dir::TempDir;
use tytanic_utils::fs::TEMP_DIR_PREFIX;
use tytanic_utils::result::ResultEx;
#[derive(Debug)]
pub struct Environment {
dir: TempDir,
}
impl Environment {
pub fn new() -> Self {
Self {
dir: TempDir::with_prefix(TEMP_DIR_PREFIX).unwrap(),
}
}
pub fn default_package() -> Self {
let this = Self::new();
let fixture = PathBuf::from_iter([
std::env!("CARGO_MANIFEST_DIR"),
"..",
"..",
"assets",
"test-package",
]);
copy_dir(&fixture, this.root()).unwrap();
this
}
}
impl Environment {
pub fn root(&self) -> &Path {
self.dir.path()
}
pub fn persist(self) -> PathBuf {
let path = self.dir.path().to_path_buf();
self.dir.leak();
path
}
}
impl Environment {
pub fn run_tytanic_with<F>(&self, f: F) -> Run
where
F: FnOnce(&mut Command) -> &mut Command,
{
let mut cmd = Command::new(cargo_bin!("tt"));
cmd.current_dir(self.root());
f(&mut cmd);
let output = cmd.output().unwrap();
Run {
cmd,
output: Output::from_std_output(output, self.root()),
}
}
pub fn run_tytanic<I, T>(&self, args: I) -> Run
where
I: IntoIterator<Item = T>,
T: AsRef<OsStr>,
{
self.run_tytanic_with(|cmd| cmd.args(args))
}
pub fn run_tytanic_in<I, T, P>(&self, path: P, args: I) -> Run
where
P: AsRef<Path>,
I: IntoIterator<Item = T>,
T: AsRef<OsStr>,
{
self.run_tytanic_with(|cmd| cmd.current_dir(self.root().join(path)).args(args))
}
}
#[derive(Debug)]
pub struct Run {
cmd: Command,
output: Output,
}
impl Run {
pub fn cmd(&self) -> &Command {
&self.cmd
}
pub fn output(&self) -> &Output {
&self.output
}
}
#[derive(Debug)]
pub struct Output {
stdout: String,
stderr: String,
status: ExitStatus,
}
impl Output {
fn from_std_output(output: process::Output, dir: &Path) -> Self {
fn convert_bytes(bytes: Vec<u8>, dir: &str) -> String {
String::from_utf8(bytes)
.unwrap()
.replace("\u{1b}", "<ESC>")
.replace(r"C:\\", "/")
.replace(r"\\", "/")
.replace(r"C:\", "/")
.replace(r"\", "/")
.replace(dir, "<TEMP_DIR>")
}
let dir = dir
.as_os_str()
.to_str()
.unwrap()
.replace(r"C:\\", "/")
.replace(r"\\", "/")
.replace(r"C:\", "/")
.replace(r"\", "/");
Output {
stdout: convert_bytes(output.stdout, &dir),
stderr: convert_bytes(output.stderr, &dir),
status: output.status,
}
}
}
impl Output {
pub fn stdout(&self) -> &str {
&self.stdout
}
pub fn stderr(&self) -> &str {
&self.stderr
}
pub fn status(&self) -> ExitStatus {
self.status
}
}
impl Display for Output {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.status.code() {
Some(code) => writeln!(f, "--- CODE: {code}")?,
None => writeln!(f, "--- SIGNALED: This is most likely a bug!")?,
}
writeln!(f, "--- STDOUT:")?;
writeln!(f, "{}", self.stdout)?;
writeln!(f, "--- STDERR:")?;
writeln!(f, "{}", self.stderr)?;
writeln!(f, "--- END")?;
Ok(())
}
}
fn copy_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir(dst).ignore(|e| e.kind() == std::io::ErrorKind::AlreadyExists)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src = entry.path();
let dst = dst.join(entry.file_name());
if entry.file_type()?.is_dir() {
copy_dir(&src, &dst)?;
} else {
if !std::fs::exists(&dst)? {
std::fs::write(&dst, "")?;
}
std::fs::copy(&src, &dst)?;
}
}
Ok(())
}