mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-28 13:10:02 +08:00
246 lines
11 KiB
Python
246 lines
11 KiB
Python
import ast
|
|
import hashlib
|
|
import importlib
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import tempfile
|
|
from types import ModuleType, SimpleNamespace
|
|
import unittest
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / 'scripts'))
|
|
|
|
|
|
class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
|
async def asyncSetUp(self):
|
|
self.temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temporary.cleanup)
|
|
self.root = Path(self.temporary.name)
|
|
|
|
async def test_reproducible_complete_package_and_tamper_rejection(self):
|
|
from build_astrbot import build
|
|
from verify_astrbot import verify_files, read_package
|
|
first = build(ROOT, self.root / 'one')
|
|
second = build(ROOT, self.root / 'two')
|
|
self.assertEqual(first.read_bytes(), second.read_bytes())
|
|
files = read_package(first)
|
|
manifest = verify_files(files)
|
|
version = (ROOT / 'version.txt').read_text().strip()
|
|
self.assertEqual(manifest['version'], version)
|
|
self.assertEqual(json.loads(files['metadata.yaml'])['version'], version)
|
|
metadata = json.loads(files['metadata.yaml'])
|
|
self.assertEqual(metadata['display_name'], '农场')
|
|
self.assertEqual(metadata['support_platforms'], ['aiocqhttp'])
|
|
self.assertTrue(metadata['short_desc'])
|
|
self.assertEqual(files['logo.png'],
|
|
(ROOT / 'logo-astrbot.png').read_bytes())
|
|
self.assertEqual(files['logo-readme.png'],
|
|
(ROOT / 'logo.png').read_bytes())
|
|
self.assertIn('农场', metadata['tags'])
|
|
from io import BytesIO
|
|
from PIL import Image
|
|
with Image.open(BytesIO(files['logo.png'])) as logo:
|
|
self.assertEqual(logo.format, 'PNG')
|
|
self.assertEqual(logo.width, logo.height)
|
|
self.assertEqual(logo.size, (256, 256))
|
|
self.assertIn('A', logo.getbands())
|
|
self.assertEqual(logo.getchannel('A').getextrema()[0], 0)
|
|
self.assertEqual(files['CHANGELOG.md'], (
|
|
ROOT / 'integrations/astrbot/CHANGELOG.md'
|
|
).read_bytes())
|
|
readme = files['README.md'].decode()
|
|
self.assertIn('logo.png', readme)
|
|
self.assertIn('width="400"', readme)
|
|
self.assertNotIn('<!-- FARM_COMMANDS -->', readme)
|
|
self.assertIn('images/help-astrbot.png', readme)
|
|
self.assertIn('images/farm-preview.png', files)
|
|
self.assertFalse(any(name.endswith('/assets.zip') for name in files))
|
|
from verify_astrbot import verify_package
|
|
with patch('verify_astrbot.MAX_ARCHIVE_BYTES', 1):
|
|
with self.assertRaisesRegex(ValueError, '16 MiB'):
|
|
verify_package(first, marketplace=True)
|
|
self.assertEqual(files['runtime.py'], (
|
|
ROOT / 'integrations/shared/runtime.py'
|
|
).read_bytes())
|
|
for name, content in files.items():
|
|
self.assertNotIn('.git/', name)
|
|
self.assertNotIn('farm.sqlite3', name)
|
|
if name.endswith('.py'):
|
|
tree = ast.parse(content)
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, ast.ImportFrom):
|
|
self.assertFalse((node.module or '').startswith(
|
|
('nonebot', 'zhenxun'),
|
|
))
|
|
files['main.py'] += b'\n# tampered\n'
|
|
with self.assertRaisesRegex(ValueError, 'checksum'):
|
|
verify_files(files)
|
|
with self.assertRaises(FileExistsError):
|
|
build(ROOT, self.root / 'one')
|
|
expected = hashlib.sha256(first.read_bytes()).hexdigest()
|
|
self.assertIn(expected, first.with_suffix('.zip.sha256').read_text())
|
|
|
|
async def test_mock_host_commands_images_identity_and_shutdown(self):
|
|
from build_astrbot import build
|
|
from ci_font import choose_font
|
|
build(ROOT, self.root / 'host')
|
|
package = ModuleType('astrbot_farm_fixture')
|
|
package.__path__ = [str(self.root / 'host/repository')]
|
|
api = ModuleType('astrbot.api')
|
|
api.AstrBotConfig = dict
|
|
events = ModuleType('astrbot.api.event')
|
|
commands = {}
|
|
|
|
def command(name, alias=None):
|
|
def decorate(handler):
|
|
commands.update({c: handler for c in {name} | (alias or set())})
|
|
return handler
|
|
return decorate
|
|
|
|
events.filter = SimpleNamespace(command=command)
|
|
events.AstrMessageEvent = type('Event', (), {})
|
|
star = ModuleType('astrbot.api.star')
|
|
|
|
class Star:
|
|
def __init__(self, context):
|
|
self.context = context
|
|
|
|
star.Star, star.Context = Star, object
|
|
star.StarTools = SimpleNamespace(
|
|
get_data_dir=lambda name: self.root / 'plugin-data',
|
|
)
|
|
components = ModuleType('astrbot.api.message_components')
|
|
components.At = type('At', (), {})
|
|
components.Image = SimpleNamespace(fromBytes=lambda content: content)
|
|
modules = {m.__name__: m for m in (
|
|
package, api, events, star, components,
|
|
)}
|
|
with patch.dict(sys.modules, modules):
|
|
try:
|
|
main = importlib.import_module(package.__name__ + '.main')
|
|
plugin = main.FarmPlugin(object(), {
|
|
'font_path': 'ignored-custom.ttf',
|
|
'data_dir': str(self.root / 'existing-custom-data'),
|
|
'image_quality': 55,
|
|
})
|
|
self.assertEqual(plugin.runtime.data_dir,
|
|
(self.root / 'existing-custom-data').resolve())
|
|
self.assertIsNone(plugin.runtime.font_path)
|
|
# 使用真实开发素材预置缓存,模拟宿主测试不访问网络。
|
|
manifest = json.loads((ROOT /
|
|
'src/farm_render/data/asset-manifest.json').read_bytes())
|
|
cached = (plugin.runtime.data_dir / 'assets'
|
|
/ manifest['sha256'] / 'assets.zip')
|
|
cached.parent.mkdir(parents=True, exist_ok=True)
|
|
cached.write_bytes((ROOT /
|
|
'src/farm_render/data/assets.zip').read_bytes())
|
|
font_manifest = json.loads((ROOT /
|
|
'src/farm_render/data/font-manifest.json').read_bytes())
|
|
fonts = (plugin.runtime.data_dir / 'fonts'
|
|
/ font_manifest['sha256'])
|
|
fonts.mkdir(parents=True, exist_ok=True)
|
|
(fonts / 'Xiaolai-Regular.ttf').write_bytes(
|
|
Path(choose_font()).read_bytes())
|
|
(fonts / 'OFL-Xiaolai.txt').write_bytes((ROOT /
|
|
'src/farm_render/data/OFL-Xiaolai.txt').read_bytes())
|
|
await plugin.initialize()
|
|
self.assertEqual(set(commands), set(main.COMMANDS))
|
|
|
|
class Event:
|
|
message_obj = SimpleNamespace(message_id='one')
|
|
|
|
def __init__(self, text, platform_id='bot-a'):
|
|
self.text, self.platform_id = text, platform_id
|
|
|
|
def get_message_str(self):
|
|
return self.text
|
|
|
|
def get_self_id(self):
|
|
return 'bot'
|
|
|
|
def get_platform_id(self):
|
|
return self.platform_id
|
|
|
|
def get_platform_name(self):
|
|
return 'aiocqhttp'
|
|
|
|
def get_sender_id(self):
|
|
return '123456'
|
|
|
|
def get_sender_name(self):
|
|
return '离线测试'
|
|
|
|
def get_messages(self):
|
|
return []
|
|
|
|
def plain_result(self, text):
|
|
return text
|
|
|
|
def chain_result(self, chain):
|
|
return chain
|
|
|
|
async def call(text, platform_id='bot-a'):
|
|
return [r async for r in plugin.farm(
|
|
Event(text, platform_id),
|
|
)][0]
|
|
|
|
first = await call('开通农场')
|
|
self.assertIn('6块土地', first)
|
|
again = await call('开通农场')
|
|
self.assertIsInstance(again, str)
|
|
self.assertIn('开通农场', await call('我的种子', 'bot-b'))
|
|
from io import BytesIO
|
|
from PIL import Image
|
|
avatar_buffer = BytesIO()
|
|
Image.new('RGB', (64, 64), '#ee2222').save(
|
|
avatar_buffer, format='PNG',
|
|
)
|
|
avatar_bytes = avatar_buffer.getvalue()
|
|
with patch.object(plugin._avatars, 'get', new=AsyncMock(
|
|
return_value=avatar_bytes)) as avatar_get:
|
|
with patch.object(plugin.runtime, 'execute',
|
|
wraps=plugin.runtime.execute) as execute:
|
|
result = await call('我的农场 高清')
|
|
self.assertEqual(execute.call_args.kwargs['avatar'],
|
|
avatar_bytes)
|
|
await call('农场详述')
|
|
await call('地块编号')
|
|
await call('我的种子 文字')
|
|
self.assertIsNone(execute.call_args.kwargs['avatar'])
|
|
self.assertEqual(avatar_get.await_count, 3)
|
|
avatar_get.assert_awaited_with('aiocqhttp', '123456')
|
|
with Image.open(BytesIO(result[0])) as image:
|
|
self.assertEqual(image.width, 1536)
|
|
red, green, blue = image.getpixel((70, 70))[:3]
|
|
self.assertGreater(red, 180)
|
|
self.assertLess(green, 90)
|
|
self.assertLess(blue, 90)
|
|
self.assertIn('仅我的农场', await call('我的种子 高清'))
|
|
self.assertIn('参数有误', await call('我的种子 文字'))
|
|
self.assertTrue(
|
|
(self.root / 'existing-custom-data/farm.sqlite3').is_file(),
|
|
)
|
|
self.assertFalse(
|
|
(self.root / 'plugin-data/farm.sqlite3').exists(),
|
|
)
|
|
await plugin.terminate()
|
|
self.assertFalse(plugin.runtime.started)
|
|
plugin = main.FarmPlugin(object(), {
|
|
'data_dir': str(self.root / 'existing-custom-data'),
|
|
})
|
|
await plugin.initialize()
|
|
reopened = Event('开通农场')
|
|
reopened.message_obj = SimpleNamespace(message_id='after-reload')
|
|
response = [r async for r in plugin.farm(reopened)][0]
|
|
self.assertIn('农场已存在', response)
|
|
await plugin.terminate()
|
|
finally:
|
|
if 'plugin' in locals() and plugin.runtime.started:
|
|
await plugin.terminate()
|
|
for name in tuple(sys.modules):
|
|
if name.startswith(package.__name__ + '.'):
|
|
del sys.modules[name]
|