#[must_use]
pub fn shard_flows(flow_count: usize, device_count: usize) -> Vec<Vec<usize>> {
if device_count == 0 {
return Vec::new();
}
let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); device_count];
for flow in 0..flow_count {
buckets[flow % device_count].push(flow);
}
buckets
}
#[must_use]
pub fn effective_sim_count(requested_parallel: usize, devices_available: usize) -> usize {
requested_parallel.clamp(1, devices_available.max(1))
}
#[must_use]
pub fn aggregate_exit(shard_codes: &[u8]) -> u8 {
shard_codes.iter().copied().max().unwrap_or(0)
}
#[must_use]
pub fn child_argv(shard_flows: &[String], udid: &str, passthrough: &[String]) -> Vec<String> {
let mut argv = vec!["run".to_string()];
argv.extend(shard_flows.iter().cloned());
argv.push("--device".to_string());
argv.push(udid.to_string());
argv.extend(passthrough.iter().cloned());
argv
}
#[must_use]
pub fn run_parallel(
exe: &std::path::Path,
shards: &[(String, Vec<String>)],
passthrough: &[String],
) -> u8 {
let mut running = Vec::new();
for (udid, flows) in shards {
if flows.is_empty() {
continue;
}
let argv = child_argv(flows, udid, passthrough);
eprintln!("smix run --parallel: sim {udid} ← {} flow(s)", flows.len());
match std::process::Command::new(exe).args(&argv).spawn() {
Ok(child) => running.push((udid.clone(), Some(child))),
Err(e) => {
eprintln!("smix run --parallel: sim {udid} failed to spawn: {e}");
running.push((udid.clone(), None));
}
}
}
let mut codes = Vec::new();
for (udid, child) in running {
let code = match child {
None => 1,
Some(mut c) => c
.wait()
.ok()
.and_then(|s| s.code())
.map_or(1, |c| c.clamp(0, 255) as u8),
};
eprintln!("smix run --parallel: sim {udid} exited {code}");
codes.push(code);
}
aggregate_exit(&codes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_sim_keeps_the_sequential_order() {
assert_eq!(shard_flows(3, 1), vec![vec![0, 1, 2]]);
}
#[test]
fn two_sims_round_robin() {
assert_eq!(shard_flows(3, 2), vec![vec![0, 2], vec![1]]);
}
#[test]
fn more_sims_than_flows_leaves_idle_sims_with_empty_buckets() {
assert_eq!(shard_flows(2, 3), vec![vec![0], vec![1], vec![]]);
}
#[test]
fn every_flow_is_assigned_exactly_once() {
let buckets = shard_flows(7, 3);
let mut all: Vec<usize> = buckets.into_iter().flatten().collect();
all.sort_unstable();
assert_eq!(all, (0..7).collect::<Vec<_>>());
}
#[test]
fn no_sims_assigns_nothing_rather_than_panicking() {
assert_eq!(shard_flows(5, 0), Vec::<Vec<usize>>::new());
}
#[test]
fn parallelism_is_capped_by_available_sims() {
assert_eq!(effective_sim_count(3, 2), 2); assert_eq!(effective_sim_count(1, 4), 1); assert_eq!(effective_sim_count(4, 4), 4);
assert_eq!(effective_sim_count(2, 0), 1); }
#[test]
fn a_failed_shard_fails_the_whole_run() {
assert_eq!(aggregate_exit(&[0, 0, 0]), 0);
assert_eq!(aggregate_exit(&[0, 2, 0]), 2); assert_eq!(aggregate_exit(&[]), 0); }
#[test]
fn child_argv_pins_the_sim_and_forwards_passthrough() {
let argv = child_argv(
&["a.yaml".into(), "b.yaml".into()],
"UDID-1",
&["--bundle-id".into(), "com.x".into(), "--no-launch".into()],
);
assert_eq!(
argv,
vec![
"run",
"a.yaml",
"b.yaml",
"--device",
"UDID-1",
"--bundle-id",
"com.x",
"--no-launch"
]
);
assert!(
!argv
.iter()
.any(|a| a == "--parallel" || a == "--also-device")
);
}
}