use std::{path::PathBuf, process::Command, sync::OnceLock};
use interpretthis::{Interpreter, InterpreterConfig, InterpreterDeps, Tools};
fn vendored_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("integration")
.join("cpython_vendored")
}
fn host_python() -> Option<&'static str> {
static AVAILABLE: OnceLock<Option<&'static str>> = OnceLock::new();
*AVAILABLE.get_or_init(|| {
["python3.12", "python3"].into_iter().find(|candidate| {
Command::new(candidate).arg("--version").output().is_ok_and(|out| out.status.success())
})
})
}
#[derive(Debug, Clone, Copy)]
struct PassRate {
passed: u32,
total: u32,
}
fn parse_pass_line(stdout: &str) -> Option<PassRate> {
for line in stdout.lines().rev() {
let line = line.trim();
let Some(rest) = line.strip_suffix(" passed") else { continue };
let (passed_str, total_str) = rest.split_once('/')?;
let passed = passed_str.parse::<u32>().ok()?;
let total = total_str.parse::<u32>().ok()?;
if passed > total {
return None;
}
return Some(PassRate { passed, total });
}
None
}
fn run_host_python(binary: &str, source_path: &std::path::Path) -> Result<(String, bool), String> {
let output = Command::new(binary)
.arg(source_path)
.env("PYTHONHASHSEED", "0")
.env("PYTHONIOENCODING", "utf-8")
.env("LC_ALL", "C.UTF-8")
.output()
.map_err(|err| {
format!("failed to invoke host {binary} on {}: {err}", source_path.display())
})?;
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
Ok((stdout, output.status.success()))
}
async fn run_interpreter(source: &str) -> (String, Option<String>) {
let interp =
Interpreter::new(InterpreterDeps { tools: Tools::new() }, InterpreterConfig::default());
let response = interp.execute(source, &Tools::new(), std::collections::HashMap::new()).await;
let error_message = response.error.as_ref().map(|err| format!("{err:?}"));
(response.stdout, error_message)
}
struct VendoredReport {
summary: String,
failing_test_lines: Vec<String>,
}
async fn drive(file_name: &str) -> Result<Option<VendoredReport>, String> {
let path = vendored_dir().join(file_name);
if !path.exists() {
return Err(format!("vendored file not found: {}", path.display()));
}
let source = std::fs::read_to_string(&path)
.map_err(|err| format!("failed to read {}: {err}", path.display()))?;
if source.is_empty() {
return Err(format!("vendored file {file_name} is empty"));
}
let Some(binary) = host_python() else {
return Ok(None);
};
let (host_stdout, host_ok) = run_host_python(binary, &path)?;
if !host_ok {
return Err(format!(
"host {binary} exited non-zero on {file_name}; stdout:\n{host_stdout}"
));
}
let Some(host_rate) = parse_pass_line(&host_stdout) else {
return Err(format!(
"host {binary} did not emit an `X/Y passed` line for {file_name}; stdout was:\n{host_stdout}"
));
};
let (cat_stdout, cat_error) = run_interpreter(&source).await;
let cat_rate = parse_pass_line(&cat_stdout);
let cat_summary = match (cat_rate, cat_error.as_deref()) {
(Some(rate), _) => format!("{}/{} passed on interpretthis", rate.passed, rate.total),
(None, Some(err)) => {
format!("0/{} passed on interpretthis (interpreter errored: {err})", host_rate.total)
}
(None, None) => format!(
"0/{} passed on interpretthis (no pass-line in stdout; last line: {:?})",
host_rate.total,
cat_stdout.lines().last().unwrap_or("")
),
};
let summary = format!(
"cpython_imported::{file_name}: host={}/{} passed; {cat_summary}",
host_rate.passed, host_rate.total
);
let failing_test_lines =
cat_stdout.lines().filter(|line| line.starts_with("FAIL ")).map(str::to_owned).collect();
Ok(Some(VendoredReport { summary, failing_test_lines }))
}
fn render(file_name: &str, outcome: &Result<Option<VendoredReport>, String>) -> String {
match outcome {
Ok(Some(report)) => {
if report.failing_test_lines.is_empty() {
report.summary.clone()
} else {
let mut out = report.summary.clone();
for line in &report.failing_test_lines {
out.push_str("\n ");
out.push_str(line);
}
out
}
}
Ok(None) => {
format!("cpython_imported::{file_name}: SKIP (host python3 not available)")
}
Err(reason) => reason.clone(),
}
}
#[expect(
clippy::print_stdout,
reason = "informational metric for CPython-imported corpus; see module docstring"
)]
fn report(message: &str) {
println!("{message}");
}
#[tokio::test]
async fn cpython_imported_test_dict_adapted() {
let file_name = "test_dict_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_set_adapted() {
let file_name = "test_set_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_list_adapted() {
let file_name = "test_list_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_int_adapted() {
let file_name = "test_int_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_float_adapted() {
let file_name = "test_float_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_str_adapted() {
let file_name = "test_str_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}
#[tokio::test]
async fn cpython_imported_test_bytes_adapted() {
let file_name = "test_bytes_adapted.py";
let outcome = drive(file_name).await;
let message = render(file_name, &outcome);
assert!(outcome.is_ok(), "{message}");
report(&message);
}