test(host): harden Phase 9 live rig

This commit is contained in:
2026-08-22 00:39:38 -04:00
parent 98bb78f9cf
commit 360d7112e6
+57 -15
View File
@@ -1020,7 +1020,8 @@ mod tests {
const PN_CHIP_FRAMES: usize = 24; const PN_CHIP_FRAMES: usize = 24;
const PN_SEQUENCE_CHIPS: usize = 4_096; const PN_SEQUENCE_CHIPS: usize = 4_096;
const PN_WINDOW_CHIPS: usize = 1_024; const PN_WINDOW_CHIPS: usize = 1_024;
const PN_FILE_SECONDS: usize = 8; const PN_FILE_SECONDS: usize = 12;
const PN_CAPTURE_SECONDS: u64 = 6;
const PN_SKIP_FRAMES: usize = PN_SAMPLE_RATE / 2; const PN_SKIP_FRAMES: usize = PN_SAMPLE_RATE / 2;
const DESKTOP_PN_SEED: u64 = 0x1357_9bdf; const DESKTOP_PN_SEED: u64 = 0x1357_9bdf;
const REMOTE_PN_SEED: u64 = 0x2468_ace1; const REMOTE_PN_SEED: u64 = 0x2468_ace1;
@@ -1630,30 +1631,36 @@ mod tests {
Ok(errors) Ok(errors)
} }
fn node_ids_named(names: &[&str]) -> Result<HashSet<u64>> { fn matching_node_ids(objects: &[serde_json::Value], names: &[&str]) -> Result<HashSet<u64>> {
let objects = pw_dump_objects("the Phase-9 xrun node set")?;
let wanted = names.iter().copied().collect::<HashSet<_>>(); let wanted = names.iter().copied().collect::<HashSet<_>>();
let mut seen_names = HashSet::new();
let found = objects let found = objects
.iter() .iter()
.filter_map(|object| { .filter_map(|object| {
let name = object let name = object
.pointer("/info/props/node.name") .pointer("/info/props/node.name")
.and_then(serde_json::Value::as_str)?; .and_then(serde_json::Value::as_str)?;
wanted if !wanted.contains(name) {
.contains(name) return None;
.then(|| object.get("id").and_then(value_u64))? }
let id = object.get("id").and_then(value_u64)?;
seen_names.insert(name);
Some(id)
}) })
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
if found.len() != wanted.len() { let mut missing = wanted.difference(&seen_names).copied().collect::<Vec<_>>();
bail!( if !missing.is_empty() {
"Phase-9 xrun snapshot resolved {} of {} required nodes ({names:?})", missing.sort_unstable();
found.len(), bail!("Phase-9 xrun snapshot did not resolve required node names: {missing:?}");
wanted.len()
);
} }
Ok(found) Ok(found)
} }
fn node_ids_named(names: &[&str]) -> Result<HashSet<u64>> {
let objects = pw_dump_objects("the Phase-9 xrun node set")?;
matching_node_ids(&objects, names)
}
fn xrun_delta( fn xrun_delta(
before: &HashMap<u64, u64>, before: &HashMap<u64, u64>,
after: &HashMap<u64, u64>, after: &HashMap<u64, u64>,
@@ -1671,7 +1678,11 @@ mod tests {
.sum() .sum()
} }
async fn record_capture(monitor_name: &str, label: &str) -> Result<Vec<u8>> { async fn record_capture_for(
monitor_name: &str,
label: &str,
duration: Duration,
) -> Result<Vec<u8>> {
let raw = RawCaptureFile::new(label); let raw = RawCaptureFile::new(label);
let output_file = let output_file =
std::fs::File::create(&raw.0).with_context(|| format!("create {}", raw.0.display()))?; std::fs::File::create(&raw.0).with_context(|| format!("create {}", raw.0.display()))?;
@@ -1690,12 +1701,16 @@ mod tests {
.stdout(Stdio::from(output_file)) .stdout(Stdio::from(output_file))
.stderr(Stdio::piped()); .stderr(Stdio::piped());
let mut recorder = ContainedProcess::spawn(&mut command, "Phase-6 parec recorder")?; let mut recorder = ContainedProcess::spawn(&mut command, "Phase-6 parec recorder")?;
tokio::time::sleep(Duration::from_secs(3)).await; tokio::time::sleep(duration).await;
recorder.ensure_running()?; recorder.ensure_running()?;
recorder.stop()?; recorder.stop()?;
std::fs::read(&raw.0).with_context(|| format!("read {}", raw.0.display())) std::fs::read(&raw.0).with_context(|| format!("read {}", raw.0.display()))
} }
async fn record_capture(monitor_name: &str, label: &str) -> Result<Vec<u8>> {
record_capture_for(monitor_name, label, Duration::from_secs(3)).await
}
fn tone_dbfs(raw: &[u8], frequency: f64) -> Result<f64> { fn tone_dbfs(raw: &[u8], frequency: f64) -> Result<f64> {
const SAMPLE_RATE: f64 = 48_000.0; const SAMPLE_RATE: f64 = 48_000.0;
const CHANNELS: usize = 2; const CHANNELS: usize = 2;
@@ -1935,7 +1950,12 @@ mod tests {
} }
let relevant_xrun_nodes = node_ids_named(&xrun_names)?; let relevant_xrun_nodes = node_ids_named(&xrun_names)?;
let xruns_before = pw_top_errors()?; let xruns_before = pw_top_errors()?;
let raw = record_capture(&monitor_name, &format!("phase9-{}", arm.label)).await?; let raw = record_capture_for(
&monitor_name,
&format!("phase9-{}", arm.label),
Duration::from_secs(PN_CAPTURE_SECONDS),
)
.await?;
let xruns_after = pw_top_errors()?; let xruns_after = pw_top_errors()?;
// Original routes must still be intact after the measurement, not only // Original routes must still be intact after the measurement, not only
@@ -2014,6 +2034,28 @@ mod tests {
assert!(max_cyclic_correlation(&vec![0.0; PN_WINDOW_CHIPS], &desktop) == 0.0); assert!(max_cyclic_correlation(&vec![0.0; PN_WINDOW_CHIPS], &desktop) == 0.0);
} }
#[test]
fn phase9_xrun_node_resolution_accepts_duplicate_names_but_not_missing_names() {
let objects = serde_json::json!([
{"id": 11, "info": {"props": {"node.name": "controlled"}}},
{"id": 12, "info": {"props": {"node.name": "controlled"}}},
{"id": 13, "info": {"props": {"node.name": "playback"}}},
{"id": 14, "info": {"props": {"node.name": "unrelated"}}}
]);
let objects = objects.as_array().expect("fixture is an array");
assert_eq!(
matching_node_ids(objects, &["controlled", "playback"])
.expect("all required names are represented"),
HashSet::from([11, 12, 13])
);
assert!(
matching_node_ids(objects, &["controlled", "missing"])
.unwrap_err()
.to_string()
.contains("missing")
);
}
/// Phase-9 rig upgrade: the Phase-6 production-path topology is driven by /// Phase-9 rig upgrade: the Phase-6 production-path topology is driven by
/// two pinned, independently generated PN probes. Every non-overlapping /// two pinned, independently generated PN probes. Every non-overlapping
/// half-second window is normalized and correlated separately on left and /// half-second window is normalized and correlated separately on left and