use std::path::Path;
use std::process::Stdio;
use tokio::process::Command;
use crate::error::Result;
#[derive(Debug, Clone)]
pub enum Verification {
FileContains(String),
FileEquals(String),
CompilesRust,
RustTestPasses {
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_rust(path, None).await,
Verification::RustTestPasses { test_src } => compile_rust(path, Some(test_src)).await,
}
}
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}")
}
}
}
}
async fn compile_rust(path: &Path, test_src: Option<&str>) -> Result<bool> {
let dir = tempfile::tempdir()?;
match test_src {
None => {
let status = Command::new("rustc")
.args(["--edition", "2021", "--crate-type", "lib", "--emit", "metadata"])
.arg("--out-dir")
.arg(dir.path())
.arg(path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await?;
Ok(status.success())
}
Some(test) => {
let base = tokio::fs::read_to_string(path).await.unwrap_or_default();
let combined = dir.path().join("combined.rs");
tokio::fs::write(&combined, format!("{base}\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());
}
}