Files
SoundGame/tools/stable-audio-3/optimized/tflite/scripts/test_windows_compat.py
T
SoundGame Dev f0f0a9b50e 初始提交:3 款纯音频游戏 + 共享引擎 + 完整文档与资产管线
- 共享引擎(app/src/main):GestureEngine 6 手势识别、AudioEngine 三层音频
  (ExoPlayer BGM 交叉淡入 / 对白 ducking / SoundPool 低延迟)、Game/GameEngine
  状态机接口、MainActivity 全屏封面 + TalkBack
- 《声命线》storyline:末世互动广播剧,3 幕 8 抉择点 4 结局
- 《声东击西》reflex:听声辨位反应,12 波战斗 + BGM 三档 BPM 变速
- 《暗室》escape:3x3 网格密室逃脱,钥匙→铁柜→保险丝→电箱→大门谜题链
- 文档:setup(从零搭建)/ architecture(架构)/ engine-api(引擎 API)/
  asset-pipeline(资产流水线)/ games/*(三款游戏设计文档)
- 资产管线:Qwen3-TTS 对白 + 程序合成音效/BGM + Mureka BGM 的 gen 脚本与台账,
  全部 ogg 资产入库可复现
- env.d 环境脚本:android/tts 入库;sfx/mureka 含密钥不入库,仅提交 .example.sh 模板
2026-08-29 13:00:34 +08:00

188 lines
8.1 KiB
Python

"""Cross-platform (Windows/macOS/Linux) compatibility tests for the TFLite CLI.
Run directly — stdlib unittest only, no pytest, NO model downloads, no network:
python scripts/test_windows_compat.py
Covers the three Windows-specific failure modes this release guards against:
1. weights.ensure_local materializes model files without symlink privileges
(symlink -> hardlink -> copy fallback chain);
2. the CLI's banners/emoji print cleanly under a legacy console code page
(the guarded sys.stdout.reconfigure(encoding="utf-8") at script top);
3. --play dispatches to the right playback backend per platform
(afplay / winsound / aplay) without executing anything.
"""
from __future__ import annotations
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
# Keep this test's own output safe under legacy Windows console code pages
# (the subprocess tests below spawn FRESH interpreters, so this does not mask them).
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except AttributeError:
pass
SCRIPTS_DIR = Path(__file__).resolve().parent # <project>/scripts
PROJECT_DIR = SCRIPTS_DIR.parent # <project>
sys.path.insert(0, str(SCRIPTS_DIR)) # import weights / sa3_tflite / examples
import weights # noqa: E402
class TestEnsureLocalLinkOrCopy(unittest.TestCase):
"""ensure_local must materialize the target on every OS, with or without
symlink privileges (Windows without Developer Mode raises WinError 1314)."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory(prefix="sa3_citest_")
self.addCleanup(self._tmp.cleanup)
self.tmp = Path(self._tmp.name)
self.fake_cache = self.tmp / "hf_cache"
self.fake_cache.mkdir()
# Redirect the project root so nothing is written into the repo checkout.
self._orig_script_dir = weights.SCRIPT_DIR
weights.SCRIPT_DIR = self.tmp / "project"
self.addCleanup(lambda: setattr(weights, "SCRIPT_DIR", self._orig_script_dir))
# Register a fake manifest entry + suppress the (non-ASCII) HF login tip
# so no in-process print depends on console encoding.
self.rel = "models/tflite/_citest/fake_model.bin"
weights.FLAT_MANIFEST[self.rel] = "tflite/_citest/fake_model.bin"
self.addCleanup(weights.FLAT_MANIFEST.pop, self.rel, None)
self._orig_tip = weights._LOGIN_TIP_SHOWN
weights._LOGIN_TIP_SHOWN = True
self.addCleanup(lambda: setattr(weights, "_LOGIN_TIP_SHOWN", self._orig_tip))
self.payload = b"not a real tflite model \x00\x01\x02" * 100
def _fake_hf_hub_download(self, repo_id, filename):
self.assertEqual(repo_id, weights.REPO_ID)
cached = self.fake_cache / filename.replace("/", "__")
cached.write_bytes(self.payload)
return str(cached)
def test_ensure_local_materializes_target(self):
"""End-to-end: monkeypatched download -> target exists and is readable.
Uses whatever link mode the OS grants (symlink where privileged,
hardlink/copy otherwise) — all satisfy exists()/is_present()."""
import huggingface_hub
with mock.patch.object(huggingface_hub, "hf_hub_download",
self._fake_hf_hub_download):
target = weights.ensure_local(self.rel, verbose=False)
self.assertTrue(target.exists(), f"target not materialized: {target}")
self.assertEqual(target.read_bytes(), self.payload)
self.assertTrue(weights.is_present(self.rel))
def test_fallback_chain_needs_no_symlink_privilege(self):
"""Force symlink_to to fail (as on Windows without Developer Mode,
WinError 1314) and verify the hardlink/copy fallback still materializes
a readable, non-symlink target. Runs identically on every OS."""
cached = self.fake_cache / "src.bin"
cached.write_bytes(self.payload)
target = self.tmp / "project" / "models" / "linked.bin"
target.parent.mkdir(parents=True, exist_ok=True)
boom = OSError(1314, "A required privilege is not held by the client")
with mock.patch.object(type(target), "symlink_to", side_effect=boom):
weights._link_or_copy(cached, target)
self.assertTrue(target.exists())
self.assertFalse(target.is_symlink())
self.assertEqual(target.read_bytes(), self.payload)
def test_copy_fallback_when_hardlink_also_fails(self):
"""Symlink AND hardlink refused (e.g. cross-volume) -> plain copy."""
cached = self.fake_cache / "src2.bin"
cached.write_bytes(self.payload)
target = self.tmp / "project" / "models" / "copied.bin"
target.parent.mkdir(parents=True, exist_ok=True)
with mock.patch.object(type(target), "symlink_to",
side_effect=OSError(1314, "no symlink privilege")), \
mock.patch.object(weights, "_LINK_FALLBACK_SHOWN", True), \
mock.patch("os.link", side_effect=OSError(17, "cross-device link")):
weights._link_or_copy(cached, target)
self.assertTrue(target.exists())
self.assertFalse(target.is_symlink())
self.assertEqual(target.read_bytes(), self.payload)
class TestConsoleEncoding(unittest.TestCase):
"""The banners use box-drawing + emoji. Run the scripts in FRESH interpreters
with PYTHONUTF8/PYTHONIOENCODING stripped, so stdout is a pipe under the
runner's native default encoding (cp1252 on Windows without the in-script
reconfigure fix) — a UnicodeEncodeError would make these exit nonzero."""
@staticmethod
def _clean_env():
env = os.environ.copy()
env.pop("PYTHONUTF8", None)
env.pop("PYTHONIOENCODING", None)
return env
def test_sa3_tflite_help(self):
r = subprocess.run(
[sys.executable, str(SCRIPTS_DIR / "sa3_tflite.py"), "--help"],
capture_output=True, env=self._clean_env(), timeout=120)
self.assertEqual(
r.returncode, 0,
f"--help failed\nstdout:\n{r.stdout.decode('utf-8', 'replace')}\n"
f"stderr:\n{r.stderr.decode('utf-8', 'replace')}")
out = r.stdout.decode("utf-8", "replace")
self.assertIn("--prompt", out)
self.assertIn("--play", out)
def test_examples_block(self):
# examples.py is a module (no __main__): render the full emoji block.
r = subprocess.run(
[sys.executable, "-c",
"import examples; examples.print_example_commands()"],
capture_output=True, env=self._clean_env(), cwd=str(SCRIPTS_DIR),
timeout=120)
self.assertEqual(
r.returncode, 0,
f"examples block failed\nstdout:\n{r.stdout.decode('utf-8', 'replace')}\n"
f"stderr:\n{r.stderr.decode('utf-8', 'replace')}")
self.assertIn("Generate audio", r.stdout.decode("utf-8", "replace"))
class TestPlayBackendDispatch(unittest.TestCase):
"""--play platform dispatch, without playing anything."""
def _backend(self):
import sa3_tflite
return sa3_tflite._play_backend
def test_darwin_uses_afplay(self):
self.assertEqual(self._backend()("darwin"), ("subprocess", ["afplay"]))
def test_windows_uses_winsound(self):
kind, argv = self._backend()("win32")
self.assertEqual(kind, "winsound")
self.assertIsNone(argv)
if sys.platform == "win32":
import winsound # stdlib on Windows — the module the dispatcher relies on
self.assertTrue(hasattr(winsound, "PlaySound"))
def test_linux_uses_aplay_or_prints_path(self):
kind, argv = self._backend()("linux")
if kind == "subprocess":
self.assertEqual(argv[0], "aplay")
else:
self.assertEqual((kind, argv), ("none", None))
def test_current_platform_resolves(self):
kind, argv = self._backend()()
self.assertIn(kind, ("subprocess", "winsound", "none"))
if kind == "subprocess":
self.assertIsInstance(argv, list)
else:
self.assertIsNone(argv)
if __name__ == "__main__":
unittest.main(verbosity=2)