use std::path::PathBuf;
use std::process::Command;
pub(crate) fn run_export(
test: String,
output: Option<PathBuf>,
package: Option<String>,
release: bool,
) -> Result<(), String> {
let bins = build_test_binaries(package.as_deref(), release)?;
if bins.is_empty() {
return Err("cargo build --tests produced no executable artifacts; \
ensure the workspace has at least one [[test]] target or \
a [lib]/[bin] with #[cfg(test)] tests"
.to_string());
}
let mut rejection_stderr: Option<String> = None;
let mut last_miss_stderr = String::new();
for bin in &bins {
let mut cmd = Command::new(bin);
cmd.arg(format!("--ktstr-export-test={test}"));
if let Some(o) = output.as_deref() {
let abs = if o.is_absolute() {
o.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| format!("resolve cwd for --output: {e}"))?
.join(o)
};
cmd.arg(format!("--ktstr-export-output={}", abs.display()));
}
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::inherit())
.stderr(std::process::Stdio::piped());
let out = cmd
.output()
.map_err(|e| format!("exec {}: {e}", bin.display()))?;
if out.status.success() {
std::io::Write::write_all(&mut std::io::stderr(), &out.stderr)
.map_err(|e| format!("forward winner stderr: {e}"))?;
return Ok(());
}
let stderr = String::from_utf8_lossy(&out.stderr).into_owned();
if out.status.code() == Some(2) {
if rejection_stderr.is_none() {
rejection_stderr = Some(stderr);
}
} else {
last_miss_stderr = stderr;
}
}
if let Some(reason) = rejection_stderr {
return Err(format!(
"test '{test}' is registered but cannot be exported:\n{}",
reason.trim_end(),
));
}
Err(format!(
"test '{test}' not found in any workspace test binary ({} candidates tried). \
Last stderr from a candidate:\n{}",
bins.len(),
last_miss_stderr.trim_end(),
))
}
fn build_test_binaries(package: Option<&str>, release: bool) -> Result<Vec<PathBuf>, String> {
let mut cmd = Command::new("cargo");
cmd.args(["build", "--tests", "--message-format=json"]);
if let Some(p) = package {
cmd.args(["--package", p]);
}
if release {
cmd.arg("--release");
}
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::inherit());
let out = cmd
.output()
.map_err(|e| format!("spawn cargo build --tests: {e}"))?;
if !out.status.success() {
return Err(format!(
"cargo build --tests failed (exit {})",
out.status.code().unwrap_or(-1),
));
}
let stdout = String::from_utf8_lossy(&out.stdout);
let mut bins: Vec<PathBuf> = Vec::new();
for line in stdout.lines() {
let Ok(msg) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if msg.get("reason").and_then(|r| r.as_str()) != Some("compiler-artifact") {
continue;
}
let Some(exe) = msg.get("executable").and_then(|e| e.as_str()) else {
continue;
};
let kinds: Vec<&str> = msg
.get("target")
.and_then(|t| t.get("kind"))
.and_then(|k| k.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let is_test_target = kinds.contains(&"test");
let is_unit_test = msg
.get("profile")
.and_then(|p| p.get("test"))
.and_then(|t| t.as_bool())
== Some(true);
if is_test_target || is_unit_test {
bins.push(PathBuf::from(exe));
}
}
bins.sort();
bins.dedup();
Ok(bins)
}