Ctrl K
ComunidadGuías…

tests/test_script_usage.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""刪掉一支腳本之前,要有證據說沒人在用。

scripts/ 底下有五十幾支可以直接執行的腳本。有些是遷移時期的一次性工具,有些
是每天跑的管線,有些是 Pan 偶爾手動叫的。從程式碼裡分不出來:

  - 「被幾個檔案引用」分不出來:action_recall 只被引用一次,卻在每一次召回
    裡都會跑;migrate_schema 同樣只被引用一次,而它上一次執行可能是半年前。
  - 檔名分不出來:full_sync_v2 聽起來現役,init_rebuild_v2 聽起來像一次性的。
  - 我分不出來:哪支是手動工作流程的一環,只有用的人知道。

所以不分類,只量測。跑一段時間之後「從來沒被叫過」才是證據,而不是猜測。

這份測試守的是量測本身:新加的可執行腳本要一起被量到,否則這份紀錄會慢慢
變成一份看起來完整、其實有缺口的清單——那比沒有更危險。
"""
from __future__ import annotations

import json
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SCRIPTS = ROOT / "scripts"
sys.path.insert(0, str(SCRIPTS))

import xkb_usage  # noqa: E402


class Recording(unittest.TestCase):
    def setUp(self):
        self._tmp = tempfile.TemporaryDirectory()
        self.path = Path(self._tmp.name) / "script-usage.jsonl"
        self.addCleanup(self._tmp.cleanup)

    def test_a_run_is_recorded(self):
        xkb_usage.record("scripts/xkb_ask.py", ["--json", "some query"], path=self.path)
        rows = xkb_usage.read(self.path)
        self.assertEqual(len(rows), 1)
        self.assertEqual(rows[0]["script"], "xkb_ask")

    def test_argument_values_are_not_kept(self):
        """參數值可能是查詢內容或路徑。這份紀錄要回答的是「有沒有人用」,
        不是「他查了什麼」。"""
        xkb_usage.record("scripts/xkb_ask.py",
                         ["--env-file", "/etc/xkb/fixture.env", "我的報價策略"],
                         path=self.path)
        blob = self.path.read_text(encoding="utf-8")
        self.assertIn("--env-file", blob)
        self.assertNotIn("/etc/xkb", blob)
        self.assertNotIn("報價", blob)

    def test_an_unwritable_path_is_silent(self):
        """觀測不能弄掛被觀測的東西。"""
        xkb_usage.record("scripts/x.py", [], path=Path(self._tmp.name) / "no" / "x.jsonl")

    def test_scripts_never_run_still_appear_in_the_summary(self):
        """從沒被叫過的那些才是重點。只列跑過的,等於把答案藏起來。"""
        with tempfile.TemporaryDirectory() as tmp:
            fake = Path(tmp)
            (fake / "used.py").write_text('if __name__ == "__main__":\n    pass\n',
                                          encoding="utf-8")
            (fake / "never.py").write_text('if __name__ == "__main__":\n    pass\n',
                                           encoding="utf-8")
            (fake / "notrunnable.py").write_text("x = 1\n", encoding="utf-8")
            xkb_usage.record("used.py", [], path=self.path)

            data = xkb_usage.summary(path=self.path, scripts_dir=fake)

        self.assertEqual(data["used"]["runs"], 1)
        self.assertEqual(data["never"]["runs"], 0)
        self.assertNotIn("notrunnable", data)


# 刻意不量測的,附上理由。
#
# 這不是「忘了接」的白名單——每一項都要說得出為什麼,否則這個例外清單會變成
# 讓缺口合法化的地方。
NOT_MEASURED = {
    # xkb_usage 需要 xkb_paths 決定紀錄寫在哪,接上去就形成
    # xkb_usage → xkb_paths → xkb_usage 的循環(2026-09-12 圖譜抓到)。
    # 而且它的 __main__ 只是印出解析到的路徑,那是除錯輸出,不是一個
    # 「有沒有人在用」值得追蹤的工具。
    "xkb_paths.py",
}


class EveryEntryPointIsMeasured(unittest.TestCase):
    def test_every_runnable_script_records_itself(self):
        missing = []
        for path in sorted(SCRIPTS.glob("*.py")):
            if path.name in NOT_MEASURED:
                continue
            text = path.read_text(encoding="utf-8", errors="ignore")
            if '__name__ == "__main__"' not in text:
                continue
            if "xkb_usage.record(__file__)" not in text:
                missing.append(path.name)
        self.assertEqual(
            missing, [],
            "這些可執行腳本沒有被量到,之後判斷「沒人用」時會少算:\n  "
            + "\n  ".join(missing))

    def test_the_exception_list_stays_honest(self):
        """例外只能是「刻意不量」,不能是「已經刪掉的檔案」留在清單裡。

        一個指向不存在檔案的例外,會讓這個清單看起來有理由、實際上什麼都沒擋。
        """
        for name in NOT_MEASURED:
            with self.subTest(script=name):
                self.assertTrue((SCRIPTS / name).exists(),
                                f"{name} 不存在了,例外清單該清掉這一行")

    def test_importing_a_script_does_not_count_as_running_it(self):
        """被 import 的次數不代表被使用——record 只在當主程式跑時呼叫。"""
        for path in sorted(SCRIPTS.glob("*.py")):
            text = path.read_text(encoding="utf-8", errors="ignore")
            if "xkb_usage.record(__file__)" not in text:
                continue
            before = text.split("xkb_usage.record(__file__)")[0]
            with self.subTest(script=path.name):
                self.assertIn('__name__ == "__main__"', before)


class TheReportRuns(unittest.TestCase):
    def test_report_does_not_crash_on_an_empty_ledger(self):
        with tempfile.TemporaryDirectory() as tmp:
            env = {"XKB_DATA_DIR": tmp, "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}
            import os
            proc = subprocess.run(
                [sys.executable, str(SCRIPTS / "xkb_usage.py"), "report", "--json"],
                capture_output=True, text=True, encoding="utf-8", errors="replace", env={**os.environ, **env})
        self.assertEqual(proc.returncode, 0, proc.stderr)
        json.loads(proc.stdout)


if __name__ == "__main__":
    unittest.main()