use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command;
use crate::error::{Error, Result};
#[derive(Debug, Clone)]
pub enum Verification {
FileContains(String),
FileEquals(String),
CompilesRust,
RustTestPasses {
test_src: String,
},
EachCompilesRust(Vec<PathBuf>),
WorkspaceTestPasses {
files: Vec<PathBuf>,
test_src: String,
},
}
impl Verification {
pub async fn passes(&self, _path: &Path, contents: &str) -> Result<bool> {
match self {
Verification::FileContains(needle) => Ok(contents.contains(needle)),
Verification::FileEquals(expected) => Ok(contents == expected),
Verification::CompilesRust => compile_source(contents, None).await,
Verification::RustTestPasses { test_src } => {
compile_source(contents, Some(test_src)).await
}
Verification::EachCompilesRust(_) | Verification::WorkspaceTestPasses { .. } => Err(
Error::Config("multi-file verification requires a workspace root".into()),
),
}
}
pub async fn passes_in(&self, root: &Path) -> Result<bool> {
match self {
Verification::EachCompilesRust(files) => {
for f in files {
let src = tokio::fs::read_to_string(root.join(f)).await.unwrap_or_default();
if !compile_source(&src, None).await? {
return Ok(false);
}
}
Ok(true)
}
Verification::WorkspaceTestPasses { files, test_src } => {
let mut combined = String::new();
for f in files {
let src = tokio::fs::read_to_string(root.join(f)).await.unwrap_or_default();
combined.push_str(&src);
combined.push('\n');
}
compile_source(&combined, Some(test_src)).await
}
_ => Err(Error::Config(
"single-file verification used in workspace mode".into(),
)),
}
}
pub fn describe(&self) -> String {
match self {
Verification::FileContains(needle) => {
format!("the file must contain exactly this text: {needle:?}")
}
Verification::FileEquals(expected) => {
format!("the file's entire contents must equal exactly: {expected:?}")
}
Verification::CompilesRust => {
"the file must compile as Rust (rustc --crate-type lib)".to_string()
}
Verification::RustTestPasses { test_src } => {
format!("the file must compile and pass this test:\n{test_src}")
}
Verification::EachCompilesRust(files) => {
format!("each of these files must compile as Rust: {files:?}")
}
Verification::WorkspaceTestPasses { files, test_src } => format!(
"these files {files:?} must together compile and pass this test:\n{test_src}"
),
}
}
}
async fn compile_source(source: &str, test_src: Option<&str>) -> Result<bool> {
let dir = tempfile::tempdir()?;
match test_src {
None => {
let lib = dir.path().join("lib.rs");
tokio::fs::write(&lib, source).await?;
let status = Command::new("rustc")
.args(["--edition", "2021", "--crate-type", "lib", "--emit", "metadata"])
.arg("--out-dir")
.arg(dir.path())
.arg(&lib)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
Ok(status.success())
}
Some(test) => {
let combined = dir.path().join("combined.rs");
tokio::fs::write(&combined, format!("{source}\n{test}\n")).await?;
let bin = dir.path().join("t");
let built = Command::new("rustc")
.args(["--edition", "2021", "--test"])
.arg(&combined)
.arg("-o")
.arg(&bin)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
if !built.success() {
return Ok(false);
}
let run = Command::new(&bin)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
Ok(run.success())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
async fn passes(v: &Verification, contents: &str) -> bool {
v.passes(&PathBuf::from("unused"), contents).await.unwrap()
}
#[tokio::test]
async fn contains_passes_and_fails() {
let v = Verification::FileContains("fn hello".into());
assert!(passes(&v, "pub fn hello() {}").await);
assert!(!passes(&v, "pub fn world() {}").await);
}
#[tokio::test]
async fn equals_is_exact() {
let v = Verification::FileEquals("a".into());
assert!(passes(&v, "a").await);
assert!(!passes(&v, "a ").await);
}
#[tokio::test]
async fn compiles_rust_rejects_stub_accepts_real() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.rs");
tokio::fs::write(&file, "fn hello").await.unwrap();
assert!(!Verification::CompilesRust.passes(&file, "fn hello").await.unwrap());
let good = "pub fn hello() -> u32 { 42 }\n";
tokio::fs::write(&file, good).await.unwrap();
assert!(Verification::CompilesRust.passes(&file, good).await.unwrap());
}
#[tokio::test]
async fn rust_test_passes_only_when_test_passes() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("hello.rs");
let good = "pub fn hello() -> u32 { 42 }\n";
tokio::fs::write(&file, good).await.unwrap();
let ok = Verification::RustTestPasses {
test_src: "#[test] fn t() { assert_eq!(hello(), 42); }".into(),
};
assert!(ok.passes(&file, good).await.unwrap());
let bad = Verification::RustTestPasses {
test_src: "#[test] fn t() { assert_eq!(hello(), 41); }".into(),
};
assert!(!bad.passes(&file, good).await.unwrap());
}
#[tokio::test]
async fn each_compiles_rust_fails_if_any_file_fails() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("a.rs"), "pub fn a() -> u32 { 1 }\n").unwrap();
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 2 }\n").unwrap();
let v = Verification::EachCompilesRust(vec!["a.rs".into(), "b.rs".into()]);
assert!(v.passes_in(root).await.unwrap());
std::fs::write(root.join("b.rs"), "pub fn b").unwrap();
assert!(!v.passes_in(root).await.unwrap());
}
#[tokio::test]
async fn workspace_test_passes_only_when_files_work_together() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path();
std::fs::write(root.join("a.rs"), "pub fn a() -> u32 { 40 }\n").unwrap();
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 2 }\n").unwrap();
let v = Verification::WorkspaceTestPasses {
files: vec!["a.rs".into(), "b.rs".into()],
test_src: "#[test] fn t() { assert_eq!(a() + b(), 42); }".into(),
};
assert!(v.passes_in(root).await.unwrap());
std::fs::write(root.join("b.rs"), "pub fn b() -> u32 { 99 }\n").unwrap();
assert!(!v.passes_in(root).await.unwrap());
}
#[tokio::test]
async fn multi_file_variant_errors_in_single_file_mode() {
let v = Verification::EachCompilesRust(vec!["a.rs".into()]);
assert!(v.passes(&PathBuf::from("unused"), "").await.is_err());
}
}