修复直播测试与 TTS 预热
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import asyncio
|
||||
import ctypes
|
||||
import logging
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from app.danmu_queue import SystemScheduler
|
||||
from app.danmu_queue import CommandHandler, QueueSystem, SystemScheduler
|
||||
|
||||
|
||||
class _FakeConfig:
|
||||
@@ -155,5 +157,167 @@ class StartupLiveCompensationTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
|
||||
|
||||
class LivehimePushVerificationTests(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
self.logger = logging.getLogger("test-livehime-push-verification")
|
||||
self.scheduler = SystemScheduler(
|
||||
_FakeConfig({
|
||||
"bilibili_push_window_keyword": "直播姬",
|
||||
"bilibili_push_click_x_ratio": 0.787,
|
||||
"bilibili_push_click_y_ratio": 0.927,
|
||||
"bilibili_stop_push_click_x_ratio": 0.787,
|
||||
"bilibili_stop_push_click_y_ratio": 0.927,
|
||||
}),
|
||||
self.logger,
|
||||
)
|
||||
|
||||
async def test_push_skips_click_when_bilibili_is_already_live(self):
|
||||
self.scheduler._get_bilibili_live_status = AsyncMock(return_value=1)
|
||||
self.scheduler._click_bilibili_live = AsyncMock()
|
||||
|
||||
self.assertTrue(await self.scheduler.push_bilibili_live())
|
||||
|
||||
self.scheduler._click_bilibili_live.assert_not_awaited()
|
||||
|
||||
async def test_push_clicks_then_waits_for_confirmed_live_status(self):
|
||||
self.scheduler._get_bilibili_live_status = AsyncMock(side_effect=[0, 0, 1])
|
||||
self.scheduler._click_bilibili_live = AsyncMock(return_value=True)
|
||||
|
||||
with patch("app.danmu_queue.asyncio.sleep", new_callable=AsyncMock):
|
||||
result = await self.scheduler.push_bilibili_live()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.scheduler._click_bilibili_live.assert_awaited_once()
|
||||
|
||||
async def test_stop_skips_click_when_bilibili_is_already_offline(self):
|
||||
self.scheduler._get_bilibili_live_status = AsyncMock(return_value=0)
|
||||
self.scheduler._click_bilibili_live = AsyncMock()
|
||||
|
||||
self.assertTrue(await self.scheduler.stop_bilibili_live())
|
||||
|
||||
self.scheduler._click_bilibili_live.assert_not_awaited()
|
||||
|
||||
async def test_live_status_timeout_returns_specific_error(self):
|
||||
self.scheduler._get_bilibili_live_status = AsyncMock(return_value=0)
|
||||
|
||||
with patch("app.danmu_queue.asyncio.sleep", new_callable=AsyncMock):
|
||||
result = await self.scheduler._wait_for_bilibili_live_status(
|
||||
1,
|
||||
timeout_seconds=0.01,
|
||||
)
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertIn("未确认开播", self.scheduler.last_live_action_error)
|
||||
|
||||
@patch("app.danmu_queue.os.name", "nt")
|
||||
async def test_click_restores_and_focuses_before_reading_physical_window_rect(self):
|
||||
hwnd = 12345
|
||||
user32 = MagicMock()
|
||||
call_order = []
|
||||
user32.IsIconic.return_value = True
|
||||
user32.GetForegroundWindow.return_value = hwnd
|
||||
user32.GetSystemMetrics.side_effect = lambda index: {
|
||||
76: 0,
|
||||
77: 0,
|
||||
78: 2560,
|
||||
79: 1440,
|
||||
}[index]
|
||||
user32.SetCursorPos.return_value = True
|
||||
user32.GetCursorPos.return_value = False
|
||||
user32.SetThreadDpiAwarenessContext.return_value = None
|
||||
|
||||
def show_window(_hwnd, _command):
|
||||
call_order.append("restore")
|
||||
return True
|
||||
|
||||
def get_window_rect(_hwnd, pointer):
|
||||
call_order.append("rect")
|
||||
rect = ctypes.cast(pointer, ctypes.POINTER(ctypes.wintypes.RECT)).contents
|
||||
rect.left = -11
|
||||
rect.top = -11
|
||||
rect.right = 2571
|
||||
rect.bottom = 1379
|
||||
return True
|
||||
|
||||
user32.ShowWindow.side_effect = show_window
|
||||
user32.GetWindowRect.side_effect = get_window_rect
|
||||
self.scheduler._find_window_by_keyword = MagicMock(return_value=hwnd)
|
||||
self.scheduler._force_foreground_window = MagicMock(return_value=True)
|
||||
|
||||
with (
|
||||
patch("app.danmu_queue.ctypes.windll.user32", user32),
|
||||
patch("app.danmu_queue.asyncio.sleep", new_callable=AsyncMock),
|
||||
):
|
||||
result = await self.scheduler._click_bilibili_live(
|
||||
x_ratio_key="bilibili_push_click_x_ratio",
|
||||
y_ratio_key="bilibili_push_click_y_ratio",
|
||||
action_name="开启推流按钮",
|
||||
)
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertLess(call_order.index("restore"), call_order.index("rect"))
|
||||
user32.SetCursorPos.assert_called_once_with(2021, 1277)
|
||||
self.assertEqual(user32.mouse_event.call_count, 2)
|
||||
|
||||
|
||||
class ManualLiveOverrideTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.system = QueueSystem.__new__(QueueSystem)
|
||||
self.system.logger = logging.getLogger("test-manual-live-override")
|
||||
self.system._manual_live_override_until_monotonic = 0.0
|
||||
self.system._manual_live_override_until_wall = 0.0
|
||||
self.system._bilibili_live_status = None
|
||||
self.system._bilibili_live_status_updated_monotonic = 0.0
|
||||
self.system._bilibili_live_status_updated_wall = 0.0
|
||||
|
||||
def test_manual_live_override_allows_commands_outside_schedule(self):
|
||||
handler = CommandHandler.__new__(CommandHandler)
|
||||
handler.system = self.system
|
||||
handler.config = SimpleNamespace(
|
||||
system_cfg={"live_start_time": "08:30", "live_end_time": "23:30"}
|
||||
)
|
||||
|
||||
with patch("app.danmu_queue.time.monotonic", return_value=100.0):
|
||||
self.system.enable_manual_live_override(300)
|
||||
self.assertTrue(handler._is_live_time())
|
||||
snapshot = self.system.manual_live_override_snapshot()
|
||||
|
||||
self.assertTrue(snapshot["active"])
|
||||
self.assertGreater(snapshot["until"], 0)
|
||||
|
||||
def test_clear_manual_live_override_restores_schedule_gate(self):
|
||||
handler = CommandHandler.__new__(CommandHandler)
|
||||
handler.system = self.system
|
||||
handler.config = SimpleNamespace(
|
||||
system_cfg={"live_start_time": "08:30", "live_end_time": "23:30"}
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.danmu_queue.time.monotonic", return_value=100.0),
|
||||
patch("app.danmu_queue.datetime") as mocked_datetime,
|
||||
):
|
||||
mocked_datetime.now.return_value = datetime(2026, 8, 16, 3, 30)
|
||||
mocked_datetime.strptime = datetime.strptime
|
||||
self.system.enable_manual_live_override(300)
|
||||
self.system.clear_manual_live_override()
|
||||
self.assertFalse(handler._is_live_time())
|
||||
|
||||
def test_confirmed_bilibili_live_status_allows_commands_outside_schedule(self):
|
||||
handler = CommandHandler.__new__(CommandHandler)
|
||||
handler.system = self.system
|
||||
handler.config = SimpleNamespace(
|
||||
system_cfg={"live_start_time": "08:30", "live_end_time": "23:30"}
|
||||
)
|
||||
|
||||
with (
|
||||
patch("app.danmu_queue.time.monotonic", return_value=100.0),
|
||||
patch("app.danmu_queue.datetime") as mocked_datetime,
|
||||
):
|
||||
mocked_datetime.now.return_value = datetime(2026, 8, 16, 3, 30)
|
||||
mocked_datetime.strptime = datetime.strptime
|
||||
self.system.set_bilibili_live_status(1)
|
||||
self.assertTrue(handler._is_live_time())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user