Ctrl K
ComunidadGuías…

tests/test_face_models.py

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

Archivo original ↗Preguntar a KiBot
"""人脸模型在任务中心的那张卡:按转变留记录、当前状态与立即重试。"""
import hashlib
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock

from support.ledger import fresh_ledger

from peach import face_detect, face_match, face_models, web_router
from peach.jobs import BackgroundJob
from peach.repository import LedgerDatabase
from peach.task_runs import DEFAULT_KEEP, TaskRunStore, task_label

TASK = face_models.TASK_KEY
DOWN = face_detect.FaceModelUnavailable("取人脸模型失败:getaddrinfo failed")


class FaceModelsCase(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.addCleanup(self.tmp.cleanup)
        self.root = Path(self.tmp.name).resolve()
        self.store = TaskRunStore(LedgerDatabase(fresh_ledger(self.root)), host="test-host")
        self.addCleanup(setattr, face_detect, "observer", None)

    def rows(self):
        return self.store.query(task_key=TASK)


class ReporterTests(FaceModelsCase):
    def test_detach_releases_only_the_owning_ledgers_reporter(self):
        face_models.attach(self.store)
        attached = face_detect.observer
        face_models.detach(object())
        self.assertIs(face_detect.observer, attached)
        face_models.detach(self.store)
        self.assertIsNone(face_detect.observer)

    def setUp(self):
        super().setUp()
        self.report = face_models.Reporter(self.store)

    def test_a_failure_leaves_one_failed_row_with_the_reason(self):
        self.report("人脸模型", DOWN)
        [run] = self.rows()
        self.assertEqual((run.status, run.trigger), ("failed", "scheduled"))
        self.assertEqual(run.error, str(DOWN))

    def test_the_same_failure_again_does_not_add_a_row(self):
        """断网时每条补头像后继都会试一次,逐条记下来就是满屏同一张卡。"""
        for _ in range(5):
            self.report("人脸模型", DOWN)
            self.report("人脸比对模型", DOWN)
        self.assertEqual(len(self.rows()), 1)

    def test_a_download_after_a_failure_closes_it_with_a_succeeded_row(self):
        self.report("人脸模型", DOWN)
        self.report("人脸模型", None)
        latest, failed = self.rows()
        self.assertEqual((latest.status, failed.status), ("succeeded", "failed"))
        self.assertEqual(latest.result_summary["outcome"], "已取得人脸模型")
        # 恢复之后再断,是新的一次失败。
        self.report("人脸模型", DOWN)
        self.assertEqual([run.status for run in self.rows()], ["failed", "succeeded", "failed"])

    def test_a_download_with_nothing_to_close_leaves_no_row(self):
        self.report("人脸模型", None)
        self.assertEqual(self.rows(), [])

    def test_a_retry_in_progress_settles_its_own_row(self):
        self.store.start(TASK, trigger="manual", mutex_key=TASK)
        self.report("人脸模型", DOWN)
        self.report("人脸模型", None)
        self.assertEqual([run.status for run in self.rows()], ["running"])

    def test_history_is_kept_to_the_usual_number_of_rows(self):
        for _ in range(DEFAULT_KEEP + 3):
            self.report("人脸模型", DOWN)
            self.report("人脸模型", None)
        self.assertEqual(len(self.store.query(task_key=TASK, limit=100)), DEFAULT_KEEP)

    def test_a_read_only_ledger_gets_no_rows(self):
        self.store.enabled = False
        self.report("人脸模型", DOWN)
        self.store.enabled = True
        self.assertEqual(self.rows(), [])

    def test_a_broken_task_table_does_not_reach_the_model_caller(self):
        broken = face_models.Reporter(SimpleNamespace(
            enabled=True, query=mock.Mock(side_effect=RuntimeError("库坏了"))))
        with self.assertLogs("peach.face_models", "WARNING"):
            broken("人脸模型", DOWN)


class FetchObserverTests(FaceModelsCase):
    def setUp(self):
        super().setUp()
        self.seen = []
        face_detect.observer = lambda label, error: self.seen.append((label, error))
        self.target = self.root / "tools" / "model.onnx"

    def fetch(self, sha256: str):
        return face_detect.fetch_model(self.target, "https://example.invalid/m.onnx", sha256,
                                       allow_download=True, label="人脸模型")

    def test_a_network_failure_is_reported_and_still_raised(self):
        with mock.patch.object(face_detect, "read_url", side_effect=OSError("断网")), \
                self.assertRaises(face_detect.FaceModelUnavailable) as caught:
            self.fetch("0" * 64)
        self.assertEqual(self.seen, [("人脸模型", caught.exception)])

    def test_a_checksum_mismatch_is_reported(self):
        with mock.patch.object(face_detect, "read_url", return_value=b"half"), \
                self.assertRaises(face_detect.FaceModelUnavailable):
            self.fetch("0" * 64)
        self.assertIn("校验不符", str(self.seen[0][1]))

    def test_a_download_is_reported_and_counted_and_a_local_hit_is_not(self):
        payload = b"onnx"
        digest = hashlib.sha256(payload).hexdigest()
        before = face_detect.downloads
        with mock.patch.object(face_detect, "read_url", return_value=payload) as read:
            self.fetch(digest)
            self.fetch(digest)
        self.assertEqual(read.call_count, 1)
        self.assertEqual(self.seen, [("人脸模型", None)])
        self.assertEqual(face_detect.downloads, before + 1)


class ModelFilesCase(FaceModelsCase):
    """两个模型落在临时目录,立即重试走真实路由与后台任务,只有取字节那一步换掉。"""

    def setUp(self):
        super().setUp()
        face_models.attach(self.store)
        self.payload = b"onnx"
        digest = hashlib.sha256(self.payload).hexdigest()
        for module, name in ((face_detect, "yunet.onnx"), (face_match, "sface.onnx")):
            for attribute, value in (("MODEL_PATH", self.root / "tools" / name),
                                     ("MODEL_SHA256", digest)):
                patcher = mock.patch.object(module, attribute, value)
                patcher.start()
                self.addCleanup(patcher.stop)
        self.contract = SimpleNamespace(face_models_job=BackgroundJob(
            "PeachFaceModelsJob", task_key=TASK, runs=self.store))

    def retry(self, **read):
        with mock.patch.object(face_detect, "read_url", **read):
            web_router.dispatch_api_post(self.contract, "/api/face-models", {})
            self.contract.face_models_job.thread.join(5)


class RetryEndpointTests(ModelFilesCase):
    """「立即重试」:一轮普通的后台任务,记录只有它自己那一行。"""

    def test_a_failed_retry_is_one_failed_manual_row(self):
        self.retry(side_effect=OSError("断网"))
        [run] = self.rows()
        self.assertEqual((run.status, run.trigger), ("failed", "manual"))
        self.assertIn("取人脸模型失败", run.error)
        self.assertIn("取人脸比对模型失败", run.error)

    def test_a_successful_retry_is_one_succeeded_row(self):
        self.retry(side_effect=OSError("断网"))
        self.retry(return_value=self.payload)
        self.assertEqual([run.status for run in self.rows()], ["succeeded", "failed"])
        self.assertTrue((self.root / "tools" / "sface.onnx").is_file())

    def test_the_route_is_open_on_a_read_only_machine(self):
        self.assertIn("/api/face-models", web_router.READ_ONLY_POST_ROUTES)


class StatusTests(ModelFilesCase):
    """活动中心「需要处理」读的那一份:和任务卡同一个来源,读的时候不联网。"""

    def status(self):
        with mock.patch.object(face_detect, "read_url",
                               side_effect=AssertionError("读状态不该联网")):
            return face_models.status(self.store)

    def test_never_fetched_is_missing_not_a_problem(self):
        self.assertEqual(self.status(), {
            "task_key": TASK, "state": "missing", "available": False, "failed_at": None,
            "error": "", "run_id": None, "retry_url": face_models.RETRY_URL})

    def test_the_latest_failure_is_reported_with_its_card(self):
        self.retry(side_effect=OSError("断网"))
        [run] = self.rows()
        current = self.status()
        self.assertEqual((current["state"], current["available"]), ("failed", False))
        self.assertEqual((current["failed_at"], current["error"], current["run_id"]),
                         (run.finished_at, run.error, run.id))
        self.assertEqual(current["retry_url"], "/api/face-models")

    def test_a_retry_in_progress_is_downloading(self):
        run = self.store.start(TASK, trigger="manual", mutex_key=TASK)
        self.assertEqual((self.status()["state"], self.status()["run_id"]), ("downloading", run.id))

    def test_both_models_on_disk_after_a_recovery_is_ready(self):
        self.retry(side_effect=OSError("断网"))
        self.retry(return_value=self.payload)
        current = self.status()
        self.assertEqual((current["state"], current["available"], current["error"]),
                         ("ready", True, ""))

    def test_models_shipped_with_the_package_are_ready_without_any_download(self):
        bundle = self.root / "program" / "tools" / "face-models"
        bundle.mkdir(parents=True)
        for module in (face_detect, face_match):
            (bundle / module.MODEL_PATH.name).write_bytes(self.payload)
        with mock.patch.dict(face_detect.os.environ, {face_detect.BUNDLE_ENV: str(bundle)}), \
                mock.patch.object(face_detect, "_verified", {}):
            self.assertEqual(self.status()["state"], "ready")
            self.retry(side_effect=AssertionError("随包有模型时立即重试也不联网"))
        self.assertEqual([run.status for run in self.rows()], ["succeeded"])
        self.assertFalse((self.root / "tools").exists(), "随包那份不往数据根复制")


class LabelTests(unittest.TestCase):
    def test_every_task_written_by_the_service_has_a_chinese_name(self):
        self.assertEqual(task_label("face-models"), "人脸模型下载")
        self.assertEqual(task_label("amane-bridge"), "amane 桥重建")
        self.assertEqual(task_label("follow-creator"), "关注作者建档")


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