use std::path::PathBuf;
use std::process::ExitCode;
use chrono::{SecondsFormat, Utc};
use smix_sdk::selftest::multi::{MultiSimStatus, MultiSimTarget, run_multi_selftest};
pub fn parse_target(arg: &str) -> Result<MultiSimTarget, String> {
let (udid, port) = arg
.split_once(':')
.ok_or_else(|| format!("expected `<UDID>:<PORT>`, got `{arg}`"))?;
let port: u16 = port
.parse()
.map_err(|e| format!("port `{port}` not u16: {e}"))?;
if udid.is_empty() {
return Err(format!("UDID empty in `{arg}`"));
}
Ok(MultiSimTarget {
udid: udid.to_string(),
runner_port: port,
})
}
pub async fn run(targets_argv: Vec<String>) -> ExitCode {
if targets_argv.is_empty() {
eprintln!(
"usage: smix selftest multi <UDID1>:<PORT1> <UDID2>:<PORT2> [...]\n\
(前置:每个 UDID 都用 `smix capsule up <UDID> --runner-port <PORT>` 起好)"
);
return ExitCode::from(64);
}
let mut targets = Vec::with_capacity(targets_argv.len());
for a in &targets_argv {
match parse_target(a) {
Ok(t) => targets.push(t),
Err(e) => {
eprintln!("smix selftest multi: arg parse error: {e}");
return ExitCode::from(64);
}
}
}
let run_id = Utc::now()
.to_rfc3339_opts(SecondsFormat::Secs, true)
.replace(':', "-");
let runs_root = PathBuf::from(format!(".smix/selftest-multi/{run_id}"));
eprintln!(
"smix selftest multi: spawning {} parallel sim selftest jobs (runs_root={})",
targets.len(),
runs_root.display()
);
for t in &targets {
eprintln!(" - {} @ port {}", t.udid, t.runner_port);
}
match run_multi_selftest(targets, runs_root.clone()).await {
Ok(summary) => {
eprintln!(
"smix selftest multi: summary — worst={:?}, pass={}, fail={}, runs={}",
summary.worst_status,
summary.pass_count,
summary.fail_count,
summary.runs.len()
);
for row in &summary.runs {
eprintln!(
" - {} → status={} coverage={:.1}% caps={} capsule_unattributed={}",
row.udid,
row.status,
row.coverage_pct,
row.capability_count,
row.capsule_unattributed
);
}
eprintln!(
"smix selftest multi: summary.json → {}",
runs_root.join("summary.json").display()
);
match summary.worst_status {
MultiSimStatus::AllPass => ExitCode::from(0),
MultiSimStatus::SomeFail => ExitCode::from(1),
}
}
Err(e) => {
eprintln!("smix selftest multi: run failed: {e}");
ExitCode::from(2)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_target_success() {
let t = parse_target("ABCD-1234:22087").unwrap();
assert_eq!(t.udid, "ABCD-1234");
assert_eq!(t.runner_port, 22087);
}
#[test]
fn parse_target_missing_colon() {
let e = parse_target("ABCD-1234").unwrap_err();
assert!(e.contains("expected `<UDID>:<PORT>`"), "got {e:?}");
}
#[test]
fn parse_target_port_not_u16() {
let e = parse_target("ABCD-1234:notaport").unwrap_err();
assert!(e.contains("not u16"), "got {e:?}");
}
#[test]
fn parse_target_empty_udid() {
let e = parse_target(":22087").unwrap_err();
assert!(e.contains("UDID empty"), "got {e:?}");
}
}