//go:build browser && darwin package callstest import ( "context" "os" "os/exec" "path/filepath" "strings" "testing" "time" ) // The audio test's record-and-play-back path, through the shipping // capture chain: LiveKit's engine module in manual rendering with the // synthetic voice as its microphone, WebRTC's processing module, and the // capture hook's noise reduction (DeepFilterNet when // CALLS_DEEPFILTER_MODEL is set, as make native-browser sets it). No // server and no browser; it sits with the native gates because it runs // the same CallsInterop build. // // Five runs must each print PASS: noise reduction on, off, and a control // with a silent microphone that must be called silent; then the test's // start on the real device module (not manual rendering), which must ask // the module to record, and its control, 0.3.7's start, which never did. // Manual rendering runs the input whenever the output runs, so the first // three pass whether or not the test opens a microphone; the last two are // what would have caught the 0.3.6/0.3.7 test that never opened one. A run // that prints nothing is a failure, not a pass. func TestNativeMacAudioSelfTest(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with swift build --package-path native/apple --product CallsInterop first") } ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) defer cancel() out, err := exec.CommandContext(ctx, binary, "--audio-selftest").CombinedOutput() var passes, fails []string for _, line := range strings.Split(string(out), "\n") { switch { case strings.HasPrefix(line, "PASS audio self-test"): passes = append(passes, line) case strings.HasPrefix(line, "FAIL audio self-test"): fails = append(fails, line) } } for _, line := range append(passes, fails...) { t.Log(line) } if err != nil || len(fails) > 0 || len(passes) != 5 { t.Fatalf("audio self-test: %d passed, %d failed, exit %v\n%s", len(passes), len(fails), err, out) } for _, want := range []string{ "(noise reduction on)", "(noise reduction off)", "control (a silent microphone)", "control (0.3.7's start on a real device never opens the microphone)", "on a real device (asks the module to record)", } { found := false for _, line := range passes { found = found || strings.Contains(line, want) } if !found { t.Errorf("no PASS line for %s", want) } } } // The main thread is never held longer than 100 ms while the audio // starts: Test audio before a call (plain, with a Bluetooth headset's // profile switch simulated as a 1.5 s hold of the engine's start on // WebRTC's worker thread, and closed partway through that start), and a // call's audio as a join starts it, with the same switch. A watchdog in // CallsInterop times the main queue every 5 ms, on the real engine // module; a test process has no microphone permission, so the module // refuses at the very end, after everything that used to block. // // 0.3.8 held it 0.1–0.7 s for the plain start and about 1.5 s with the // switch (Paul's "pause, then a beachball", 2026-09-27). A control run // blocks the main thread on purpose and must be seen, so a pass means the // watchdog was looking. Five runs, five PASS lines. func TestNativeMacAudioStartStall(t *testing.T) { root, err := filepath.Abs("../..") if err != nil { t.Fatal(err) } binary := filepath.Join(root, "native/apple/.build/debug/CallsInterop") if _, err := os.Stat(binary); err != nil { t.Fatal("build native interop executable with swift build --package-path native/apple --product CallsInterop first") } ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) defer cancel() out, err := exec.CommandContext(ctx, binary, "--audio-start-stall").CombinedOutput() var passes, fails []string for _, line := range strings.Split(string(out), "\n") { switch { case strings.HasPrefix(line, "PASS "): passes = append(passes, line) case strings.HasPrefix(line, "FAIL "): fails = append(fails, line) } } for _, line := range append(passes, fails...) { t.Log(line) } if err != nil || len(fails) > 0 || len(passes) != 5 { t.Fatalf("audio start stall: %d passed, %d failed, exit %v\n%s", len(passes), len(fails), err, out) } for _, want := range []string{ "audio start stall control", "PASS Test audio start:", "Test audio start, Bluetooth switch simulated", "Test audio closed while its microphone starts", "Call join audio start, Bluetooth switch simulated", } { found := false for _, line := range passes { found = found || strings.Contains(line, want) } if !found { t.Errorf("no PASS line for %s", want) } } }