fixed # 新增本地部署脚本,一键生成对应平台的压缩包内容

This commit is contained in:
2026-10-22 18:33:42 +08:00
parent 37c933db0c
commit d238005561
41 changed files with 1158 additions and 545 deletions
+157
View File
@@ -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]
+48
View File
@@ -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)
+62 -1
View File
@@ -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)
+5
View File
@@ -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
View File
@@ -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)
+44
View File
@@ -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"})
+6
View File
@@ -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)
+91 -6
View File
@@ -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))