tests/test_console_encoding.py
Rama main · réplica de lectura del repositorio original.
"""腳本做完事情之後,不可以死在回報那一行。
XKB 的輸出到處是 emoji。在 Windows 的 cp950 主控台,`print("📚 …")` 會拋
UnicodeEncodeError——而那個 print 發生在工作已經完成之後。結果是事情做完了,
卻以一個看起來像程式壞掉的 traceback 結束。
這個現象被當成「環境問題」處理了很久,做法是要求每個呼叫端帶 PYTHONUTF8=1。
2026-09-11 的完整測試裡,那是唯一一個從頭到尾都紅的 error,而我每次都帶著那個
環境變數跑,所以每次都看不到——我把環境設定當成了測試通過。
它不是環境問題:任何人在 Windows 主控台直接跑 build_vector_index.py 都會炸。
"""
from __future__ import annotations
import io
import os
import subprocess
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
sys.path.insert(0, str(SCRIPTS))
import xkb_console # noqa: E402
EMOJI = "📚 ✅ ❌ 🔢"
class UseUtf8(unittest.TestCase):
def test_it_is_a_no_op_when_the_stream_has_no_reconfigure(self):
"""被包起來的 stream(測試、pipe)不一定有 reconfigure,不能因此炸。"""
real_out, real_err = sys.stdout, sys.stderr
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
try:
xkb_console.use_utf8() # 不該拋
finally:
sys.stdout, sys.stderr = real_out, real_err
def test_it_does_not_raise_twice(self):
xkb_console.use_utf8()
xkb_console.use_utf8()
class EmojiSurvivesALegacyConsole(unittest.TestCase):
"""在一個刻意設成非 UTF-8 的子行程裡印 emoji。"""
def _run(self, body: str, *, legacy: bool) -> subprocess.CompletedProcess:
env = {**os.environ}
if legacy:
# 模擬 Windows 主控台的舊字碼頁;POSIX 上用 ascii 效果等價。
env["PYTHONIOENCODING"] = "cp950" if os.name == "nt" else "ascii"
env.pop("PYTHONUTF8", None)
return subprocess.run(
[sys.executable, "-c", body], capture_output=True, text=True,
encoding="utf-8", errors="replace", env=env, cwd=str(ROOT))
def test_printing_emoji_without_the_guard_fails_on_a_legacy_console(self):
"""先證明這個環境真的會炸——否則下面那個測試什麼都沒驗到。"""
proc = self._run(f'print("{EMOJI}")', legacy=True)
if proc.returncode == 0:
raise unittest.SkipTest("這個環境的 stdout 不會因為字碼頁失敗")
self.assertIn("UnicodeEncodeError", proc.stderr)
def test_printing_emoji_with_the_guard_succeeds(self):
body = (
"import sys; sys.path.insert(0, 'scripts')\n"
"import xkb_console; xkb_console.use_utf8()\n"
f'print("{EMOJI}")\n'
)
proc = self._run(body, legacy=True)
self.assertEqual(proc.returncode, 0, proc.stderr)
class TheScriptThatCrashedIsGuarded(unittest.TestCase):
def test_build_vector_index_calls_the_guard(self):
"""它是實際炸過的那一支:print(f"📚 Loaded …") 在 cp950 下拋例外。"""
text = (SCRIPTS / "build_vector_index.py").read_text(encoding="utf-8")
self.assertIn("xkb_console.use_utf8()", text)
# 要在任何 print 之前就轉好,所以 guard 必須在 main() 之外。
before_main = text.split("def main(")[0]
self.assertIn("xkb_console.use_utf8()", before_main)
if __name__ == "__main__":
unittest.main()