mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-11-02 18:50:08 +08:00
fixed # 适配AstrBot
This commit is contained in:
@@ -31,8 +31,41 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
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('/main/logo-readme.png', readme)
|
||||
self.assertIn('width="400"', readme)
|
||||
self.assertNotIn('<!-- FARM_COMMANDS -->', readme)
|
||||
self.assertIn('/农场帮助 [文字]', readme)
|
||||
self.assertIn('/开垦 条件', readme)
|
||||
self.assertIn('raw.githubusercontent.com', 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/zhenxun/runtime.py'
|
||||
ROOT / 'integrations/shared/runtime.py'
|
||||
).read_bytes())
|
||||
for name, content in files.items():
|
||||
self.assertNotIn('.git/', name)
|
||||
@@ -93,6 +126,14 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
plugin = main.FarmPlugin(object(), {
|
||||
'font_path': str(choose_font()), 'image_quality': 55,
|
||||
})
|
||||
# 使用真实开发素材预置缓存,模拟宿主测试不访问网络。
|
||||
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())
|
||||
await plugin.initialize()
|
||||
self.assertEqual(set(commands), set(main.COMMANDS))
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ spec.loader.exec_module(migration)
|
||||
|
||||
|
||||
class ConfigMigrationTests(unittest.TestCase):
|
||||
def test_zhenxun_registers_global_quality_in_plugin_metadata(self):
|
||||
def test_zhenxun_registers_quality_and_resource_configuration(self):
|
||||
plugin = ModuleType('nonebot.plugin')
|
||||
plugin.PluginMetadata = lambda **values: SimpleNamespace(**values)
|
||||
utils = ModuleType('zhenxun.configs.utils')
|
||||
@@ -42,11 +42,20 @@ class ConfigMigrationTests(unittest.TestCase):
|
||||
exec(compile(source.read_text(encoding='utf-8'), str(source),
|
||||
'exec'), namespace)
|
||||
configs = namespace['__plugin_meta__'].extra['configs']
|
||||
self.assertEqual(len(configs), 1)
|
||||
self.assertEqual(configs[0].key, '农场图片质量')
|
||||
self.assertEqual(configs[0].value, 75)
|
||||
self.assertEqual(configs[0].default_value, 75)
|
||||
self.assertIs(configs[0].type, int)
|
||||
self.assertEqual(len(configs), 3)
|
||||
by_key = {config.key: config for config in configs}
|
||||
self.assertEqual(set(by_key), {
|
||||
'农场图片质量', '资源获取地址', 'farm_service 地址',
|
||||
})
|
||||
for key, expected, value_type in (
|
||||
('农场图片质量', 75, int),
|
||||
('资源获取地址', 'Github', str),
|
||||
('farm_service 地址', '', str),
|
||||
):
|
||||
with self.subTest(key=key):
|
||||
self.assertEqual(by_key[key].value, expected)
|
||||
self.assertEqual(by_key[key].default_value, expected)
|
||||
self.assertIs(by_key[key].type, value_type)
|
||||
|
||||
@unittest.skipUnless(importlib.util.find_spec('pydantic'),
|
||||
'NoneBot host config requires Pydantic')
|
||||
@@ -67,6 +76,21 @@ class ConfigMigrationTests(unittest.TestCase):
|
||||
spec.loader.exec_module(module)
|
||||
config = module.Config
|
||||
self.assertEqual(config().farm_image_quality, 75)
|
||||
self.assertEqual(config().farm_resource_source, 'Github')
|
||||
self.assertEqual(config().farm_service_url, '')
|
||||
for source in ('Github', 'Gitea', 'farm_service'):
|
||||
selected = config(
|
||||
farm_resource_source=source,
|
||||
farm_service_url='https://farm.example/service',
|
||||
)
|
||||
self.assertEqual(selected.farm_resource_source, source)
|
||||
self.assertEqual(selected.farm_service_url,
|
||||
'https://farm.example/service')
|
||||
for source in ('github', 'unknown', '', None):
|
||||
with self.subTest(source=source), self.assertRaises(
|
||||
library.ValidationError,
|
||||
):
|
||||
config(farm_resource_source=source)
|
||||
for value in (1, 95):
|
||||
actual = config(farm_image_quality=value).farm_image_quality
|
||||
self.assertEqual(actual, value)
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import asyncio
|
||||
from dataclasses import replace
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from farm_core.commands import (
|
||||
COMMAND_SPECS, CommandSpec, command_map, command_specs, help_groups,
|
||||
help_text,
|
||||
)
|
||||
from farm_render import FarmRenderer, RenderService
|
||||
from farm_render.help import help_layout, render_help, wrap_text
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
|
||||
from ci_font import choose_font
|
||||
|
||||
|
||||
class HelpMenuTests(unittest.IsolatedAsyncioTestCase):
|
||||
def setUp(self):
|
||||
self.renderer = FarmRenderer(choose_font())
|
||||
|
||||
def test_registration_and_help_share_platform_definitions(self):
|
||||
exchange = CommandSpec(
|
||||
'购买农场币', 'fixture_exchange', '金币兑换',
|
||||
'购买农场币 数量', '仅用于测试已接入的平台命令。',
|
||||
platforms=('zhenxun',),
|
||||
)
|
||||
specs = (*COMMAND_SPECS, exchange)
|
||||
for platform in ('zhenxun', 'nonebot', 'astrbot'):
|
||||
selected = command_specs(platform, specs)
|
||||
mapping = command_map(platform, specs)
|
||||
self.assertEqual('购买农场币' in mapping, platform == 'zhenxun')
|
||||
self.assertEqual('金币兑换' in help_text(selected),
|
||||
platform == 'zhenxun')
|
||||
for spec in selected:
|
||||
self.assertEqual(mapping[spec.name], spec.action)
|
||||
self.assertIn(spec.usage, help_text(selected))
|
||||
self.assertEqual(mapping['除虫'], mapping['农场除虫'])
|
||||
with self.assertRaises(ValueError):
|
||||
command_map('zhenxun', (*specs, exchange))
|
||||
|
||||
def test_long_future_commands_wrap_without_loss_and_grow(self):
|
||||
specs = command_specs('astrbot')
|
||||
long = '很长的新增指令参数与说明' * 8
|
||||
lines = wrap_text(long, self.renderer.font_path, 28, 484)
|
||||
self.assertEqual(''.join(lines), long)
|
||||
from farm_render.canvas import Canvas
|
||||
self.assertTrue(all(Canvas.measure_text(
|
||||
line, self.renderer.font_path, 28,
|
||||
) <= 484 for line in lines))
|
||||
extra = tuple(CommandSpec(
|
||||
f'新增{i}', f'action{i}', '新增功能', long, long,
|
||||
) for i in range(10))
|
||||
before = help_layout(specs, self.renderer.font_path)[1]
|
||||
after = help_layout((*specs, *extra), self.renderer.font_path)[1]
|
||||
self.assertGreater(after, before)
|
||||
grouped = help_groups((*specs, *extra))
|
||||
self.assertEqual(grouped[-1][0], '新增功能')
|
||||
|
||||
async def test_image_codec_quality_cache_and_concurrent_dedup(self):
|
||||
specs = command_specs('astrbot')
|
||||
service = RenderService(self.renderer)
|
||||
try:
|
||||
with patch('farm_render.service.render_help',
|
||||
wraps=render_help) as fn:
|
||||
results = await asyncio.gather(*(
|
||||
service.help_menu(specs, 'astrbot', 'JPEG', 75)
|
||||
for _ in range(4)
|
||||
))
|
||||
self.assertEqual(fn.call_count, 1)
|
||||
self.assertTrue(all(r == results[0] for r in results))
|
||||
self.assertEqual(await service.help_menu(
|
||||
specs, 'astrbot', 'JPEG', 75,
|
||||
), results[0])
|
||||
self.assertEqual(fn.call_count, 1)
|
||||
changed = (*specs, replace(specs[-1], name='扩展帮助',
|
||||
usage='扩展帮助'))
|
||||
await service.help_menu(changed, 'astrbot', 'JPEG', 75)
|
||||
await service.help_menu(specs, 'nonebot', 'JPEG', 75)
|
||||
await service.help_menu(specs, 'astrbot', 'JPEG', 90)
|
||||
self.assertEqual(fn.call_count, 4)
|
||||
with Image.open(BytesIO(results[0])) as image:
|
||||
self.assertEqual(image.width, 1200)
|
||||
self.assertGreater(image.height, 1200)
|
||||
self.assertEqual(image.format, 'JPEG')
|
||||
png = await service.help_menu(specs, 'astrbot', 'PNG', 75)
|
||||
with Image.open(BytesIO(png)) as image:
|
||||
self.assertEqual(image.format, 'PNG')
|
||||
finally:
|
||||
await service.close()
|
||||
with self.assertRaises(RuntimeError):
|
||||
await service.help_menu(specs, 'astrbot')
|
||||
@@ -26,15 +26,17 @@ class NoneBotPackageTests(unittest.TestCase):
|
||||
other = build(ROOT, self.root / "second")
|
||||
self.assertEqual(self.bundle.read_bytes(), other.read_bytes())
|
||||
self.assertEqual(self.files["nonebot_plugin_farm/runtime.py"], (
|
||||
ROOT / "integrations/zhenxun/runtime.py"
|
||||
ROOT / "integrations/shared/runtime.py"
|
||||
).read_bytes())
|
||||
for suffix in ("farm_core/data/plants.json",
|
||||
"farm_render/data/assets.zip"):
|
||||
"farm_render/data/asset-manifest.json"):
|
||||
self.assertTrue(any(name.endswith(suffix) for name in self.files))
|
||||
self.assertEqual(verify_files(self.files)["target"], "nonebot")
|
||||
self.assertIn("pyproject.toml", self.files)
|
||||
self.assertIn(".github/workflows/publish.yaml", self.files)
|
||||
self.assertFalse(any("legacy/" in name for name in self.files))
|
||||
self.assertFalse(any(name.endswith('/assets.zip')
|
||||
for name in self.files))
|
||||
with self.assertRaises(FileExistsError):
|
||||
build(ROOT, self.root / "built")
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
sys.modules[self.package] = module
|
||||
self.module = importlib.import_module(self.package + ".runtime")
|
||||
self.runtime = self.module.Runtime(self.root / "data", choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
await self.runtime.start()
|
||||
self.context = dict(bot_id="bot", platform="OneBot V11", user_id="1",
|
||||
message_id="registration", display_name="离线玩家")
|
||||
@@ -39,6 +40,15 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
del sys.modules[name]
|
||||
self.temporary.cleanup()
|
||||
|
||||
def seed_assets(self, runtime):
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.bundle',
|
||||
)
|
||||
target = (runtime.data_dir / 'assets'
|
||||
/ bundle.expected_manifest()['sha256'] / 'assets.zip')
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / 'src/farm_render/data/assets.zip', target)
|
||||
|
||||
async def call(self, action, arguments="", message="event", **changes):
|
||||
return await self.runtime.execute(
|
||||
action, arguments,
|
||||
@@ -50,7 +60,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
from PIL import Image
|
||||
from unittest.mock import AsyncMock
|
||||
self.assertEqual(self.module.COMMANDS['地块编号'], 'plot_ids')
|
||||
self.assertIn('地块编号', await self.call('help'))
|
||||
self.assertIn('地块编号', await self.call('help', '文字'))
|
||||
first = await self.call('register')
|
||||
self.assertIn('6块土地', first)
|
||||
self.assertIn('地块编号', first)
|
||||
@@ -71,9 +81,47 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn('参数有误', await self.call('plot_ids', '1'))
|
||||
self.assertIn('开通农场', await self.call('plot_ids', user_id='other'))
|
||||
|
||||
async def test_dynamic_help_image_text_fallback_and_no_player_access(self):
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from unittest.mock import AsyncMock
|
||||
with patch.object(self.runtime.store, 'migrate_legacy',
|
||||
new=AsyncMock()) as migrate:
|
||||
image = await self.call('help', message=None)
|
||||
migrate.assert_not_awaited()
|
||||
with Image.open(BytesIO(image)) as decoded:
|
||||
self.assertEqual(decoded.width, 1200)
|
||||
text = await self.call('help', '文字', message=None)
|
||||
for command in self.module.COMMANDS:
|
||||
self.assertIn(command, text)
|
||||
self.assertNotIn('购买农场币', text)
|
||||
self.assertIn('仅我的农场支持高清',
|
||||
await self.call('help', '高清'))
|
||||
self.assertIn('参数有误', await self.call('help', '2'))
|
||||
with patch.object(self.runtime.rendering, 'help_menu',
|
||||
new=AsyncMock(side_effect=OSError('fixture'))):
|
||||
self.assertEqual(await self.call('help'), text)
|
||||
rendering = self.runtime.rendering
|
||||
self.runtime.rendering = None
|
||||
try:
|
||||
self.assertEqual(await self.call('help'), text)
|
||||
finally:
|
||||
self.runtime.rendering = rendering
|
||||
|
||||
async def test_help_uses_live_global_quality(self):
|
||||
from unittest.mock import AsyncMock
|
||||
with patch.object(self.runtime.rendering, 'help_menu',
|
||||
new=AsyncMock(return_value=b'image')) as render:
|
||||
self.runtime.image_quality_provider = lambda: 91
|
||||
self.assertEqual(await self.call('help'), b'image')
|
||||
self.assertEqual(render.await_args.args[-1], 91)
|
||||
self.runtime.image_quality_provider = lambda: False
|
||||
self.assertIn('图片质量配置无效', await self.call('help'))
|
||||
self.assertIsInstance(await self.call('help', '文字'), str)
|
||||
|
||||
async def test_reclaim_conditions_charge_replay_and_arguments(self):
|
||||
self.assertEqual(self.module.COMMANDS['开垦'], 'reclaim')
|
||||
self.assertIn('开垦 条件', await self.call('help'))
|
||||
self.assertIn('开垦 条件', await self.call('help', '文字'))
|
||||
await self.call('register')
|
||||
who = self.runtime_identity()
|
||||
asset = importlib.import_module(
|
||||
@@ -226,7 +274,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
))
|
||||
for action in ('seeds', 'crops'):
|
||||
self.assertIsInstance(await self.call(action, '1 文字'), str)
|
||||
help_text = await self.call('help')
|
||||
help_text = await self.call('help', '文字')
|
||||
self.assertIn('仅我的农场支持高清', help_text)
|
||||
self.assertNotIn('地块编号 [高清]', help_text)
|
||||
self.assertNotIn('[高清/文字]', help_text)
|
||||
@@ -328,7 +376,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_pest_command_clears_pests_and_preserves_growth(self):
|
||||
self.assertEqual(self.module.COMMANDS["农场除虫"], "clear_pests")
|
||||
help_text = await self.call("help")
|
||||
help_text = await self.call("help", "文字")
|
||||
self.assertIn("每只虫", help_text)
|
||||
self.assertIn("间隔至少2小时", help_text)
|
||||
self.runtime.service.clock = lambda: 1000
|
||||
@@ -414,6 +462,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
other = self.module.Runtime(
|
||||
self.root / "without-font", self.root / "missing.ttf",
|
||||
)
|
||||
self.seed_assets(other)
|
||||
await other.start()
|
||||
try:
|
||||
response = await other.execute("farm", "", **self.context)
|
||||
@@ -427,6 +476,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
(fonts / "HYWenHei-85W.ttf").write_bytes(b"broken")
|
||||
shutil.copyfile(choose_font(), fonts / "msyh.ttf")
|
||||
other = self.module.Runtime(self.root / "default-font", font_dir=fonts)
|
||||
self.seed_assets(other)
|
||||
await other.start()
|
||||
try:
|
||||
self.assertIsNotNone(other.rendering)
|
||||
@@ -446,6 +496,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.root / "explicit-font", explicit,
|
||||
font_dir=self.root / "not-present",
|
||||
)
|
||||
self.seed_assets(other)
|
||||
await other.start()
|
||||
try:
|
||||
self.assertEqual(other.rendering.renderer.font_path, explicit)
|
||||
@@ -465,6 +516,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
old = self.root / 'directories/farm_preview'
|
||||
target = old.with_name('farm_db')
|
||||
previous = self.module.Runtime(old, choose_font())
|
||||
self.seed_assets(previous)
|
||||
await previous.start()
|
||||
try:
|
||||
await previous.execute('register', '旧农场', **self.context)
|
||||
@@ -474,6 +526,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
before = old_database.read_bytes()
|
||||
target.mkdir()
|
||||
current = self.module.Runtime(target, choose_font())
|
||||
self.seed_assets(current)
|
||||
await current.start()
|
||||
try:
|
||||
self.assertEqual(old_database.read_bytes(), before)
|
||||
@@ -557,12 +610,16 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
platform.PlatformUtils = SimpleNamespace()
|
||||
config = ModuleType("zhenxun.configs.config")
|
||||
quality_values = {'value': 49}
|
||||
|
||||
def configured_value(key, default, **kwargs):
|
||||
if key != '农场图片质量':
|
||||
return default
|
||||
value = quality_values['value']
|
||||
return value if not kwargs['build_model'] else int(value)
|
||||
|
||||
config.Config = SimpleNamespace(
|
||||
get=lambda module: SimpleNamespace(
|
||||
configs={}, get=lambda key, default, **kwargs: (
|
||||
quality_values['value'] if not kwargs['build_model']
|
||||
else int(quality_values['value'])
|
||||
),
|
||||
configs={}, get=configured_value,
|
||||
),
|
||||
get_config=lambda *args, **kwargs: int(quality_values['value']),
|
||||
_simple_data={},
|
||||
@@ -584,6 +641,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
(self.root / 'farm_db').resolve())
|
||||
self.assertEqual(set(hooks["commands"]), set(self.module.COMMANDS))
|
||||
self.assertEqual(len(hooks["start"]), 1)
|
||||
self.assertEqual(imported.resource_options(), ('Github', ''))
|
||||
self.seed_assets(imported.runtime)
|
||||
await hooks["start"][0]()
|
||||
try:
|
||||
bot = SimpleNamespace(self_id="bot", adapter=SimpleNamespace(
|
||||
|
||||
Reference in New Issue
Block a user