use std::fs; use std::ops::Deref; use std::path::{Path, PathBuf}; use std::process::Command; use std::sync::{Arc, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{Result, ensure}; use crate::highlight::Cache; /// A fresh directory under the system temp dir, self-removes on drop. pub struct TempDir(PathBuf); impl TempDir { pub fn new(label: &str) -> Self { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("clock before epoch") .as_nanos(); let path = std::env::temp_dir().join(format!( "sorcery-{label}-{}-{nanos}", std::process::id(), )); fs::create_dir_all(&path).expect("creating temp dir"); TempDir(path) } } impl Deref for TempDir { type Target = Path; fn deref(&self) -> &Path { &self.0 } } impl Drop for TempDir { fn drop(&mut self) { let _ = fs::remove_dir_all(&self.0); } } /// Runs git in `repo` isolated from the developer's own config (signing, /// hooks, templates) and returns trimmed stdout. pub fn git(repo: &Path, args: &[&str]) -> Result { let output = Command::new("git") .current_dir(repo) .args(args) .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_AUTHOR_NAME", "test") .env("GIT_AUTHOR_EMAIL", "test@example") .env("GIT_COMMITTER_NAME", "test") .env("GIT_COMMITTER_EMAIL", "test@example") .output()?; ensure!( output.status.success(), "git {}: {}", args.join(" "), String::from_utf8_lossy(&output.stderr), ); Ok(String::from_utf8(output.stdout)?.trim().to_owned()) } pub fn init_repo(repo: &Path) -> Result<()> { fs::create_dir_all(repo)?; git(repo, &["init", "-q", "-b", "main"])?; Ok(()) } pub fn init_sha256_repo(repo: &Path) -> Result<()> { fs::create_dir_all(repo)?; git( repo, &["init", "-q", "-b", "main", "--object-format=sha256"], )?; Ok(()) } /// Stages everything and commits it, returning the new commit's hex id. pub fn commit(repo: &Path, message: &str) -> Result { git(repo, &["add", "-A"])?; git(repo, &["commit", "-qm", message])?; git(repo, &["rev-parse", "HEAD"]) } /// `Cache::new` claims a process-wide slot, so all tests share one. pub fn grammar_cache() -> Arc { static CACHE: OnceLock> = OnceLock::new(); CACHE .get_or_init(|| Arc::new(Cache::new(std::env::temp_dir().join("sorcery-test-grammars")))) .clone() }