From 91bcc19ac0d629b8a219c5ba604acb5f072c01cc Mon Sep 17 00:00:00 2001 From: Sam Sung <51686767+samwhosung@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:22:17 +0700 Subject: [PATCH] Test builds open no audio device (#2388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every headless test app built `GamePlugins`, whose `SoundPlugin` opened the default output device unless `$WOW_NOSOUND` or `$WOW_CAPTURE` was set, and no test run sets either. Under nextest that is one CoreAudio client and speaker stream per test process. With a few runs going at once, `coreaudiod` sat near 300% CPU, every sound on the machine crackled, and each test blocked on the audio server's connect. A test build now takes the existing silent path, so its mixer is `None`, as with no device. Test: `a_test_client_opens_no_audio_device`. With the change undone it fails on a machine with a speaker, taking 96 s; with it, the test passes in 0.06 s. Fixes #2380 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 (cherry picked from commit fbc90b9bbf272a784f3703eb0c68ea466fe90951) # Conflicts: # crates/benilla-app/src/game_plugins.rs # docs/DEVIATIONS.md Absorbed into the octo fork. The sound/mod.rs fix applies unchanged. Of the tests the conflict hunk carried, only this commit's own a_test_client_opens_no_audio_device is kept, retargeted from upstream's headless_client_unrun to our headless_client (same contract: every plugin's build runs, no schedule does); resource_writers and the_glue_preview_has_one_feeder come from other upstream commits and need glue_scene and GluePreview, which this fork has not got. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/src/game_plugins.rs | 9 +++++++++ crates/benilla-app/src/sound/mod.rs | 16 +++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/crates/benilla-app/src/game_plugins.rs b/crates/benilla-app/src/game_plugins.rs index 9fc6bc6d..c821a1d9 100644 --- a/crates/benilla-app/src/game_plugins.rs +++ b/crates/benilla-app/src/game_plugins.rs @@ -554,6 +554,15 @@ pub(crate) mod schedule_tests { c.systems.values().any(|s| s.name.contains("benilla_app::")) } + /// A test client opens no audio device: one stream per test process, one process per test, + /// floods the system audio server and crackles every sound on the machine. + #[test] + fn a_test_client_opens_no_audio_device() { + let app = headless_client(); + let output = app.world().non_send_resource::(); + assert!(output.mixer.is_none()); + } + /// One row of the class table: the type's name for the message, and its `TypeId`. type ClassRow = (&'static str, fn() -> TypeId); diff --git a/crates/benilla-app/src/sound/mod.rs b/crates/benilla-app/src/sound/mod.rs index f2ac95b3..2bbc89dd 100644 --- a/crates/benilla-app/src/sound/mod.rs +++ b/crates/benilla-app/src/sound/mod.rs @@ -224,8 +224,8 @@ fn feed_music_suppression( } } -/// The backend output; `mixer` is `None` with no device or under `$WOW_NOSOUND`. Non-Send: the -/// device stream is not `Send` on every platform, so audio systems run on the main thread. +/// The backend output; `mixer` is `None` with no device, under `$WOW_NOSOUND` or in a test. +/// Non-Send: the device stream is not `Send` on every platform, so audio systems run on the main thread. pub(crate) struct SoundOutput { pub(crate) mixer: Option, /// Live kit channels, pumped by [`kit::pump_channels`]. @@ -351,19 +351,21 @@ impl Plugin for SoundPlugin { fn build(&self, app: &mut App) { net::register(app); app.add_observer(on_cvar); - // `$WOW_NOSOUND` (a silent unattended run) and `$WOW_CAPTURE` open no device at all, so - // they never hold the audio hardware against a session someone is listening to. + // `$WOW_NOSOUND` (a silent unattended run), `$WOW_CAPTURE` and a test build open no device + // at all, so they never hold the audio hardware against a session someone is listening to. let silent = ["WOW_NOSOUND", "WOW_CAPTURE"] .into_iter() - .find(|v| std::env::var_os(v).is_some()); + .find(|v| std::env::var_os(v).is_some()) + .map(|v| format!("${v} set")) + .or_else(|| cfg!(test).then(|| "a test build".to_owned())); // Before the mixer: the probe's taps are main-track effects, fixed when kira builds it. let probe_dir = if silent.is_some() { None } else { probe::output_dir() }; - let mixer = if let Some(var) = silent { - info!("${var} set — audio disabled"); + let mixer = if let Some(why) = silent { + info!("{why} — audio disabled"); None } else { match Mixer::new(probe_dir.as_deref()) {