mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-11-02 18:50:08 +08:00
fixed # 新增本地部署脚本,一键生成对应平台的压缩包内容
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
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 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)
|
||||
self.assertEqual(files['runtime.py'], (
|
||||
ROOT / 'integrations/zhenxun/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': str(choose_font()), 'image_quality': 55,
|
||||
})
|
||||
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 'player'
|
||||
|
||||
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'))
|
||||
result = await call('我的农场 高清')
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
with Image.open(BytesIO(result[0])) as image:
|
||||
self.assertEqual(image.width, 1536)
|
||||
self.assertIn('仅我的农场', await call('我的种子 高清'))
|
||||
self.assertIn('暂无种子', await call('我的种子 文字'))
|
||||
self.assertTrue((self.root / 'plugin-data/farm.sqlite3').is_file())
|
||||
await plugin.terminate()
|
||||
self.assertFalse(plugin.runtime.started)
|
||||
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]
|
||||
@@ -0,0 +1,48 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'scripts'))
|
||||
|
||||
|
||||
class BuildPluginTests(unittest.TestCase):
|
||||
def test_all_platforms_from_another_directory_and_repeated_output(self):
|
||||
from build_plugin import build_plugin
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
for platform in ('nonebot', 'zhenxun', 'astrbot'):
|
||||
first = build_plugin(platform, root=root / platform)
|
||||
before = first.read_bytes()
|
||||
second = build_plugin(platform, root=root / platform)
|
||||
self.assertNotEqual(first.parent, second.parent)
|
||||
self.assertEqual(first.read_bytes(), before)
|
||||
self.assertTrue(second.is_file())
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'Windows entry points')
|
||||
def test_cmd_entries_and_nonzero_failure(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
env = {**os.environ, 'FARM_PYTHON': sys.executable,
|
||||
'FARM_BUILD_NO_PAUSE': '1'}
|
||||
for platform in ('nonebot', 'zhenxun', 'astrbot'):
|
||||
script = ROOT / f'build_{platform}.cmd'
|
||||
result = subprocess.run(
|
||||
['cmd.exe', '/d', '/c', str(script), '-OutputDirectory',
|
||||
str(root / platform)], cwd=root, env=env,
|
||||
capture_output=True, timeout=120,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stdout.decode(
|
||||
errors='replace',
|
||||
) + result.stderr.decode(errors='replace'))
|
||||
self.assertEqual(len(list((root / platform).glob('*.zip'))), 1)
|
||||
env['FARM_PYTHON'] = str(root / 'not-a-python.exe')
|
||||
failed = subprocess.run(
|
||||
['cmd.exe', '/d', '/c', str(ROOT / 'build_astrbot.cmd')],
|
||||
cwd=root, env=env, capture_output=True, timeout=30,
|
||||
)
|
||||
self.assertNotEqual(failed.returncode, 0)
|
||||
@@ -1,8 +1,10 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
import sys
|
||||
from types import ModuleType, SimpleNamespace
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -15,6 +17,65 @@ spec.loader.exec_module(migration)
|
||||
|
||||
|
||||
class ConfigMigrationTests(unittest.TestCase):
|
||||
def test_zhenxun_registers_global_quality_in_plugin_metadata(self):
|
||||
plugin = ModuleType('nonebot.plugin')
|
||||
plugin.PluginMetadata = lambda **values: SimpleNamespace(**values)
|
||||
utils = ModuleType('zhenxun.configs.utils')
|
||||
utils.Command = lambda **values: SimpleNamespace(**values)
|
||||
utils.RegisterConfig = lambda **values: SimpleNamespace(**values)
|
||||
utils.PluginExtraData = lambda **values: SimpleNamespace(
|
||||
to_dict=lambda: values,
|
||||
)
|
||||
package = ModuleType('quality_metadata_fixture')
|
||||
package.__path__ = []
|
||||
version = ModuleType(package.__name__ + '._version')
|
||||
version.__version__ = '0.0.0'
|
||||
runtime = ModuleType(package.__name__ + '.runtime')
|
||||
runtime.COMMANDS, runtime.HELP = {}, ''
|
||||
commands = ModuleType(package.__name__ + '.commands')
|
||||
modules = {m.__name__: m for m in (
|
||||
plugin, utils, package, version, runtime, commands,
|
||||
)}
|
||||
source = ROOT / 'integrations/zhenxun/__init__.py'
|
||||
namespace = {'__package__': package.__name__}
|
||||
with patch.dict(sys.modules, modules):
|
||||
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)
|
||||
|
||||
@unittest.skipUnless(importlib.util.find_spec('pydantic'),
|
||||
'NoneBot host config requires Pydantic')
|
||||
def test_nonebot_quality_config_is_strict_and_bounded(self):
|
||||
libraries = [importlib.import_module('pydantic')]
|
||||
try:
|
||||
libraries.append(importlib.import_module('pydantic.v1'))
|
||||
except ImportError:
|
||||
pass
|
||||
for library in libraries:
|
||||
with self.subTest(library=library.__name__):
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
'farm_nonebot_quality_config',
|
||||
ROOT / 'integrations/nonebot/config.py',
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
with patch.dict(sys.modules, {'pydantic': library}):
|
||||
spec.loader.exec_module(module)
|
||||
config = module.Config
|
||||
self.assertEqual(config().farm_image_quality, 75)
|
||||
for value in (1, 95):
|
||||
actual = config(farm_image_quality=value).farm_image_quality
|
||||
self.assertEqual(actual, value)
|
||||
for value in (0, 96, True, '75', 75.5):
|
||||
with self.subTest(value=value), self.assertRaises(
|
||||
library.ValidationError,
|
||||
):
|
||||
config(farm_image_quality=value)
|
||||
|
||||
def test_cleanup_backups_and_repeat_are_scoped(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
root = Path(temporary)
|
||||
|
||||
@@ -117,6 +117,11 @@ class NoneBotPackageTests(unittest.TestCase):
|
||||
commit = next(data for method, path, data in calls
|
||||
if path == "git/commits")
|
||||
self.assertEqual(commit["parents"], ["existing"])
|
||||
self.assertEqual(commit["message"], (
|
||||
manifest["source"]["message"] + "\n\n"
|
||||
+ "Farm-Source-Commit: " + manifest["source"]["commit"] + "\n"
|
||||
+ "Farm-Version: " + manifest["version"]
|
||||
))
|
||||
self.assertIn(("PATCH", "git/refs/heads/main", {
|
||||
"sha": "generated", "force": False,
|
||||
}), calls)
|
||||
|
||||
+60
-32
@@ -10,7 +10,7 @@ from unittest.mock import patch
|
||||
from farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
||||
from farm_core.domain import FarmError, Plant
|
||||
from farm_core.repository import SQLiteRepository
|
||||
from farm_core.pests import advance_plot
|
||||
from farm_core.pests import advance_plot, start_growth
|
||||
|
||||
|
||||
class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
@@ -37,41 +37,39 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
return (await self.service.snapshot(self.who)).plots[0]
|
||||
|
||||
async def test_speed_integrates_each_pest_interval_and_offline_maturity(self):
|
||||
self.now = 4600
|
||||
self.now = 8199
|
||||
self.assertEqual((await self.plot()).pest_count, 0)
|
||||
self.now = 8200
|
||||
first = await self.plot()
|
||||
self.assertEqual(first.pest_count, 1)
|
||||
self.assertEqual(first.growth_remaining, 1080000)
|
||||
self.now = 8200
|
||||
self.assertEqual(first.growth_remaining, 720000)
|
||||
self.now = 15400
|
||||
second = await self.plot()
|
||||
self.assertEqual(second.pest_count, 2)
|
||||
self.assertEqual(second.growth_remaining, 738000)
|
||||
self.now = 11800
|
||||
third = await self.plot()
|
||||
self.assertEqual(third.pest_count, 3)
|
||||
self.assertEqual(third.growth_remaining, 414000)
|
||||
self.now = 16670
|
||||
self.assertEqual(second.growth_remaining, 36000)
|
||||
self.now = 15799
|
||||
self.assertGreater((await self.plot()).growth_remaining, 0)
|
||||
self.now = 16671
|
||||
self.now = 15800
|
||||
mature = await self.plot()
|
||||
self.assertEqual(mature.mature_at, 16671)
|
||||
self.assertEqual(mature.maturity_pests, 3)
|
||||
self.assertEqual(mature.mature_at, 15800)
|
||||
self.assertEqual(mature.maturity_pests, 2)
|
||||
receipt = await self.service.harvest(self.who, "harvest")
|
||||
self.assertEqual(receipt.data["quantity"], 85)
|
||||
self.assertEqual(receipt.data["quantity"], 90)
|
||||
self.assertEqual(receipt.data["experience"], 20)
|
||||
self.assertEqual(receipt.data["harvested"][0]["pests"], 3)
|
||||
self.assertEqual(receipt.data["harvested"][0]["pests"], 2)
|
||||
again = await self.plot()
|
||||
self.assertEqual(again.pest_count, 0)
|
||||
self.assertEqual(again.maturity_pests, 0)
|
||||
|
||||
async def test_clear_before_maturity_restores_rate_but_not_lost_time(self):
|
||||
self.now = 6400
|
||||
self.now = 10000
|
||||
receipt = await self.service.clear_pests(self.who, "clear")
|
||||
self.assertEqual(receipt.data["quantity"], 1)
|
||||
cleared = await self.plot()
|
||||
self.assertEqual(cleared.pest_count, 0)
|
||||
self.assertEqual(cleared.growth_remaining, 909000)
|
||||
self.assertEqual(cleared.growth_remaining, 549000)
|
||||
self.assertEqual(cleared.pest_spawned, 1)
|
||||
self.now = 8200
|
||||
self.now = 15400
|
||||
self.assertEqual((await self.plot()).pest_count, 1)
|
||||
repeated = await self.service.clear_pests(self.who, "clear")
|
||||
self.assertEqual(repeated, receipt)
|
||||
@@ -82,12 +80,12 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
await self.service.clear_pests(self.who, "late-clear")
|
||||
mature = await self.plot()
|
||||
self.assertEqual(mature.pest_count, 0)
|
||||
self.assertEqual(mature.maturity_pests, 3)
|
||||
self.assertEqual(mature.maturity_pests, 2)
|
||||
receipt = await self.service.harvest(self.who, "harvest")
|
||||
self.assertEqual(receipt.data["quantity"], 85)
|
||||
self.assertEqual(receipt.data["quantity"], 90)
|
||||
|
||||
async def test_reopen_preserves_cleared_progress_and_concurrent_clear(self):
|
||||
self.now = 6400
|
||||
self.now = 10000
|
||||
results = await asyncio.gather(
|
||||
self.service.clear_pests(self.who, "clear-1"),
|
||||
self.service.clear_pests(self.who, "clear-2"),
|
||||
@@ -105,7 +103,7 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
).fetchone(), (0, 1))
|
||||
|
||||
async def test_failed_clear_rolls_back_all_plot_updates(self):
|
||||
self.now = 6400
|
||||
self.now = 10000
|
||||
before = await self.store.snapshot(self.who)
|
||||
original = SQLiteRepository.save_plot
|
||||
|
||||
@@ -121,7 +119,7 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_empty_clear_has_no_reward_and_clear_awards_per_pest(self):
|
||||
with self.assertRaisesRegex(FarmError, "nothing_to_clear"):
|
||||
await self.service.clear_pests(self.who, "empty-clear")
|
||||
self.now = 4600
|
||||
self.now = 8200
|
||||
before = await self.service.snapshot(self.who)
|
||||
await self.service.clear_pests(self.who, "clear")
|
||||
after = await self.service.snapshot(self.who)
|
||||
@@ -132,23 +130,23 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_multi_plot_pest_rewards_replay_and_transaction_rollback(self):
|
||||
await self.service.buy(self.who, '虫害测试菜', 1, 'buy-second')
|
||||
await self.service.sow(self.who, '虫害测试菜', 1, 'sow-second')
|
||||
self.now = 11800
|
||||
self.now = 15400
|
||||
before = await self.store.snapshot(self.who)
|
||||
ledger = await self.store.ledger(self.who)
|
||||
with patch.object(SQLiteRepository, 'asset',
|
||||
side_effect=RuntimeError('reward failure')):
|
||||
with self.assertRaises(RuntimeError):
|
||||
await self.service.clear_pests(self.who, 'clear-six')
|
||||
await self.service.clear_pests(self.who, 'clear-four')
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
self.assertEqual(await self.store.ledger(self.who), ledger)
|
||||
receipts = await asyncio.gather(*(
|
||||
self.service.clear_pests(self.who, 'clear-six') for _ in range(3)
|
||||
self.service.clear_pests(self.who, 'clear-four') for _ in range(3)
|
||||
))
|
||||
self.assertTrue(all(r == receipts[0] for r in receipts))
|
||||
self.assertEqual(receipts[0].data['quantity'], 6)
|
||||
self.assertEqual(receipts[0].data['experience'], 6)
|
||||
self.assertEqual(receipts[0].data['quantity'], 4)
|
||||
self.assertEqual(receipts[0].data['experience'], 4)
|
||||
after = await self.service.snapshot(self.who)
|
||||
self.assertEqual(after.account.experience, before.account.experience+6)
|
||||
self.assertEqual(after.account.experience, before.account.experience+4)
|
||||
with self.assertRaisesRegex(FarmError, 'nothing_to_clear'):
|
||||
await self.service.clear_pests(self.who, 'empty-after')
|
||||
self.assertEqual((await self.service.snapshot(self.who)).account,
|
||||
@@ -158,10 +156,10 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
initial = (await self.store.snapshot(self.who)).plots[0]
|
||||
direct = advance_plot(initial, 20000)
|
||||
split = initial
|
||||
for instant in (1001, 4599, 4600, 7000, 11800, 16671, 20000):
|
||||
for instant in (1001, 8199, 8200, 10000, 15400, 15800, 20000):
|
||||
split = advance_plot(split, instant)
|
||||
self.assertEqual(split, direct)
|
||||
self.assertEqual(direct.maturity_pests, 3)
|
||||
self.assertEqual(direct.maturity_pests, 2)
|
||||
|
||||
async def test_growth_stage_uses_work_not_predicted_finish_time(self):
|
||||
import sys
|
||||
@@ -180,4 +178,34 @@ class PestTests(unittest.IsolatedAsyncioTestCase):
|
||||
plots=(projected,))
|
||||
view = renderer.scene(self.who, snapshot, self.now).plots[0]
|
||||
self.assertEqual(view.stage, 2)
|
||||
self.assertEqual(view.pest_count, 2)
|
||||
self.assertEqual(view.pest_count, 1)
|
||||
|
||||
async def test_short_crops_mature_without_pests(self):
|
||||
stored = (await self.store.snapshot(self.who)).plots[0]
|
||||
for duration in (3600, 7200):
|
||||
with self.subTest(duration=duration):
|
||||
plant = replace(self.plant, growth_seconds=duration)
|
||||
plot = start_growth(replace(stored, plant=plant), 1000)
|
||||
mature = advance_plot(plot, 1000 + duration)
|
||||
self.assertEqual(mature.growth_remaining, 0)
|
||||
self.assertEqual(mature.mature_at, 1000 + duration)
|
||||
self.assertEqual(mature.pest_count, 0)
|
||||
self.assertEqual(mature.maturity_pests, 0)
|
||||
|
||||
async def test_regrowth_uses_its_own_longer_pest_interval(self):
|
||||
stored = (await self.store.snapshot(self.who)).plots[0]
|
||||
plant = replace(self.plant, regrowth_seconds=36001)
|
||||
plot = start_growth(replace(
|
||||
stored, plant=plant, harvest_count=1,
|
||||
), 1000)
|
||||
self.assertEqual(advance_plot(plot, 19000).pest_count, 0)
|
||||
self.assertEqual(advance_plot(plot, 19001).pest_count, 1)
|
||||
|
||||
async def test_pending_pest_is_preserved_then_spacing_increases(self):
|
||||
stored = (await self.store.snapshot(self.who)).plots[0]
|
||||
old = replace(stored, next_pest_at=4600)
|
||||
first = advance_plot(old, 4600)
|
||||
self.assertEqual(first.pest_count, 1)
|
||||
self.assertEqual(first.next_pest_at, 11800)
|
||||
self.assertEqual(advance_plot(first, 11799).pest_count, 1)
|
||||
self.assertEqual(advance_plot(first, 11800).pest_count, 2)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "scripts"))
|
||||
from build_zhenxun import source_identity
|
||||
from release_metadata import commit_message
|
||||
|
||||
|
||||
class ReleaseMetadataTests(unittest.TestCase):
|
||||
def test_multiline_unicode_message_is_bound_to_build_commit(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
def git(*args, message=None):
|
||||
return subprocess.check_output(
|
||||
["git", "-c", "user.name=Farm Test",
|
||||
"-c", "user.email=farm-test@example.invalid", *args],
|
||||
cwd=temporary, input=message, text=True, encoding="utf-8",
|
||||
stderr=subprocess.DEVNULL,
|
||||
).strip()
|
||||
|
||||
git("init", "--quiet")
|
||||
original = '修复农场显示\n\n保留正文、引号和 $() 字符。\n第二行。'
|
||||
git("commit", "--allow-empty", "--file=-", message=original)
|
||||
source = source_identity(temporary)
|
||||
self.assertEqual(source["message"], original)
|
||||
self.assertEqual(source["commit"], git("rev-parse", "HEAD"))
|
||||
git("commit", "--allow-empty", "--file=-", message="后续提交")
|
||||
manifest = {"source": source, "version": "2.0.1"}
|
||||
expected = (
|
||||
original + "\n\nFarm-Source-Commit: " + source["commit"]
|
||||
+ "\nFarm-Version: 2.0.1"
|
||||
)
|
||||
self.assertEqual(commit_message(manifest), expected)
|
||||
self.assertNotIn("后续提交", commit_message(manifest))
|
||||
|
||||
def test_old_or_invalid_metadata_requires_rebuilding(self):
|
||||
for source in ({"commit": "old"}, {"message": " "},
|
||||
{"message": None}, {"message": 123}):
|
||||
with self.subTest(source=source):
|
||||
with self.assertRaisesRegex(ValueError, "rebuild"):
|
||||
commit_message({"source": source, "version": "2.0.1"})
|
||||
@@ -256,6 +256,12 @@ class ZhenxunPackageTests(unittest.TestCase):
|
||||
commit = next(data for method, path, data in calls
|
||||
if method == "POST" and path == "git/commits")
|
||||
self.assertEqual(commit["parents"], ["existing"])
|
||||
manifest = json.loads(read_package(package)["FARM-MANIFEST.json"])
|
||||
self.assertEqual(commit["message"], (
|
||||
manifest["source"]["message"] + "\n\n"
|
||||
+ "Farm-Source-Commit: " + manifest["source"]["commit"] + "\n"
|
||||
+ "Farm-Version: " + manifest["version"]
|
||||
))
|
||||
self.assertIn(("PATCH", "git/refs/heads/main", {
|
||||
"sha": "created", "force": False,
|
||||
}), calls)
|
||||
|
||||
@@ -99,9 +99,9 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
with Image.open(BytesIO(content)) as image:
|
||||
self.assertEqual(image.format, 'JPEG')
|
||||
self.assertEqual(image.size, (768, 1152))
|
||||
high = await self.call('seeds', '1 高清', message=None)
|
||||
with Image.open(BytesIO(high)) as image:
|
||||
self.assertEqual(image.size, (1024, 1536))
|
||||
self.assertIn('仅我的农场支持高清', await self.call(
|
||||
'seeds', '1 高清', message=None,
|
||||
))
|
||||
self.assertIn('胡萝卜(#1) 2粒', await self.call(
|
||||
'seeds', '文字', message=None,
|
||||
))
|
||||
@@ -153,6 +153,71 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
'steal', '2', target_user_id='2', message='invalid',
|
||||
))
|
||||
|
||||
async def test_image_quality_reaches_every_encoder_and_farm_high(self):
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
canvas = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.canvas',
|
||||
).Canvas
|
||||
original = canvas.encode
|
||||
encoded = []
|
||||
|
||||
def encode(instance, codec, quality=75, method=2):
|
||||
encoded.append(quality)
|
||||
return original(instance, codec, quality, method)
|
||||
|
||||
await self.call('register')
|
||||
quality = 48
|
||||
self.runtime.image_quality_provider = lambda: quality
|
||||
with patch.object(canvas, 'encode', new=encode):
|
||||
for configured in (48, 95):
|
||||
quality = configured
|
||||
for action in ('farm', 'plot_ids', 'details', 'shop',
|
||||
'seeds', 'crops'):
|
||||
with self.subTest(quality=quality, action=action):
|
||||
encoded.clear()
|
||||
content = await self.call(action, message=None)
|
||||
self.assertIsInstance(content, bytes)
|
||||
self.assertTrue(encoded)
|
||||
self.assertEqual(set(encoded), {quality})
|
||||
encoded.clear()
|
||||
content = await self.call('farm', '高清', message=None)
|
||||
self.assertEqual(set(encoded), {max(85, quality)})
|
||||
with Image.open(BytesIO(content)) as image:
|
||||
self.assertEqual(image.width, 1536)
|
||||
|
||||
async def test_only_farm_accepts_high_and_warehouse_keeps_text(self):
|
||||
await self.call('register')
|
||||
for action in ('plot_ids', 'details', 'shop', 'seeds', 'crops'):
|
||||
for arguments in ('高清', '1 高清'):
|
||||
with self.subTest(action=action, arguments=arguments):
|
||||
self.assertIn('仅我的农场支持高清', await self.call(
|
||||
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.assertNotIn('地块编号 [高清]', help_text)
|
||||
self.assertNotIn('[高清/文字]', help_text)
|
||||
|
||||
async def test_image_quality_validation_and_live_recovery(self):
|
||||
await self.call('register')
|
||||
for invalid in (0, 96, True, 74.5, '75', None):
|
||||
with self.subTest(invalid=invalid):
|
||||
self.runtime.image_quality_provider = lambda: invalid
|
||||
result = await self.call('farm')
|
||||
self.assertIsInstance(result, str)
|
||||
self.assertIn('图片质量配置无效', result)
|
||||
self.assertIsInstance(await self.call('seeds', '文字'), str)
|
||||
self.runtime.image_quality_provider = lambda: 1
|
||||
self.assertIsInstance(await self.call('farm'), bytes)
|
||||
self.runtime.image_quality_provider = lambda: 95
|
||||
self.assertIsInstance(await self.call('farm'), bytes)
|
||||
for invalid in (0, 96, True, 74.5, '75'):
|
||||
with self.assertRaises(ValueError):
|
||||
self.module.Runtime(self.root / 'unused', image_quality=invalid)
|
||||
|
||||
async def test_original_commands_transactions_and_png(self):
|
||||
self.assertNotIn("测试农场", self.module.COMMANDS)
|
||||
self.assertIn("开通农场", self.module.COMMANDS)
|
||||
@@ -233,14 +298,19 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
async def test_pest_command_clears_pests_and_preserves_growth(self):
|
||||
self.assertEqual(self.module.COMMANDS["农场除虫"], "clear_pests")
|
||||
self.assertIn("每只虫", 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
|
||||
await self.call("register")
|
||||
await self.call("buy", "白萝卜 1", "buy")
|
||||
await self.call("sow", "白萝卜 1", "sow")
|
||||
self.assertIn("没有虫", await self.call("clear_pests", message="empty"))
|
||||
duration = self.runtime.service.catalogue.get("白萝卜").growth_seconds
|
||||
self.runtime.service.clock = lambda: 1000 + max(3600, (duration+3)//4)
|
||||
first_pest_at = 1000 + max(7200, (duration + 1) // 2)
|
||||
self.runtime.service.clock = lambda: first_pest_at - 1
|
||||
self.assertNotIn("虫子1只", await self.call("inventory"))
|
||||
self.runtime.service.clock = lambda: first_pest_at
|
||||
self.assertIn("虫子1只", await self.call("inventory"))
|
||||
first = await self.call("clear_pests", message="clear")
|
||||
self.assertIn("除虫已完成", first)
|
||||
@@ -456,8 +526,15 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
platform = ModuleType("zhenxun.utils.platform")
|
||||
platform.PlatformUtils = SimpleNamespace()
|
||||
config = ModuleType("zhenxun.configs.config")
|
||||
quality_values = {'value': 49}
|
||||
config.Config = SimpleNamespace(
|
||||
get=lambda module: SimpleNamespace(configs={}),
|
||||
get=lambda module: SimpleNamespace(
|
||||
configs={}, get=lambda key, default, **kwargs: (
|
||||
quality_values['value'] if not kwargs['build_model']
|
||||
else int(quality_values['value'])
|
||||
),
|
||||
),
|
||||
get_config=lambda *args, **kwargs: int(quality_values['value']),
|
||||
_simple_data={},
|
||||
)
|
||||
modules = {m.__name__:m for m in (
|
||||
@@ -465,6 +542,14 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
)}
|
||||
with patch.dict(sys.modules, modules):
|
||||
imported = importlib.import_module(self.package + ".commands")
|
||||
self.assertEqual(imported.runtime.image_quality_provider(), 49)
|
||||
quality_values['value'] = 91
|
||||
self.assertEqual(imported.runtime.image_quality_provider(), 91)
|
||||
quality_values['value'] = 74.5
|
||||
self.assertEqual(imported.runtime.image_quality_provider(), 74.5)
|
||||
quality_values['value'] = True
|
||||
self.assertIs(imported.runtime.image_quality_provider(), True)
|
||||
quality_values['value'] = 75
|
||||
self.assertEqual(imported.runtime.data_dir,
|
||||
(self.root / 'farm_db').resolve())
|
||||
self.assertEqual(set(hooks["commands"]), set(self.module.COMMANDS))
|
||||
|
||||
Reference in New Issue
Block a user