#![allow(dead_code)]
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defnix")]
pub struct NixProgramSpec {
pub source: String,
pub expected_json: String,
#[serde(default)]
pub expected_error: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub skip: bool,
#[serde(default)]
pub note: String,
}
#[must_use]
pub fn load_corpus() -> Vec<tatara_lisp::NamedDefinition<NixProgramSpec>> {
let corpus_dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("oracle_corpus");
let mut paths: Vec<PathBuf> = std::fs::read_dir(&corpus_dir)
.unwrap_or_else(|e| panic!("corpus dir {}: {e}", corpus_dir.display()))
.filter_map(|e| e.ok().map(|d| d.path()))
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("lisp"))
.collect();
paths.sort();
let mut out = Vec::new();
for path in paths {
let src = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let mut defs = tatara_lisp::compile_named::<NixProgramSpec>(&src)
.unwrap_or_else(|e| panic!("compile {}: {e}", path.display()));
for def in &mut defs {
def.name = format!(
"{}::{}",
path.file_stem().and_then(|s| s.to_str()).unwrap_or("?"),
def.name
);
}
out.extend(defs);
}
out
}
#[must_use]
pub fn value_to_json(v: &sui_eval::Value) -> serde_json::Value {
v.to_json()
}
pub fn online_mode() -> bool {
env::var("SUI_TEST_ONLINE")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
}
pub fn nix_available() -> bool {
which("nix-instantiate").is_some()
}
pub fn nix_store_available() -> bool {
which("nix-store").is_some()
}
pub fn skip_if_offline(test_name: &str) -> bool {
if !online_mode() {
eprintln!("skip {test_name}: SUI_TEST_ONLINE not set");
return true;
}
if !nix_available() {
eprintln!("skip {test_name}: nix-instantiate not on PATH");
return true;
}
false
}
fn which(cmd: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
for dir in env::split_paths(&path) {
let candidate = dir.join(cmd);
if candidate.is_file() {
return Some(candidate);
}
}
None
}
pub fn nix_eval_json(expr: &str) -> serde_json::Value {
let wrapped;
let passed: &str = if expr.trim_start().starts_with('-') {
wrapped = format!("({expr})");
&wrapped
} else {
expr
};
run_nix_instantiate(&["--eval", "--json", "--strict", "-E", passed])
}
pub fn nix_eval_json_file(path: &Path) -> serde_json::Value {
run_nix_instantiate(&["--eval", "--json", "--strict", &path.to_string_lossy()])
}
fn run_nix_instantiate(args: &[&str]) -> serde_json::Value {
let output = match Command::new("nix-instantiate").args(args).output() {
Ok(o) => o,
Err(e) => return error_json(format!("spawn nix-instantiate failed: {e}")),
};
if !output.status.success() {
return error_json(String::from_utf8_lossy(&output.stderr).into_owned());
}
let stdout = String::from_utf8_lossy(&output.stdout);
match serde_json::from_str(stdout.trim()) {
Ok(v) => v,
Err(e) => error_json(format!("nix stdout not json: {e}: {stdout}")),
}
}
pub fn sui_eval_json(expr: &str) -> serde_json::Value {
match sui_eval::eval(expr) {
Ok(v) => v.to_json(),
Err(e) => error_json(format!("{e}")),
}
}
pub fn sui_eval_json_file(path: &Path) -> serde_json::Value {
let source = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => return error_json(format!("read {}: {e}", path.display())),
};
match sui_eval::eval_with_file(&source, Some(path.to_path_buf())) {
Ok(v) => v.to_json(),
Err(e) => error_json(format!("{e}")),
}
}
fn error_json(msg: impl Into<String>) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert(
"__error".to_string(),
serde_json::Value::String(msg.into()),
);
serde_json::Value::Object(map)
}
pub fn is_error_json(v: &serde_json::Value) -> bool {
v.get("__error").is_some()
}
pub fn assert_eq_nix(expr: &str) {
if skip_if_offline("assert_eq_nix") {
return;
}
let oracle = nix_eval_json(expr);
let ours = sui_eval_json(expr);
if oracle != ours {
let oracle_s = serde_json::to_string(&oracle).unwrap_or_default();
let ours_s = serde_json::to_string(&ours).unwrap_or_default();
panic!(
"differential mismatch on {expr:?}\n nix: {oracle_s}\n sui: {ours_s}"
);
}
}
pub fn assert_eq_nix_file(path: &Path) {
if skip_if_offline("assert_eq_nix_file") {
return;
}
let oracle = nix_eval_json_file(path);
let ours = sui_eval_json_file(path);
if oracle != ours {
let oracle_s = serde_json::to_string(&oracle).unwrap_or_default();
let ours_s = serde_json::to_string(&ours).unwrap_or_default();
panic!(
"differential mismatch on {}\n nix: {oracle_s}\n sui: {ours_s}",
path.display()
);
}
}
pub fn pleme_io_root() -> PathBuf {
if let Ok(v) = env::var("PLEME_IO_ROOT") {
return PathBuf::from(v);
}
let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home).join("code/github/pleme-io")
}
pub fn nix_store_root() -> PathBuf {
env::var("NIX_STORE_ROOT")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/nix/store"))
}
pub fn pleme_io_flake_nix_sample(n: usize) -> Vec<PathBuf> {
collect_files_by_name(&pleme_io_root(), "flake.nix", n, 3)
}
pub fn pleme_io_flake_lock_sample(n: usize) -> Vec<PathBuf> {
collect_files_by_name(&pleme_io_root(), "flake.lock", n, 3)
}
pub fn nix_store_drv_sample(n: usize) -> Vec<PathBuf> {
let root = nix_store_root();
if !root.is_dir() {
return Vec::new();
}
let mut out: Vec<PathBuf> = match std::fs::read_dir(&root) {
Ok(it) => it
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) == Some("drv"))
.collect(),
Err(_) => return Vec::new(),
};
out.sort();
out.truncate(n);
out
}
pub fn nix_store_path_sample(n: usize) -> Vec<PathBuf> {
let root = nix_store_root();
if !root.is_dir() {
return Vec::new();
}
let mut out: Vec<PathBuf> = match std::fs::read_dir(&root) {
Ok(it) => it
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().and_then(|s| s.to_str()) != Some("drv"))
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| !n.starts_with('.') && n != ".links" && n != ".db")
.unwrap_or(false)
})
.collect(),
Err(_) => return Vec::new(),
};
out.sort();
out.truncate(n);
out
}
fn collect_files_by_name(
root: &Path,
file_name: &str,
limit: usize,
max_depth: usize,
) -> Vec<PathBuf> {
if !root.is_dir() {
return Vec::new();
}
let mut out = Vec::new();
walk(root, file_name, max_depth, &mut out);
out.sort();
out.truncate(limit);
out
}
fn walk(dir: &Path, file_name: &str, depth_remaining: usize, out: &mut Vec<PathBuf>) {
const SKIP_DIRS: &[&str] = &[
".git",
"target",
"node_modules",
"result",
"dist",
"build",
".direnv",
];
let entries = match std::fs::read_dir(dir) {
Ok(it) => it,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
let name = match entry.file_name().into_string() {
Ok(s) => s,
Err(_) => continue,
};
if path.is_file() && name == file_name {
out.push(path);
continue;
}
if path.is_dir() && depth_remaining > 0 {
if name.starts_with('.') && name != "." {
continue;
}
if SKIP_DIRS.contains(&name.as_str()) || name.starts_with("result-") {
continue;
}
walk(&path, file_name, depth_remaining - 1, out);
}
}
}
#[cfg(test)]
mod sanity {
use super::*;
#[test]
fn online_mode_defaults_off() {
if env::var("SUI_TEST_ONLINE").is_ok() {
return;
}
assert!(!online_mode());
}
#[test]
fn pleme_io_root_is_resolvable_even_if_missing() {
let _ = pleme_io_root();
}
#[test]
fn nix_store_root_defaults() {
let root = nix_store_root();
assert!(!root.as_os_str().is_empty());
}
}