Files
codex/third_party/voice/test_bazel_windows.py
Benjamin Carlsson da20788df9 Bundle Linux voice runtimes and improve audio reliability (#44714)
## Why

Linux voice needs system ALSA plugins and enough buffering to accommodate PipeWire graph cycles without losing capture samples. Voice startup failures also need actionable diagnostics without exposing native error details.

## What changed

- Build and bundle GNU voice helpers and runtimes with primary Linux musl release archives, and sign the archives. Keep Python wheels free of these libraries to preserve `manylinux_2_17` compatibility.
- Discover ALSA plugins in fixed system directories and increase Linux capture and playback buffering to support larger PipeWire graph cycles.
- Report voice failures by stage, preserve negotiation timeout classification, and discard native error sources. Suppress the misleading `requested` closure message after failure cleanup.
- Add explicit Windows MSVC, pkgconf, and CMake toolchain configuration and preserve host architecture in native build environments.

## Testing

Add coverage for Linux release assembly, ALSA plugin discovery, PipeWire capture and playback, classified startup failures, failure cleanup rendering, and Windows build environment handling.

GitOrigin-RevId: d805eace96a669ce3a4489f12e2db6f68f9f7f53
2026-09-11 03:16:01 +00:00

214 lines
8.5 KiB
Python

"""Exercise declared tool selection and the platform-independent payload copy action."""
import copy
import errno
import json
from pathlib import Path
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import patch
import bazel_windows
from bazel_copy import copy_payloads
from bazel_windows import selected_inputs
class WindowsInputsTests(unittest.TestCase):
def test_adapter_preserves_host_architecture_without_developer_path(self):
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / "action.json"
config.write_text("{}")
for architecture in ("AMD64", "ARM64"):
environment = {"PROCESSOR_ARCHITECTURE": architecture, "PATH": "unsafe"}
with (
self.subTest(architecture=architecture),
patch.object(
bazel_windows, "os", SimpleNamespace(environ=environment)
),
patch.object(
bazel_windows.sys, "argv", ["driver", "unknown", str(config)]
),
patch.object(
bazel_windows,
"selected_inputs",
return_value={"target": "unused"},
),
patch.object(
bazel_windows,
"build_environment",
return_value=({"PATH": "declared"}, {}),
),
):
with self.assertRaisesRegex(
ValueError, "unknown Windows native action"
):
bazel_windows.main()
self.assertEqual(
environment,
{
"PROCESSOR_ARCHITECTURE": architecture,
"PATH": "declared",
"HOME": environment["HOME"],
"USERPROFILE": environment["HOME"],
},
)
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.repository = self.root / "external/installed tools"
self.names = {
"shell": "cygwin/bin/bash.exe",
"make": "cygwin/bin/make.exe",
"cygpath": "cygwin/bin/cygpath.exe",
"automake": "cygwin/bin/automake-1.18",
"pkg_config": "pkgconf-image/PFiles64/pkgconf 3.0.6/pkgconf.exe",
}
for name in self.names.values():
path = self.repository / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"synthetic declared executable")
self.manifest = self.repository / "voice-tools.json"
self.metadata = {
"schemaVersion": 1,
"target": "aarch64-pc-windows-msvc",
"cygwinArchitecture": "x86_64",
"tools": self.names,
}
self.manifest.write_text(json.dumps(self.metadata))
self.config = {
"inputs": {
"schemaVersion": 1,
"target": "aarch64-pc-windows-msvc",
"systemRoot": "C:/Windows",
"tools": {
"cc": "external/msvc/cl.exe",
"pkg_config": str(
(self.repository / self.names["pkg_config"]).relative_to(
self.root
)
),
},
"INCLUDE": ["external/sdk/include"],
"LIB": ["external/sdk/lib"],
},
"manifest": str(self.manifest.relative_to(self.root)),
"installed_files": [
str((self.repository / name).relative_to(self.root))
for name in self.names.values()
],
}
def test_paths_are_anchored_before_recipe_changes_directory(self):
before = copy.deepcopy(self.config)
expected = {
**self.config["inputs"],
"tools": {
"cc": str(self.root / "external/msvc/cl.exe"),
**{
name: str(self.repository / path)
for name, path in self.names.items()
},
},
"INCLUDE": [str(self.root / "external/sdk/include")],
"LIB": [str(self.root / "external/sdk/lib")],
}
self.assertEqual(selected_inputs(self.config, self.root), expected)
self.assertEqual(self.config, before)
def test_manifest_cannot_select_a_file_omitted_from_action_inputs(self):
self.config["installed_files"].pop()
with self.assertRaisesRegex(ValueError, "selection differs: pkg_config"):
selected_inputs(self.config, self.root)
def test_manifest_cannot_escape_installed_tree(self):
outside = self.repository.parent / "outside.exe"
outside.write_bytes(b"not part of the installed support tree")
inside = self.repository / "cygwin/bin/other-bash.exe"
try:
inside.symlink_to(outside)
except OSError as error:
if error.errno not in (errno.EPERM, errno.EACCES):
raise
self.skipTest("creating symlinks requires OS permission")
self.config["installed_files"].append(str(inside.relative_to(self.root)))
self.metadata["tools"]["shell"] = str(inside.relative_to(self.repository))
self.manifest.write_text(json.dumps(self.metadata))
with self.assertRaisesRegex(ValueError, "selection differs: shell"):
selected_inputs(self.config, self.root)
def test_manifest_cannot_replace_the_build_script_executable(self):
alternative = self.repository / "pkgconf-image/another/pkgconf.exe"
alternative.parent.mkdir(parents=True)
alternative.write_bytes(b"different declared executable")
self.config["installed_files"].append(str(alternative.relative_to(self.root)))
self.metadata["tools"]["pkg_config"] = str(
alternative.relative_to(self.repository)
)
self.manifest.write_text(json.dumps(self.metadata))
with self.assertRaisesRegex(ValueError, "selection differs: pkg_config"):
selected_inputs(self.config, self.root)
def test_wrong_target_or_emulation_contract_is_rejected(self):
for key, value in (
("target", "x86_64-pc-windows-msvc"),
("cygwinArchitecture", "aarch64"),
):
with self.subTest(key=key):
self.manifest.write_text(json.dumps({**self.metadata, key: value}))
with self.assertRaisesRegex(ValueError, "selected target"):
selected_inputs(self.config, self.root)
def test_missing_declared_executable_does_not_fall_back_to_path(self):
(self.repository / self.names["shell"]).unlink()
with self.assertRaises(FileNotFoundError):
selected_inputs(self.config, self.root)
class LibraryCopiesTests(unittest.TestCase):
def test_import_library_and_dll_bytes_remain_distinct(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
dll = root / "source.dll"
library = root / "source.lib"
dll.write_bytes(b"DLL payload")
library.write_bytes(b"import library payload")
locator = root / "package/lib/search-path"
copy_payloads(
locator,
[
dll,
root / "package/bin/audio.dll",
library,
root / "package/lib/audio.lib",
],
)
self.assertTrue(locator.is_dir())
self.assertEqual(
{
path.relative_to(root / "package").as_posix(): path.read_bytes()
for path in (root / "package").rglob("*")
if path.is_file()
},
{
"bin/audio.dll": b"DLL payload",
"lib/audio.lib": b"import library payload",
},
)
def test_missing_payload_fails_the_action(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
with self.assertRaises(FileNotFoundError):
copy_payloads(
root / "lib/search-path",
[root / "missing.dll", root / "bin/audio.dll"],
)
if __name__ == "__main__":
unittest.main()