use chrono::Local;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(1);
pub struct ArtifactWriter {
file: Mutex<File>,
path: PathBuf,
}
impl ArtifactWriter {
#[must_use]
pub fn new(crate_name: &str) -> Option<Self> {
let path = generate_artifact_path(crate_name).ok()?;
if let Some(parent) = path.parent()
&& let Err(e) = fs::create_dir_all(parent)
{
eprintln!(
"Warning: Failed to create artifact directory {}: {e}",
parent.display()
);
return None;
}
let file = match File::create(&path) {
Ok(f) => f,
Err(e) => {
eprintln!(
"Warning: Failed to create artifact file {}: {e}",
path.display()
);
return None;
}
};
Some(ArtifactWriter {
file: Mutex::new(file),
path,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Write for ArtifactWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let mut file = self.file.lock().unwrap();
file.write_all(buf)?;
io::stdout().write_all(buf)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
let mut file = self.file.lock().unwrap();
file.flush()?;
io::stdout().flush()
}
}
fn generate_artifact_path(crate_name: &str) -> io::Result<PathBuf> {
let workspace_root = find_workspace_root()?;
let artifacts_dir = workspace_root
.join("target")
.join("test-artifacts")
.join(crate_name);
let filename = generate_unique_filename(crate_name);
Ok(artifacts_dir.join(filename))
}
fn generate_unique_filename(crate_name: &str) -> String {
let timestamp = Local::now().format("%Y%m%dT%H%M%S%3f");
let pid = std::process::id();
let counter = COUNTER.fetch_add(1, Ordering::SeqCst);
format!("{crate_name}-{timestamp}-{pid}-{counter:03}.log")
}
fn find_workspace_root() -> io::Result<PathBuf> {
let current_dir = std::env::current_dir()?;
for ancestor in current_dir.ancestors() {
let cargo_toml = ancestor.join("Cargo.toml");
if cargo_toml.exists() {
if let Ok(contents) = std::fs::read_to_string(&cargo_toml)
&& contents.contains("[workspace]")
{
return Ok(ancestor.to_path_buf());
}
}
}
Ok(current_dir)
}
#[must_use]
pub fn maybe_writer(crate_name: &str) -> Option<ArtifactWriter> {
if std::env::var("SQRY_TEST_VERBOSE_ARTIFACTS").is_err() {
return None;
}
if let Some(writer) = ArtifactWriter::new(crate_name) {
eprintln!(
"Test artifacts will be written to: {}",
writer.path().display()
);
Some(writer)
} else {
eprintln!("Warning: Failed to create artifact writer for {crate_name}");
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
use std::io::Write;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_generate_unique_filename_format() {
let filename = generate_unique_filename("sqry-core");
assert!(filename.starts_with("sqry-core-"));
assert!(
std::path::Path::new(&filename)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("log"))
);
let pid = std::process::id();
assert!(filename.contains(&format!("-{pid}-")));
let parts: Vec<&str> = filename.rsplitn(2, '-').collect();
assert!(parts[0].len() >= 7); }
#[test]
fn test_generate_unique_filename_increments_counter() {
let name1 = generate_unique_filename("test");
let name2 = generate_unique_filename("test");
assert_ne!(name1, name2, "Filenames should be unique");
}
#[test]
fn test_maybe_writer_disabled_by_default() {
let _lock = ENV_MUTEX.lock().unwrap();
unsafe {
env::remove_var("SQRY_TEST_VERBOSE_ARTIFACTS");
}
let writer = maybe_writer("test-crate");
assert!(
writer.is_none(),
"Writer should be None when artifacts are disabled"
);
}
#[test]
fn test_maybe_writer_enabled_with_env() {
let _lock = ENV_MUTEX.lock().unwrap();
unsafe {
env::set_var("SQRY_TEST_VERBOSE_ARTIFACTS", "1");
}
let writer = maybe_writer("test-crate");
drop(writer);
unsafe {
env::remove_var("SQRY_TEST_VERBOSE_ARTIFACTS");
}
}
#[test]
fn test_artifact_writer_writes_to_file() {
let _lock = ENV_MUTEX.lock().unwrap();
let Some(mut writer) = ArtifactWriter::new("test-writer") else {
eprintln!("Skipping test: cannot create artifact writer in this environment");
return;
};
let test_data = b"Test log message\n";
let result = writer.write(test_data);
assert!(result.is_ok(), "Write should succeed");
assert_eq!(result.unwrap(), test_data.len());
assert!(writer.flush().is_ok(), "Flush should succeed");
assert!(
writer.path().exists(),
"Artifact file should exist at {:?}",
writer.path()
);
let _ = fs::remove_file(writer.path());
}
#[test]
fn test_find_workspace_root() {
let result = find_workspace_root();
assert!(result.is_ok(), "find_workspace_root should not fail");
}
#[test]
fn test_collision_resistance() {
let mut filenames = std::collections::HashSet::new();
for _ in 0..100 {
let name = generate_unique_filename("collision-test");
assert!(
filenames.insert(name.clone()),
"Collision detected: {name} already exists"
);
}
assert_eq!(filenames.len(), 100, "All 100 filenames should be unique");
}
}