api_test.py 10.4 KB
"""
api_test.py - 对 api.py 中 FootballReplayMatch.replay_match_event 的单元测试

使用伪数据覆盖主要分支:
1. 时间阈值内直接匹配成功
2. 时间匹配失败 + LLM 判定不是进球
3. 时间匹配失败 + LLM 判定是进球 + LLM 匹配成功
4. 时间匹配失败 + LLM 判定是进球 + LLM 匹配失败
5. 无进球事件 / events 为空
6. 边界:缺少 replay 键时抛出异常
"""
import os
import time
import unittest
from types import SimpleNamespace
from unittest.mock import MagicMock, patch

try:
    from api import FootballReplayMatch
except ImportError:
    from .api import FootballReplayMatch

from pathlib import Path
def get_video_files_pathlib(folder_path, extensions=None):
    if extensions is None:
        extensions = {'.mp4'}
    else:
        extensions = {ext.lower() for ext in extensions}
    
    folder = Path(folder_path)
    image_files = [f.name for f in folder.iterdir() 
                   if f.is_file() and f.suffix.lower() in extensions]
    return sorted(image_files)

def _fake_settings(match_by_time_threshold: int = 30):
    """构造一个 settings 伪对象,兼容 api.py 中的 value_or_default 读取逻辑。"""
    return SimpleNamespace(
        match_by_time_threshold=match_by_time_threshold,
        llm=SimpleNamespace(base_url="http://192.168.1.59:11434/v1", 
                            model_name="Qwen3.6-35B-A3B-UD-Q8_K_XL.gguf", 
                            temperature=0.7, 
                            api_key='no_key'
                            ),
        common=SimpleNamespace(cache_dir="/root/lzw/tmp_0527_replay_cache_08/videos_cache", 
                               task_log_dir="/root/lzw/tmp_0527_replay_cache_08/tasks_log"
                               ),
        save_frames_enable=True,
    )




def get_event_list():
    # url = "http://video.mam.miguvideo.com/mnt6/fastclip3/wsc/2026/04/29/df760b8d15394cbc9ed0d0a4a9c4b786_1080PS/29133605/vodtmp/3b3e99cf4ca84c3782503d8817242de2.m3u8"
    events_mp4_dir = r"/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/goal"
    
    events_mp4_names = get_video_files_pathlib(events_mp4_dir)
    
    event_lis = []
    for mp4 in events_mp4_names:
        mp4_path = os.path.join(events_mp4_dir, mp4)
        event_lis.append({
            "id": f"event_{mp4.split('.')[0]}",
            "type": "1",
            "url": mp4_path,
            # "event_utc": None
        })
    return event_lis

def _base_data(i, replay_path, event_lis):

    return {
        "id": f"task_{str(i).zfill(3)}",
        "match_id": f"match_{str(i).zfill(3)}",
        "replay": {
            "id": f"replay_{str(i).zfill(3)}",
            "url": replay_path,
            # "start_utc": None,
            # "end_utc": None
        },
        "events": event_lis,
    }


class TestReplayMatchEvent(unittest.TestCase):

    @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    def test_match_by_time_success(self, mock_video_cls, mock_live_cls):
        # """场景1:时间差在阈值内,直接通过 match_by_time 命中。"""
        settings = _fake_settings(match_by_time_threshold=10)  # threshold = 30 * 1000 = 30000 ms
        frm = FootballReplayMatch(settings)

        replays_mp4_dir = "/root/lzw/69e4ed7c872d573c9554510b-auto-10154973/replay"
        relays_mp4_names = get_video_files_pathlib(replays_mp4_dir)
        
        event_list = get_event_list()
        
        all_time = 0.0
        max_time = 0.0
        for i, replay_name in enumerate(relays_mp4_names):
            print(100*"*" + f"replay_name: {replay_name}")
            replay_path = os.path.join(replays_mp4_dir, replay_name)
            data = _base_data(i+1, replay_path, event_list)
            
            start_time = time.time()
            result = frm.replay_match_event(data)
            end_time = time.time()
            cost_time = end_time - start_time
            all_time += cost_time
            print(f"replay_name: {replay_name}, cost time:{cost_time}")
            if max_time < cost_time:
                max_time = cost_time
            
            print(result)
            print()
            print()
            print()
            
        print(100*"#")
        print(f"max_time: {max_time}")
        print(f"all_time: {all_time}")
        # exit()

    #     self.assertEqual(result["id"], "task_001")
    #     self.assertEqual(result["match_id"], "match_001")
    #     self.assertEqual(result["replay_id"], "replay_001")
    #     # event_001 与 replay.start_utc 差值 20 ms < 30000 ms,应命中 event_001 的 id
    #     self.assertEqual(result["event_id"], "event_001")
    #     # 未调用 LLM 相关方法
    #     mock_video_cls.return_value.video_event.assert_not_called()
    #     mock_live_cls.return_value.match_batch.assert_not_called()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_match_by_time_fail_then_not_goal(self, mock_video_cls, mock_live_cls):
    #     """场景2:时间匹配失败,det_goal_replay 判定不是进球。"""
    #     mock_video_instance = MagicMock()
    #     mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "宣传片"}
    #     mock_video_cls.return_value = mock_video_instance

    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     result = frm.replay_match_event(data)

    #     self.assertEqual(result["event_id"], None)
    #     mock_video_instance.video_event.assert_called_once()
    #     mock_live_cls.return_value.match_batch.assert_not_called()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_match_by_time_fail_then_goal_then_llm_match(self, mock_video_cls, mock_live_cls):
    #     """场景3:时间匹配失败 -> 判定是进球 -> LLM 匹配成功。"""
    #     mock_video_instance = MagicMock()
    #     mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "世界波"}
    #     mock_video_cls.return_value = mock_video_instance

    #     mock_live_instance = MagicMock()
    #     mock_live_instance.match_batch.return_value = {"video_id": "event_003", "video_path": "http://example.com/event3.mp4", "asr_text": ""}
    #     mock_live_cls.return_value = mock_live_instance

    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     result = frm.replay_match_event(data)

    #     self.assertEqual(result["event_id"], "event_003")
    #     mock_video_instance.video_event.assert_called_once()
    #     mock_live_instance.match_batch.assert_called_once()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_match_by_time_fail_then_goal_then_llm_no_match(self, mock_video_cls, mock_live_cls):
    #     """场景4:时间匹配失败 -> 判定是进球 -> LLM 未匹配到任何事件。"""
    #     mock_video_instance = MagicMock()
    #     mock_video_instance.video_event.return_value = {"event_name": "进球", "description": "头球破门"}
    #     mock_video_cls.return_value = mock_video_instance

    #     mock_live_instance = MagicMock()
    #     mock_live_instance.match_batch.return_value = None
    #     mock_live_cls.return_value = mock_live_instance

    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     result = frm.replay_match_event(data)

    #     self.assertEqual(result["event_id"], None)
    #     mock_video_instance.video_event.assert_called_once()
    #     mock_live_instance.match_batch.assert_called_once()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_no_goal_events(self, mock_video_cls, mock_live_cls):
    #     """场景5:events 中没有任何 type='1' 的进球事件。"""
    #     mock_video_instance = MagicMock()
    #     mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "无候选事件"}
    #     mock_video_cls.return_value = mock_video_instance

    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     data["events"] = [
    #         {"id": "event_005", "type": "2", "url": "http://example.com/event5.mp4", "event_utc": 1020},
    #     ]
    #     result = frm.replay_match_event(data)

    #     self.assertEqual(result["event_id"], None)
    #     mock_video_instance.video_event.assert_called_once()
    #     mock_live_cls.return_value.match_batch.assert_not_called()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_empty_events(self, mock_video_cls, mock_live_cls):
    #     """场景6:events 为空列表。"""
    #     mock_video_instance = MagicMock()
    #     mock_video_instance.video_event.return_value = {"event_name": "无进球", "description": "空列表"}
    #     mock_video_cls.return_value = mock_video_instance

    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     data["events"] = []
    #     result = frm.replay_match_event(data)

    #     self.assertEqual(result["event_id"], None)
    #     mock_video_instance.video_event.assert_called_once()
    #     mock_live_cls.return_value.match_batch.assert_not_called()

    # @patch("utils.football_replay_match_live.FootballReplayMatchLive")
    # @patch("utils.football_replay_video_event_by_llm.FootballReplayVideoEvent")
    # def test_missing_replay_key(self, mock_video_cls, mock_live_cls):
    #     """场景7:data 缺少 replay 键,应抛出 KeyError。"""
    #     settings = _fake_settings(match_by_time_threshold=30)
    #     frm = FootballReplayMatch(settings)

    #     data = _base_data()
    #     del data["replay"]

    #     with self.assertRaises(KeyError):
    #         frm.replay_match_event(data)


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