use std::fs;
use std::path::{Path, PathBuf};
use chrono::Utc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MultiSimStatus {
AllPass,
SomeFail,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerSimRow {
pub udid: String,
pub status: String,
pub coverage_pct: f64,
pub capability_count: usize,
pub capsule_unattributed: i64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MultiSimSummary {
pub schema_version: u32,
pub worst_status: MultiSimStatus,
pub pass_count: usize,
pub fail_count: usize,
pub runs: Vec<PerSimRow>,
}
pub struct PerSimInput<'a> {
pub udid: &'a str,
pub result_json_path: &'a Path,
}
pub fn aggregate_results(inputs: &[PerSimInput]) -> MultiSimSummary {
let runs: Vec<PerSimRow> = inputs.iter().map(judge_one).collect();
let pass_count = runs.iter().filter(|r| r.status == "passed").count();
let fail_count = runs.len() - pass_count;
let worst_status = if fail_count == 0 {
MultiSimStatus::AllPass
} else {
MultiSimStatus::SomeFail
};
MultiSimSummary {
schema_version: 1,
worst_status,
pass_count,
fail_count,
runs,
}
}
fn judge_one(input: &PerSimInput) -> PerSimRow {
let body = match fs::read_to_string(input.result_json_path) {
Ok(b) => b,
Err(_) => return missing_row(input.udid),
};
let v: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(_) => return missing_row(input.udid),
};
let coverage_pct = v["coverage_pct"].as_f64().unwrap_or(0.0);
let capability_count = v["capabilities"].as_array().map(|a| a.len()).unwrap_or(0);
let capsule_unattributed = v["capsule_reconcile"]["unattributed_count"]
.as_i64()
.unwrap_or(-1);
let bad_cap = v["capabilities"]
.as_array()
.map(|caps| {
caps.iter().any(|c| {
let s = c["status"].as_str().unwrap_or("");
s != "passed" && s != "c2_deferred" && s != "skipped"
})
})
.unwrap_or(false);
let bad_spec = v["selector_spec_segments"]
.as_array()
.map(|specs| {
specs
.iter()
.any(|c| c["status"].as_str().unwrap_or("") != "passed")
})
.unwrap_or(false);
let unattr_max = std::env::var("SMIX_CAPSULE_UNATTR_MAX")
.ok()
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let status = if bad_cap || bad_spec {
"failed_capability"
} else if capsule_unattributed > unattr_max {
"capsule_unattributed"
} else {
"passed"
};
PerSimRow {
udid: input.udid.to_string(),
status: status.to_string(),
coverage_pct,
capability_count,
capsule_unattributed,
}
}
fn missing_row(udid: &str) -> PerSimRow {
PerSimRow {
udid: udid.to_string(),
status: "result_missing".to_string(),
coverage_pct: 0.0,
capability_count: 0,
capsule_unattributed: -1,
}
}
#[derive(Debug, Clone)]
pub struct MultiSimTarget {
pub udid: String,
pub runner_port: u16,
}
pub async fn run_multi_selftest(
targets: Vec<MultiSimTarget>,
runs_root: PathBuf,
) -> std::io::Result<MultiSimSummary> {
if targets.is_empty() {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"run_multi_selftest: targets is empty",
));
}
fs::create_dir_all(&runs_root)?;
let mut handles = Vec::with_capacity(targets.len());
for t in &targets {
let udid = t.udid.clone();
let port = t.runner_port;
let sim_run_dir = runs_root.join(&udid);
handles.push(tokio::spawn(async move {
if let Err(e) = run_single_sim(&udid, port, sim_run_dir).await {
eprintln!(
"selftest_multi: sim {udid} (port {port}) failed: {e} — \
aggregator 会判 result_missing"
);
}
}));
}
for h in handles {
if let Err(e) = h.await {
eprintln!("selftest_multi: tokio JoinError: {e}");
}
}
let paths: Vec<PathBuf> = targets
.iter()
.map(|t| runs_root.join(&t.udid).join("result.json"))
.collect();
let inputs: Vec<PerSimInput> = targets
.iter()
.zip(paths.iter())
.map(|(t, p)| PerSimInput {
udid: t.udid.as_str(),
result_json_path: p.as_path(),
})
.collect();
let summary = aggregate_results(&inputs);
let summary_path = runs_root.join("summary.json");
let body = serde_json::to_string_pretty(&summary)
.map_err(|e| std::io::Error::other(format!("serialize: {e}")))?;
fs::write(&summary_path, body)?;
Ok(summary)
}
async fn run_single_sim(udid: &str, runner_port: u16, sim_run_dir: PathBuf) -> Result<(), String> {
let app = crate::App::connect_to_runner(runner_port)
.await
.map_err(|e| format!("connect_to_runner({runner_port}): {}", e.message))?
.with_udid(udid);
let started_at = Utc::now();
let mut harness = super::Harness::new_with_run_dir(sim_run_dir, udid.to_string(), started_at)
.map_err(|e| format!("Harness::new_with_run_dir: {e}"))?;
let env = super::scenario::ScenarioEnv::from_process_env();
let capsule_started = match app.start_capsule_recording().await {
Ok(()) => true,
Err(e) => {
eprintln!(
"selftest_multi[{udid}]: start_capsule_recording failed: {} — 继续跑 scenario",
e.message
);
false
}
};
super::scenario::run_c1_segments(&app, &mut harness, &env).await;
if capsule_started {
match app.stop_capsule_recording_and_reconcile(None).await {
Ok(recon) => {
let summary = super::CapsuleReconcileSummary::from_reconciliation(&recon);
harness.set_capsule_reconcile(summary);
}
Err(e) => {
eprintln!(
"selftest_multi[{udid}]: stop_capsule_recording_and_reconcile failed: {}",
e.message
);
}
}
}
let lib_rs = include_str!("../lib.rs");
let verify_report = super::verifier::verify_cover_or_panic(lib_rs);
harness
.write_result_json(&verify_report)
.map_err(|e| format!("write_result_json: {e}"))?;
Ok(())
}