mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-30 22:30:01 +08:00
add # 新增使用开源字体
This commit is contained in:
@@ -52,12 +52,10 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
ROOT / 'integrations/astrbot/CHANGELOG.md'
|
||||
).read_bytes())
|
||||
readme = files['README.md'].decode()
|
||||
self.assertIn('/main/logo-readme.png', readme)
|
||||
self.assertIn('logo.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/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
|
||||
@@ -124,8 +122,13 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
try:
|
||||
main = importlib.import_module(package.__name__ + '.main')
|
||||
plugin = main.FarmPlugin(object(), {
|
||||
'font_path': str(choose_font()), 'image_quality': 55,
|
||||
'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())
|
||||
@@ -134,6 +137,15 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
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))
|
||||
|
||||
@@ -207,12 +219,24 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertLess(green, 90)
|
||||
self.assertLess(blue, 90)
|
||||
self.assertIn('仅我的农场', await call('我的种子 高清'))
|
||||
self.assertIn('暂无种子', await call('我的种子 文字'))
|
||||
self.assertIn('参数有误', await call('我的种子 文字'))
|
||||
self.assertTrue(
|
||||
(self.root / 'plugin-data/farm.sqlite3').is_file(),
|
||||
(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()
|
||||
|
||||
@@ -25,7 +25,7 @@ class BuildPluginTests(unittest.TestCase):
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'Windows entry points')
|
||||
def test_cmd_entries_and_nonzero_failure(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
with tempfile.TemporaryDirectory(prefix='farm build ') as temporary:
|
||||
root = Path(temporary)
|
||||
env = {**os.environ, 'FARM_PYTHON': sys.executable,
|
||||
'FARM_BUILD_NO_PAUSE': '1'}
|
||||
|
||||
@@ -78,6 +78,10 @@ class ConfigMigrationTests(unittest.TestCase):
|
||||
self.assertEqual(config().farm_image_quality, 75)
|
||||
self.assertEqual(config().farm_resource_source, 'Github')
|
||||
self.assertEqual(config().farm_service_url, '')
|
||||
self.assertEqual(str(config(
|
||||
farm_data_dir='existing-custom-data',
|
||||
).farm_data_dir), 'existing-custom-data')
|
||||
self.assertIsNone(config(farm_data_dir='').farm_data_dir)
|
||||
for source in ('Github', 'Gitea', 'farm_service'):
|
||||
selected = config(
|
||||
farm_resource_source=source,
|
||||
|
||||
@@ -18,9 +18,24 @@ class DataDirectoryTests(unittest.TestCase):
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.base = Path(self.temp.name)
|
||||
|
||||
def test_default_and_explicit_directory(self):
|
||||
def test_fixed_directory(self):
|
||||
self.assertEqual(
|
||||
self.module.data_directory(self.base), self.base / 'farm_db',
|
||||
)
|
||||
custom = self.base / 'custom'
|
||||
self.assertEqual(self.module.data_directory(self.base, custom), custom)
|
||||
self.assertEqual(self.module.data_directory(self.base, ''),
|
||||
self.base / 'farm_db')
|
||||
|
||||
def test_nonebot_directory_is_fixed(self):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'nonebot_farm_paths', ROOT / 'integrations/nonebot/data_paths.py',
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
self.assertEqual(module.data_directory(self.base),
|
||||
self.base / 'nonebot_plugin_farm/farm_db')
|
||||
custom = self.base / 'custom'
|
||||
self.assertEqual(module.data_directory(self.base, custom), custom)
|
||||
self.assertEqual(module.data_directory(self.base, ''),
|
||||
self.base / 'nonebot_plugin_farm/farm_db')
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import hashlib
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
from farm_render import font_bundle
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from ci_font import choose_font
|
||||
|
||||
|
||||
class FontBundleTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.font = Path(choose_font()).read_bytes()
|
||||
cls.license = (ROOT / "src/farm_render/data/OFL-Xiaolai.txt").read_bytes()
|
||||
output = BytesIO()
|
||||
with ZipFile(output, "w", ZIP_DEFLATED) as archive:
|
||||
archive.writestr("Xiaolai-Regular.ttf", cls.font)
|
||||
archive.writestr("OFL-Xiaolai.txt", cls.license)
|
||||
cls.archive = output.getvalue()
|
||||
|
||||
def setUp(self):
|
||||
directory = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(directory.cleanup)
|
||||
self.root = Path(directory.name)
|
||||
manifest = font_bundle.expected_manifest()
|
||||
manifest.update(
|
||||
sha256=hashlib.sha256(self.archive).hexdigest(),
|
||||
size_bytes=len(self.archive),
|
||||
)
|
||||
self.manifest = manifest
|
||||
patcher = patch.object(font_bundle, "expected_manifest",
|
||||
return_value=manifest)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
def download(self, url, destination, limit):
|
||||
self.assertEqual(limit, len(self.archive))
|
||||
destination.write(self.archive)
|
||||
|
||||
def test_service_download_cache_and_license_repair(self):
|
||||
with patch.object(font_bundle, "_copy_remote",
|
||||
side_effect=self.download) as fetch:
|
||||
path = font_bundle.ensure_font(
|
||||
self.root, "farm_service", "https://farm.example/prefix/",
|
||||
)
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
"https://farm.example/prefix/v1/fonts/"
|
||||
+ self.manifest["sha256"] + "/assets.zip")
|
||||
self.assertEqual(path.read_bytes(), self.font)
|
||||
license_path = path.with_name("OFL-Xiaolai.txt")
|
||||
self.assertEqual(license_path.read_bytes(), self.license)
|
||||
path.write_bytes(b"corrupt")
|
||||
license_path.unlink()
|
||||
font_bundle.ensure_font(self.root)
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
self.assertEqual(path.read_bytes(), self.font)
|
||||
self.assertEqual(license_path.read_bytes(), self.license)
|
||||
|
||||
def test_corrupt_download_leaves_no_font_or_partial(self):
|
||||
def corrupt(url, destination, limit):
|
||||
destination.write(b"bad")
|
||||
with patch.object(font_bundle, "_copy_remote", side_effect=corrupt):
|
||||
with self.assertRaisesRegex(ValueError, "SHA256"):
|
||||
font_bundle.ensure_font(self.root)
|
||||
self.assertFalse(list(self.root.rglob("*.ttf")))
|
||||
self.assertFalse(list(self.root.rglob("*.download")))
|
||||
|
||||
def test_mirrors_follow_selected_resource_source(self):
|
||||
for source in ("Github", "Gitea"):
|
||||
with patch.object(font_bundle, "_copy_remote",
|
||||
side_effect=self.download) as fetch:
|
||||
font_bundle.ensure_font(self.root / source, source)
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
self.manifest["sources"][source.lower()])
|
||||
@@ -0,0 +1,56 @@
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'scripts'))
|
||||
|
||||
|
||||
class HelpPackageTests(unittest.TestCase):
|
||||
def test_prepared_workflow_image_is_used_and_wrong_platform_rejected(self):
|
||||
from build_zhenxun import build
|
||||
from help_image import build_help_image
|
||||
from verify_zhenxun import read_package
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
folder = Path(temporary)
|
||||
prepared = folder / 'help-zhenxun.png'
|
||||
content = build_help_image(ROOT, 'zhenxun')
|
||||
prepared.write_bytes(content)
|
||||
with patch('help_image.subprocess',
|
||||
SimpleNamespace(run=Mock())) as regenerate:
|
||||
archive = build(ROOT, folder / 'built', help_image=prepared)
|
||||
regenerate.run.assert_not_called()
|
||||
self.assertEqual(
|
||||
read_package(archive)['images/help-zhenxun.png'], content,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, 'different platform'):
|
||||
build_help_image(ROOT, 'astrbot', prepared)
|
||||
|
||||
def test_all_packages_embed_matching_generated_help_and_readme(self):
|
||||
import importlib
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
for platform in ('zhenxun', 'nonebot', 'astrbot'):
|
||||
with self.subTest(platform=platform):
|
||||
builder = importlib.import_module('build_' + platform)
|
||||
verifier = importlib.import_module('verify_' + platform)
|
||||
archive = builder.build(ROOT, Path(temporary) / platform)
|
||||
files = verifier.read_package(archive)
|
||||
image_name = f'images/help-{platform}.png'
|
||||
self.assertIn(image_name, files.keys())
|
||||
self.assertIn('./' + image_name,
|
||||
files['README.md'].decode('utf-8'))
|
||||
for other in ('zhenxun', 'nonebot', 'astrbot'):
|
||||
if other != platform:
|
||||
self.assertNotIn(f'images/help-{other}.png',
|
||||
files.keys())
|
||||
with Image.open(BytesIO(files[image_name])) as image:
|
||||
self.assertEqual(image.format, 'PNG')
|
||||
self.assertEqual(image.width, 1200)
|
||||
self.assertGreater(image.height, 500)
|
||||
image.verify()
|
||||
@@ -0,0 +1,189 @@
|
||||
import asyncio
|
||||
import json
|
||||
from contextlib import closing
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from farm_core import Asset, Catalogue, FarmService, Identity, SQLiteStore
|
||||
from farm_core.domain import FarmError, Plot
|
||||
from farm_core.repository import SQLiteRepository
|
||||
|
||||
|
||||
class UpgradeTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.path = Path(self.temp.name) / 'farm.sqlite3'
|
||||
self.store = SQLiteStore(self.path)
|
||||
await self.store.initialize()
|
||||
self.now = 1000
|
||||
self.service = FarmService(self.store, Catalogue.bundled(),
|
||||
lambda: self.now)
|
||||
self.who = Identity('test', 'bot', 'qq', 'upgrade')
|
||||
await self.service.register(self.who, '农场', 'register')
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.store.close()
|
||||
self.temp.cleanup()
|
||||
|
||||
async def fund(self, coins=100000000, exp=10000000):
|
||||
def operation(repo):
|
||||
account = repo.account()
|
||||
repo.asset(Asset.POINT, coins - account.point)
|
||||
repo.asset(Asset.EXPERIENCE, exp - account.experience)
|
||||
return {}
|
||||
await self.store.transact(self.who, f'fund:{coins}:{exp}',
|
||||
'fixture', {}, self.now, operation)
|
||||
|
||||
async def test_tiers_cost_ordinal_replay_and_restart(self):
|
||||
await self.fund()
|
||||
self.assertEqual((await self.service.upgrade_conditions(
|
||||
self.who, 1,
|
||||
))['coin_cost'], 200000)
|
||||
first = await self.service.upgrade(self.who, 1, 'red')
|
||||
self.assertEqual(first.data['land_name'], '红土地')
|
||||
self.assertEqual(await self.service.upgrade(self.who, 1, 'red'), first)
|
||||
for operation in ('black', 'gold'):
|
||||
await self.service.upgrade(self.who, 1, operation)
|
||||
plot = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual((plot.land_level, plot.yield_bonus,
|
||||
plot.experience_bonus), (3, 28, 28))
|
||||
condition = await self.service.upgrade_conditions(self.who, 2)
|
||||
self.assertEqual((condition['ordinal'], condition['coin_cost']),
|
||||
(2, 220000))
|
||||
before = await self.store.snapshot(self.who)
|
||||
with self.assertRaisesRegex(FarmError, 'land_max_level'):
|
||||
await self.service.upgrade(self.who, 1, 'fourth')
|
||||
with self.assertRaisesRegex(FarmError, 'operation_conflict'):
|
||||
await self.service.upgrade(self.who, 2, 'red')
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
await self.store.close()
|
||||
await self.store.initialize()
|
||||
self.assertEqual((await self.service.snapshot(self.who)).plots[0], plot)
|
||||
|
||||
async def test_current_season_matures_only_once_and_next_season_grows(self):
|
||||
await self.fund()
|
||||
plant = next(p for p in self.service.catalogue if p.crop > 1)
|
||||
await self.service.buy(self.who, f'#{plant.plant_id}', 1, 'buy')
|
||||
await self.service.sow(self.who, f'#{plant.plant_id}', 1, 'sow')
|
||||
await self.service.upgrade(self.who, 1, 'red')
|
||||
plot = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual((plot.mature_at, plot.harvest_count), (self.now, 0))
|
||||
await self.service.harvest(self.who, 'harvest')
|
||||
plot = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual(plot.harvest_count, 1)
|
||||
self.assertGreater(plot.mature_at, self.now)
|
||||
await self.service.upgrade(self.who, 1, 'black')
|
||||
plot = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual((plot.harvest_count, plot.mature_at), (1, self.now))
|
||||
if plant.crop > 2:
|
||||
await self.service.harvest(self.who, 'harvest2')
|
||||
next_plot = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual(next_plot.growth_remaining,
|
||||
(plant.regrowth_seconds * 80 + 99) // 100 * 100)
|
||||
|
||||
async def test_level_balance_invalid_slot_and_rollback(self):
|
||||
for slot in (0, -1, True, 1.5, 31):
|
||||
with self.assertRaises(FarmError):
|
||||
await self.service.upgrade(self.who, slot, 'invalid')
|
||||
with self.assertRaisesRegex(FarmError, 'unknown_plot'):
|
||||
await self.service.upgrade(self.who, 7, 'missing')
|
||||
with self.assertRaisesRegex(FarmError, 'level_required'):
|
||||
await self.service.upgrade(self.who, 1, 'level')
|
||||
await self.fund(coins=199999)
|
||||
before = await self.store.snapshot(self.who)
|
||||
with self.assertRaisesRegex(FarmError, 'insufficient_funds'):
|
||||
await self.service.upgrade(self.who, 1, 'poor')
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
await self.fund()
|
||||
before = await self.store.snapshot(self.who)
|
||||
with patch.object(SQLiteRepository, 'save_plot',
|
||||
side_effect=RuntimeError('fault')):
|
||||
with self.assertRaisesRegex(RuntimeError, 'fault'):
|
||||
await self.service.upgrade(self.who, 1, 'rollback')
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
|
||||
async def test_concurrent_same_message_single_charge(self):
|
||||
await self.fund()
|
||||
before = await self.store.snapshot(self.who)
|
||||
results = await asyncio.gather(*(
|
||||
self.service.upgrade(self.who, 1, 'same') for _ in range(12)
|
||||
))
|
||||
self.assertTrue(all(r == results[0] for r in results))
|
||||
after = await self.store.snapshot(self.who)
|
||||
self.assertEqual(after.plots[0].land_level, 1)
|
||||
self.assertEqual(before.account.point - after.account.point, 200000)
|
||||
|
||||
async def test_upgrade_preserves_pests_theft_and_withered_state(self):
|
||||
await self.fund()
|
||||
plant = next(p for p in self.service.catalogue if p.crop > 1)
|
||||
original = Plot(1, plant, self.now - 1, pest_count=1, pest_spawned=2,
|
||||
maturity_pests=2, stolen_count=1)
|
||||
def operation(repo):
|
||||
repo.save_plot(original)
|
||||
repo.save_plot(Plot(2, plant, None, plant.crop))
|
||||
return {}
|
||||
await self.store.transact(self.who, 'setup', 'fixture', {}, self.now,
|
||||
operation)
|
||||
await self.service.upgrade(self.who, 1, 'ripe')
|
||||
await self.service.upgrade(self.who, 2, 'withered')
|
||||
plots = (await self.service.snapshot(self.who)).plots
|
||||
self.assertEqual((plots[0].maturity_pests, plots[0].stolen_count),
|
||||
(2, 1))
|
||||
self.assertTrue(plots[1].withered)
|
||||
|
||||
async def test_v4_migration_and_original_legacy_land_levels(self):
|
||||
await self.store.close()
|
||||
with closing(sqlite3.connect(self.path)) as db, db:
|
||||
db.execute('ALTER TABLE plots DROP COLUMN land_level')
|
||||
db.execute('PRAGMA user_version=4')
|
||||
db.execute('INSERT INTO operations VALUES (?, ?, ?, ?, ?, ?)', (
|
||||
self.who.key, 'legacy-fixture', 'legacy_import', 'fixture',
|
||||
json.dumps({'legacy_rows': {'userSoil': [
|
||||
{'soilIndex': 2, 'soilLevel': 3},
|
||||
{'soilIndex': 3, 'soilLevel': 1},
|
||||
]}}), self.now,
|
||||
))
|
||||
await self.store.initialize()
|
||||
plots = (await self.store.snapshot(self.who)).plots
|
||||
self.assertEqual([p.land_level for p in plots], [0, 3, 1, 0, 0, 0])
|
||||
# 只恢复明确等级,不重写旧版已经保存的资产和加成。
|
||||
self.assertEqual(plots[1].yield_bonus, 0)
|
||||
|
||||
async def test_growing_pests_freeze_at_forced_maturity(self):
|
||||
await self.fund()
|
||||
plant = self.service.catalogue.get('白萝卜')
|
||||
from farm_core.pests import start_growth
|
||||
plot = start_growth(Plot(1, plant), self.now)
|
||||
await self.store.transact(
|
||||
self.who, 'plant', 'fixture', {}, self.now,
|
||||
lambda repo: repo.save_plot(plot) or {},
|
||||
)
|
||||
self.now += max(7200, (plant.growth_seconds + 1) // 2) + 1
|
||||
await self.service.upgrade(self.who, 1, 'upgrade-pests')
|
||||
mature = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual((mature.mature_at, mature.growth_remaining,
|
||||
mature.maturity_pests), (self.now, 0, 1))
|
||||
self.assertIsNone(mature.next_pest_at)
|
||||
self.now += 999999
|
||||
later = (await self.service.snapshot(self.who)).plots[0]
|
||||
self.assertEqual(later.pest_count, 1)
|
||||
|
||||
async def test_ordinal_above_configured_table_is_rejected(self):
|
||||
await self.fund()
|
||||
def operation(repo):
|
||||
for slot in range(1, 25):
|
||||
if slot > 6:
|
||||
repo.add_plot(slot)
|
||||
repo.save_plot(Plot(slot, land_level=1))
|
||||
repo.add_plot(25)
|
||||
return {}
|
||||
await self.store.transact(self.who, '24-red', 'fixture', {}, self.now,
|
||||
operation)
|
||||
before = await self.store.snapshot(self.who)
|
||||
with self.assertRaisesRegex(FarmError, 'land_rule_unavailable'):
|
||||
await self.service.upgrade(self.who, 25, '25th-red')
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
@@ -29,9 +29,19 @@ class NoneBotPackageTests(unittest.TestCase):
|
||||
ROOT / "integrations/shared/runtime.py"
|
||||
).read_bytes())
|
||||
for suffix in ("farm_core/data/plants.json",
|
||||
"farm_render/data/asset-manifest.json"):
|
||||
"farm_render/data/asset-manifest.json",
|
||||
"farm_render/data/font-manifest.json",
|
||||
"farm_render/data/OFL-Xiaolai.txt"):
|
||||
self.assertTrue(any(name.endswith(suffix) for name in self.files))
|
||||
self.assertEqual(verify_files(self.files)["target"], "nonebot")
|
||||
self.assertFalse(any(name.endswith('.ttf') for name in self.files))
|
||||
for name in ("font-manifest.json", "OFL-Xiaolai.txt"):
|
||||
self.assertEqual(
|
||||
self.files[
|
||||
"nonebot_plugin_farm/_vendor/farm_render/data/" + name
|
||||
],
|
||||
(ROOT / "src/farm_render/data" / name).read_bytes(),
|
||||
)
|
||||
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))
|
||||
|
||||
@@ -60,6 +60,29 @@ class RenderTests(unittest.TestCase):
|
||||
self.assertEqual(canvas.image.getpixel((x-15, y))[:3],
|
||||
ImageColor.getrgb(color))
|
||||
|
||||
def test_land_upgrade_sprites_and_both_caches_change(self):
|
||||
baseline = snapshot()
|
||||
options = RenderOptions(format='PNG')
|
||||
original = self.renderer.render(self.who, baseline, 0, options)
|
||||
for level, name in ((1, '红土地'), (2, '黑土地'), (3, '金土地')):
|
||||
current = replace(baseline, plots=(
|
||||
replace(baseline.plots[0], land_level=level),
|
||||
*baseline.plots[1:],
|
||||
))
|
||||
with patch.object(self.renderer.assets, 'image',
|
||||
wraps=self.renderer.assets.image) as read:
|
||||
result = self.renderer.render(self.who, current, 0, options)
|
||||
self.assertFalse(result.cache_hit)
|
||||
self.assertNotEqual(result.content, original.content)
|
||||
self.assertIn(f'soil/{name}.png',
|
||||
[call.args[0] for call in read.call_args_list])
|
||||
self.assertTrue(self.renderer.render(
|
||||
self.who, current, 0, options,
|
||||
).cache_hit)
|
||||
self.assertEqual(self.renderer.render(
|
||||
self.who, baseline, 0, options,
|
||||
).content, original.content)
|
||||
|
||||
def test_plot_id_image_has_separate_cache_from_normal_farm(self):
|
||||
plain = self.renderer.render(self.who, snapshot(), 0)
|
||||
options = RenderOptions(show_plot_ids=True)
|
||||
|
||||
@@ -142,6 +142,7 @@ class StealingTests(unittest.IsolatedAsyncioTestCase):
|
||||
with closing(sqlite3.connect(self.store.path)) as db, db:
|
||||
db.execute('DROP TABLE thefts')
|
||||
db.execute('ALTER TABLE plots DROP COLUMN stolen_count')
|
||||
db.execute('ALTER TABLE plots DROP COLUMN land_level')
|
||||
db.execute('PRAGMA user_version=3')
|
||||
await self.store.initialize()
|
||||
self.assertEqual(await self.service.snapshot(self.owner), before)
|
||||
|
||||
+137
-14
@@ -48,6 +48,15 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
/ bundle.expected_manifest()['sha256'] / 'assets.zip')
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(ROOT / 'src/farm_render/data/assets.zip', target)
|
||||
fonts = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.font_bundle',
|
||||
)
|
||||
font_dir = (runtime.data_dir / 'fonts'
|
||||
/ fonts.expected_manifest()['sha256'])
|
||||
font_dir.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(choose_font(), font_dir / 'Xiaolai-Regular.ttf')
|
||||
shutil.copyfile(ROOT / 'src/farm_render/data/OFL-Xiaolai.txt',
|
||||
font_dir / 'OFL-Xiaolai.txt')
|
||||
|
||||
async def call(self, action, arguments="", message="event", **changes):
|
||||
return await self.runtime.execute(
|
||||
@@ -55,12 +64,60 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
**{**self.context, "message_id":message, **changes},
|
||||
)
|
||||
|
||||
async def test_removed_commands_and_text_options(self):
|
||||
for platform in ('zhenxun', 'nonebot', 'astrbot'):
|
||||
self.assertNotIn('我的农场币', self.module.command_map(platform))
|
||||
self.assertNotIn('我的点券', self.module.command_map(platform))
|
||||
await self.call('register')
|
||||
for action in ('seeds', 'crops', 'help'):
|
||||
for args in ('文字', '1 文字'):
|
||||
with self.subTest(action=action, args=args):
|
||||
self.assertIn('参数有误', await self.call(action, args))
|
||||
for alias in ('my-point', 'my-vipPoint'):
|
||||
self.assertIn('参数有误', await self.call('farm', alias))
|
||||
for action in ('seeds', 'crops'):
|
||||
self.assertIsInstance(await self.call(action, '1'), bytes)
|
||||
|
||||
async def test_land_upgrade_command_conditions_charge_and_replay(self):
|
||||
self.assertEqual(self.module.COMMANDS['土地升级'], 'upgrade')
|
||||
await self.call('register')
|
||||
from unittest.mock import AsyncMock
|
||||
asset = importlib.import_module(
|
||||
self.package + '._vendor.farm_core.domain',
|
||||
).Asset
|
||||
def fund(repo):
|
||||
repo.asset(asset.POINT, 999500)
|
||||
repo.asset(asset.EXPERIENCE, 1000000)
|
||||
return {}
|
||||
await self.runtime.store.transact(self.runtime_identity(), 'fund',
|
||||
'fixture', {}, 1000, fund)
|
||||
with patch.object(self.runtime.service, 'upgrade',
|
||||
new=AsyncMock()) as upgrade:
|
||||
condition = await self.call('upgrade', '1 条件', message=None)
|
||||
self.assertIn('红土地', condition)
|
||||
self.assertIn('200000', condition)
|
||||
upgrade.assert_not_awaited()
|
||||
self.assertIn('稳定消息标识', await self.call(
|
||||
'upgrade', '1', message=None,
|
||||
))
|
||||
upgrade.assert_not_awaited()
|
||||
result = await self.call('upgrade', '1', message='upgrade-red')
|
||||
self.assertIn('地块1已升为红土地', result)
|
||||
self.assertEqual(result, await self.call(
|
||||
'upgrade', '1', message='upgrade-red',
|
||||
))
|
||||
snapshot = await self.runtime.service.snapshot(self.runtime_identity())
|
||||
self.assertEqual(snapshot.account.point, 800000)
|
||||
self.assertEqual(snapshot.plots[0].land_level, 1)
|
||||
self.assertIn('地块不存在', await self.call('upgrade', '7'))
|
||||
self.assertIn('请发送', await self.call('upgrade', '1 2'))
|
||||
|
||||
async def test_plot_ids_command_registration_hint_and_read_only(self):
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
from unittest.mock import AsyncMock
|
||||
self.assertEqual(self.module.COMMANDS['地块编号'], 'plot_ids')
|
||||
self.assertIn('地块编号', await self.call('help', '文字'))
|
||||
self.assertIn('地块编号', self.module.help_text(self.runtime.command_specs))
|
||||
first = await self.call('register')
|
||||
self.assertIn('6块土地', first)
|
||||
self.assertIn('地块编号', first)
|
||||
@@ -91,7 +148,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
migrate.assert_not_awaited()
|
||||
with Image.open(BytesIO(image)) as decoded:
|
||||
self.assertEqual(decoded.width, 1200)
|
||||
text = await self.call('help', '文字', message=None)
|
||||
text = self.module.help_text(self.runtime.command_specs)
|
||||
for command in self.module.COMMANDS:
|
||||
self.assertIn(command, text)
|
||||
self.assertNotIn('购买农场币', text)
|
||||
@@ -117,11 +174,11 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
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)
|
||||
self.assertIsInstance(self.module.help_text(self.runtime.command_specs), 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('开垦 条件', self.module.help_text(self.runtime.command_specs))
|
||||
await self.call('register')
|
||||
who = self.runtime_identity()
|
||||
asset = importlib.import_module(
|
||||
@@ -180,7 +237,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn('仅我的农场支持高清', await self.call(
|
||||
'seeds', '1 高清', message=None,
|
||||
))
|
||||
self.assertIn('胡萝卜(#1) 2粒', await self.call(
|
||||
self.assertIn('参数有误', await self.call(
|
||||
'seeds', '文字', message=None,
|
||||
))
|
||||
self.assertEqual(before, await self.runtime.service.snapshot(
|
||||
@@ -264,7 +321,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
with Image.open(BytesIO(content)) as image:
|
||||
self.assertEqual(image.width, 1536)
|
||||
|
||||
async def test_only_farm_accepts_high_and_warehouse_keeps_text(self):
|
||||
async def test_only_farm_accepts_high_and_text_option_is_removed(self):
|
||||
await self.call('register')
|
||||
for action in ('plot_ids', 'details', 'shop', 'seeds', 'crops'):
|
||||
for arguments in ('高清', '1 高清'):
|
||||
@@ -273,9 +330,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
action, arguments, message=None,
|
||||
))
|
||||
for action in ('seeds', 'crops'):
|
||||
self.assertIsInstance(await self.call(action, '1 文字'), str)
|
||||
help_text = await self.call('help', '文字')
|
||||
self.assertIn('仅我的农场支持高清', help_text)
|
||||
self.assertIn('参数有误', await self.call(action, '1 文字'))
|
||||
help_text = self.module.help_text(self.runtime.command_specs)
|
||||
self.assertNotIn('地块编号 [高清]', help_text)
|
||||
self.assertNotIn('[高清/文字]', help_text)
|
||||
|
||||
@@ -376,9 +432,8 @@ 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 = self.module.help_text(self.runtime.command_specs)
|
||||
self.assertIn("每只虫", help_text)
|
||||
self.assertIn("间隔至少2小时", help_text)
|
||||
self.runtime.service.clock = lambda: 1000
|
||||
await self.call("register")
|
||||
await self.call("buy", "白萝卜 1", "buy")
|
||||
@@ -466,11 +521,11 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
await other.start()
|
||||
try:
|
||||
response = await other.execute("farm", "", **self.context)
|
||||
self.assertIn("FARM_PREVIEW_FONT", response)
|
||||
self.assertIn("小赖字体", response)
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_default_font_uses_host_resources_and_skips_broken_font(self):
|
||||
async def test_default_font_uses_service_cache_xiaolai(self):
|
||||
fonts = self.root / "host-fonts"
|
||||
fonts.mkdir()
|
||||
(fonts / "HYWenHei-85W.ttf").write_bytes(b"broken")
|
||||
@@ -482,7 +537,9 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIsNotNone(other.rendering)
|
||||
self.assertEqual(
|
||||
other.rendering.renderer.font_path,
|
||||
(fonts / "msyh.ttf").resolve(),
|
||||
next((other.data_dir / "fonts").glob(
|
||||
"*/Xiaolai-Regular.ttf",
|
||||
)).resolve(),
|
||||
)
|
||||
await other.execute("register", "", **self.context)
|
||||
result = await other.execute("farm", "", **self.context)
|
||||
@@ -490,6 +547,19 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_font_download_failure_keeps_text_commands_available(self):
|
||||
other = self.module.Runtime(self.root / "offline-font")
|
||||
self.seed_assets(other)
|
||||
with patch.object(self.module, 'ensure_font', side_effect=OSError()):
|
||||
await other.start()
|
||||
try:
|
||||
self.assertIsNone(other.rendering)
|
||||
text = await other.execute("register", "", **self.context)
|
||||
self.assertIsInstance(text, str)
|
||||
self.assertIn("注册", text)
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_explicit_font_overrides_host_defaults(self):
|
||||
explicit = Path(choose_font()).resolve()
|
||||
other = self.module.Runtime(
|
||||
@@ -503,6 +573,59 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_download_failure_stops_until_explicit_reload(self):
|
||||
for resource in ('assets', 'font'):
|
||||
other = self.module.Runtime(self.root / ('stop-' + resource))
|
||||
self.seed_assets(other)
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=TimeoutError()) as fetch:
|
||||
with patch.object(self.module, 'monotonic',
|
||||
return_value=1000, create=True):
|
||||
await other.start()
|
||||
self.assertTrue(other.started)
|
||||
self.assertIsNone(other.rendering)
|
||||
for now in (1001, 1061, 100000):
|
||||
with patch.object(self.module, 'monotonic',
|
||||
return_value=now, create=True):
|
||||
await asyncio.gather(*(
|
||||
other.start() for _ in range(10)
|
||||
))
|
||||
response = await other.execute(
|
||||
'help', '', **self.context,
|
||||
)
|
||||
self.assertIsInstance(response, str)
|
||||
await asyncio.sleep(0)
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
await other.close()
|
||||
await other.start()
|
||||
self.assertIsNotNone(other.rendering)
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_resource_logs_identify_stage_without_private_errors(self):
|
||||
from urllib.error import HTTPError
|
||||
|
||||
for resource, failure, reason in (
|
||||
('assets', HTTPError('https://private/token', 404,
|
||||
'private-token', {}, None), 'http_404'),
|
||||
('font', ValueError('Font archive size or SHA256 mismatch'),
|
||||
'integrity_mismatch'),
|
||||
):
|
||||
other = self.module.Runtime(self.root / ('failed-' + resource))
|
||||
self.seed_assets(other)
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=failure), \
|
||||
self.assertLogs(self.module.logger, 'WARNING') as logs:
|
||||
await other.start()
|
||||
output = '\n'.join(logs.output)
|
||||
self.assertIn('resource=' + resource, output)
|
||||
self.assertIn('reason=' + reason, output)
|
||||
self.assertNotIn('private', output)
|
||||
finally:
|
||||
await other.close()
|
||||
|
||||
async def test_insufficient_funds_has_chinese_message_and_no_debit(self):
|
||||
await self.call("register")
|
||||
before = await self.call("inventory")
|
||||
|
||||
Reference in New Issue
Block a user