Files
helper/tests/test_text_pacing.py
T
2026-10-15 18:27:46 +08:00

183 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""文本节奏、统一发送入口及等待期间的退出/重连验证;不连接微信。"""
from __future__ import annotations
import threading
import time
import unittest
from unittest.mock import Mock, patch
from wxbot_framework.pacing import TextSendDelay
from wxbot_framework.sdk import CapabilityError, LegacyService, Message, PluginContext, ServiceFacade
class MemoryBackend:
def __init__(self, session="original"):
self.session_id = session
self.sent = []
self.stopped = False
def send_text(self, chat, text):
self.sent.append((chat, text, time.monotonic()))
return True
def stop(self):
self.stopped = True
return {"dll_unloaded": True}
class RecordingCancel:
def __init__(self):
self.waits = []
self.cancelled = False
def wait(self, seconds):
self.waits.append(seconds)
return self.cancelled
def is_set(self):
return self.cancelled
def set(self):
self.cancelled = True
class TextPacingTests(unittest.TestCase):
def test_character_groups_include_chinese_symbols_and_short_text(self):
pacing = TextSendDelay.from_config()
with patch("wxbot_framework.pacing.random.uniform", return_value=0.2):
self.assertAlmostEqual(pacing.seconds_for("你好🌸 abc!"), 0.2)
self.assertAlmostEqual(pacing.seconds_for("字" * 10), 0.2)
self.assertAlmostEqual(pacing.seconds_for("字" * 11), 0.4)
self.assertAlmostEqual(pacing.seconds_for("字" * 100), 2.0)
self.assertEqual(TextSendDelay.from_config({"enabled": False}).seconds_for("你好"), 0)
def test_configuration_rejects_invalid_random_ranges_and_group_size(self):
for options in ([], {"enabled": 1}, {"characters_per_group": 0},
{"characters_per_group": True}, {"min_seconds": -1},
{"max_seconds": float("inf")}, {"max_seconds": float("nan")},
{"min_seconds": True}, {"min_seconds": 0.4, "max_seconds": 0.3}):
with self.subTest(options=options), self.assertRaises(ValueError):
TextSendDelay.from_config(options)
def test_all_plugin_text_apis_wait_and_other_payloads_do_not(self):
backend = MemoryBackend()
service = ServiceFacade()
service.bind(backend)
cancel = RecordingCancel()
service._cancel = cancel
ctx = PluginContext("test", {}, service, {"text.send"})
legacy = LegacyService(service, backend.session_id)
message = Message(text="test", chat_id="wxid_friend", session_id=backend.session_id)
with patch("wxbot_framework.pacing.random.uniform", return_value=0.2):
ctx.reply(message, "你好")
ctx.send_text("12345@chatroom", "群聊")
legacy.send_text("filehelper", "旧插件")
legacy.send_payload({"type": 11237, "data": {"to_wxid": "filehelper", "content": "文本"}})
with self.assertRaises(CapabilityError):
legacy.send_payload({"type": 99999})
self.assertEqual(cancel.waits, [0.2] * 4)
self.assertEqual(len(backend.sent), 4)
service.close()
def test_invalid_text_is_rejected_before_wait_or_native_send(self):
backend = MemoryBackend()
service = ServiceFacade()
service.bind(backend)
cancel = RecordingCancel()
service._cancel = cancel
for text in (None, "", "含\0字符", "字" * 342):
with self.subTest(text_type=type(text)), self.assertRaises(ValueError):
service.send_text("filehelper", text)
self.assertEqual(cancel.waits, [])
self.assertEqual(backend.sent, [])
service.close()
def test_two_concurrent_plugins_each_wait_between_native_sends(self):
backend = MemoryBackend()
service = ServiceFacade(text_delay={"min_seconds": 0.08, "max_seconds": 0.08})
service.bind(backend)
threads = [threading.Thread(target=service.send_text, args=("filehelper", str(i))) for i in range(2)]
started = time.monotonic()
for thread in threads:
thread.start()
for thread in threads:
thread.join(2)
self.assertFalse(thread.is_alive())
self.assertEqual(len(backend.sent), 2)
self.assertGreaterEqual(backend.sent[0][2] - started, 0.07)
self.assertGreaterEqual(backend.sent[1][2] - backend.sent[0][2], 0.07)
service.close()
def test_close_interrupts_wait_without_blocking_connection_queries(self):
backend = MemoryBackend()
service = ServiceFacade()
service.bind(backend)
waiting, errors = threading.Event(), []
def delay(_text):
waiting.set()
return 30
service.text_delay = Mock(seconds_for=delay)
thread = self._start_send(service, errors)
self.assertTrue(waiting.wait(1))
self.assertTrue(service.connected)
service.close()
thread.join(1)
self.assertFalse(thread.is_alive())
self.assertEqual(len(errors), 1)
self.assertEqual(backend.sent, [])
self.assertTrue(backend.stopped)
def test_pending_send_cannot_move_to_new_connection_even_without_session_argument(self):
original, replacement = MemoryBackend(), MemoryBackend("replacement")
service = ServiceFacade()
service.bind(original)
waiting, errors = threading.Event(), []
def delay(_text):
waiting.set()
return 30
service.text_delay = Mock(seconds_for=delay)
thread = self._start_send(service, errors)
self.assertTrue(waiting.wait(1))
service.close()
service.bind(replacement)
thread.join(1)
self.assertFalse(thread.is_alive())
self.assertEqual(len(errors), 1)
self.assertEqual(original.sent, [])
self.assertEqual(replacement.sent, [])
service.text_delay = TextSendDelay.from_config({"enabled": False})
service.send_text("filehelper", "新连接")
self.assertEqual(replacement.sent[0][:2], ("filehelper", "新连接"))
service.close()
def test_shutdown_cancellation_rejects_new_sends_before_backend_cleanup(self):
backend = MemoryBackend()
service = ServiceFacade(text_delay={"enabled": False})
service.bind(backend)
service.cancel_pending()
with self.assertRaises(RuntimeError):
service.send_text("filehelper", "退出期间不再发消息")
self.assertEqual(backend.sent, [])
self.assertFalse(backend.stopped)
service.close()
self.assertTrue(backend.stopped)
@staticmethod
def _start_send(service, errors):
def send():
try:
service.send_text("filehelper", "等待发送")
except RuntimeError as error:
errors.append(error)
thread = threading.Thread(target=send, daemon=True)
thread.start()
return thread
if __name__ == "__main__":
unittest.main()