use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use sha2::{Digest, Sha256};
const SETUP: &str = "scripts/dev/test.sh setup (on Windows: python scripts\\setup_dev.py)";
fn repo_root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.find(|dir| dir.join("scripts/generate_sample_data.py").exists())
.expect("the repository holding scripts/generate_sample_data.py")
.to_path_buf()
}
pub fn sample_data_dir() -> PathBuf {
ensure_sample_data();
repo_root().join("tests/sample-data")
}
fn inputs_digest(root: &Path) -> String {
let mut hasher = Sha256::new();
for input in [
"scripts/generate_sample_data.py",
"scripts/requirements-fixtures.txt",
] {
let bytes =
std::fs::read(root.join(input)).unwrap_or_else(|e| panic!("reading {input}: {e}"));
hasher.update(&bytes);
}
hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}
fn is_current(dir: &Path, digest: &str) -> bool {
std::fs::read_to_string(dir.join(".generated"))
.is_ok_and(|stamp| stamp.lines().next().map(str::trim) == Some(digest))
}
pub fn ensure_sample_data() {
static READY: OnceLock<Result<(), String>> = OnceLock::new();
if let Err(message) = READY.get_or_init(prepare) {
panic!("{message}");
}
}
fn prepare() -> Result<(), String> {
let root = repo_root();
let dir = root.join("tests/sample-data");
let digest = inputs_digest(&root);
if is_current(&dir, &digest) {
return Ok(());
}
generate(&root, &dir).map_err(|detail| {
format!(
"Could not generate the test fixtures in {}.\n\
Run {SETUP} to set up .venv and generate them.\n\n{detail}",
dir.display()
)
})
}
fn generate(root: &Path, dir: &Path) -> Result<(), String> {
eprintln!(
"{} is missing or out of date; generating it...",
dir.display()
);
let venv_python = if cfg!(windows) {
root.join(".venv/Scripts/python.exe")
} else {
root.join(".venv/bin/python")
};
let pythons: Vec<OsString> = if venv_python.exists() {
vec![venv_python.into()]
} else {
vec!["python3".into(), "python".into()]
};
let output = pythons
.iter()
.find_map(|python| {
Command::new(python)
.arg(root.join("scripts/generate_sample_data.py"))
.arg("--out")
.arg(dir)
.arg("--if-stale")
.output()
.ok()
})
.ok_or("No Python found.")?;
if !output.status.success() {
return Err(format!(
"generate_sample_data.py exited with {}:\n{}",
output.status,
String::from_utf8_lossy(&output.stderr).trim_end()
));
}
eprintln!("Generated {}.", dir.display());
Ok(())
}
pub fn buffer_lines(buf: &ratatui::buffer::Buffer) -> Vec<String> {
let area = buf.area;
(area.top()..area.bottom())
.map(|y| {
(area.left()..area.right())
.map(|x| buf[(x, y)].symbol())
.collect()
})
.collect()
}
pub fn buffer_text(buf: &ratatui::buffer::Buffer) -> String {
buffer_lines(buf).join("\n")
}
pub fn test_runtime() -> tokio::runtime::Handle {
super::isolate_cache();
static RT: std::sync::OnceLock<tokio::runtime::Runtime> = std::sync::OnceLock::new();
RT.get_or_init(|| {
tokio::runtime::Builder::new_multi_thread()
.worker_threads(1)
.enable_all()
.build()
.expect("test tokio runtime")
})
.handle()
.clone()
}