mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-29 13:40:00 +08:00
fixed # 将资源文件迁移到Service仓库中
This commit is contained in:
@@ -46,12 +46,13 @@ class BundleTests(unittest.TestCase):
|
||||
bundle.ensure_assets(self.root / 'gitea', 'Gitea')
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
'https://gitea.diuse.work/Diuse/farm_service/'
|
||||
'raw/branch/main/assets/assets.zip')
|
||||
'raw/branch/main/catalogue/releases/'
|
||||
+ self.expected['catalogue_release'] + '/assets.zip')
|
||||
bundle.ensure_assets(self.root / 'service', 'farm_service',
|
||||
'https://farm.example/prefix/')
|
||||
self.assertEqual(fetch.call_args.args[0],
|
||||
'https://farm.example/prefix/v1/assets/'
|
||||
+ self.expected['sha256'] + '/assets.zip')
|
||||
'https://farm.example/prefix/v1/catalogue/releases/'
|
||||
+ self.expected['catalogue_release'] + '/assets.zip')
|
||||
|
||||
def test_failed_download_leaves_no_partial_and_wrong_hash_is_rejected(self):
|
||||
def corrupt(url, destination, limit):
|
||||
|
||||
@@ -146,7 +146,12 @@ class AstrBotPackageTests(unittest.IsolatedAsyncioTestCase):
|
||||
Path(choose_font()).read_bytes())
|
||||
(fonts / 'OFL-Xiaolai.txt').write_bytes((ROOT /
|
||||
'src/farm_render/data/OFL-Xiaolai.txt').read_bytes())
|
||||
await plugin.initialize()
|
||||
with patch(
|
||||
package.__name__ +
|
||||
'._vendor.farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline fixture'),
|
||||
):
|
||||
await plugin.initialize()
|
||||
self.assertEqual(set(commands), set(main.COMMANDS))
|
||||
|
||||
class Event:
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from farm_core.catalogue_release import make_manifest, parse_manifest
|
||||
from farm_render.catalogue_bundle import load_catalogue_release
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def release(price=21, extra=False):
|
||||
plants = json.loads((ROOT / 'src/farm_core/data/plants.json').read_bytes())
|
||||
plants.pop('source_sha256', None)
|
||||
plants['plants'][0]['price'] = price
|
||||
metadata = json.loads(
|
||||
(ROOT / 'src/farm_render/data/metadata.json').read_bytes()
|
||||
)
|
||||
if extra:
|
||||
row = copy.deepcopy(plants['plants'][0])
|
||||
row.update(id=9001, name='更新测试作物')
|
||||
plants['plants'].append(row)
|
||||
metadata['plants'][row['name']] = copy.deepcopy(
|
||||
metadata['plants'][plants['plants'][0]['name']]
|
||||
)
|
||||
files = {
|
||||
'plants.json': json.dumps(plants, ensure_ascii=False).encode(),
|
||||
'metadata.json': json.dumps(metadata, ensure_ascii=False).encode(),
|
||||
**{name: (ROOT / 'src/farm_render/data' / name).read_bytes()
|
||||
for name in ('assets.zip', 'land-soils.zip')},
|
||||
}
|
||||
return make_manifest(files, 'test-v1'), files
|
||||
|
||||
|
||||
class CatalogueReleaseTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
self.manifest, self.files = release(price=99)
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
def fetch(self, url, limit):
|
||||
if url.endswith('/manifest.json'):
|
||||
data = json.dumps(self.manifest).encode()
|
||||
else:
|
||||
self.assertIn('/releases/' + self.manifest['release'] + '/', url)
|
||||
data = self.files[url.rsplit('/', 1)[-1]]
|
||||
self.assertLessEqual(len(data), limit)
|
||||
return data
|
||||
|
||||
def load(self, **kwargs):
|
||||
return load_catalogue_release(self.root, **kwargs)
|
||||
|
||||
def test_new_catalogue_and_metadata_activate_as_one_version(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual(result.origin, 'remote')
|
||||
self.assertTrue(result.bundle_path.is_file())
|
||||
self.assertTrue(result.soils_path.is_file())
|
||||
current = json.loads((self.root / 'catalogue/current.json').read_bytes())
|
||||
self.assertEqual(current['release'], self.manifest['release'])
|
||||
|
||||
def test_404_reuses_verified_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('404')):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
|
||||
def test_wrong_bytes_do_not_replace_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
before = (self.root / 'catalogue/current.json').read_bytes()
|
||||
self.manifest, self.files = release(price=100)
|
||||
self.files['plants.json'] = b'broken'
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(1).price, 99)
|
||||
self.assertEqual((self.root / 'catalogue/current.json').read_bytes(),
|
||||
before)
|
||||
|
||||
def test_first_offline_start_uses_bootstrap_without_images(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline')):
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'bootstrap')
|
||||
self.assertEqual(len(result.catalogue), 202)
|
||||
self.assertFalse((self.root / 'catalogue/current.json').exists())
|
||||
|
||||
def test_cache_loss_with_unknown_player_id_refuses_start(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline')):
|
||||
with self.assertRaisesRegex(ValueError, 'catalogue.*ID'):
|
||||
self.load(required_ids={9001})
|
||||
|
||||
def test_incompatible_downgrade_keeps_added_crop(self):
|
||||
self.manifest, self.files = release(extra=True)
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
self.manifest, self.files = release()
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
result = self.load()
|
||||
self.assertEqual(result.catalogue.get_by_id(9001).name, '更新测试作物')
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
|
||||
def test_manifest_rejects_bool_unknown_schema_paths_and_fingerprint(self):
|
||||
for key, value in [('schema', True), ('schema', 2),
|
||||
('release', '0' * 64)]:
|
||||
bad = copy.deepcopy(self.manifest)
|
||||
bad[key] = value
|
||||
with self.assertRaises(ValueError):
|
||||
parse_manifest(json.dumps(bad).encode())
|
||||
bad = copy.deepcopy(self.manifest)
|
||||
bad['files']['../file'] = bad['files'].pop('assets.zip')
|
||||
with self.assertRaises(ValueError):
|
||||
parse_manifest(json.dumps(bad).encode())
|
||||
|
||||
def test_selected_service_prefix_is_respected(self):
|
||||
seen = []
|
||||
def fetch(url, limit):
|
||||
seen.append(url)
|
||||
return self.fetch(url, limit)
|
||||
with patch('farm_render.catalogue_bundle._fetch', side_effect=fetch):
|
||||
self.load(source='farm_service',
|
||||
service_url='https://farm.test/prefix')
|
||||
self.assertTrue(all(
|
||||
url.startswith('https://farm.test/prefix/v1/catalogue/')
|
||||
for url in seen
|
||||
))
|
||||
|
||||
def test_current_matching_remote_still_checks_player_ids(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
with self.assertRaisesRegex(ValueError, 'catalogue.*ID'):
|
||||
self.load(required_ids={9001})
|
||||
|
||||
def test_consistent_but_invalid_metadata_keeps_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
self.files['metadata.json'] = b'{"schema":1,"canvas":[0,0]}'
|
||||
self.manifest = make_manifest(self.files, 'bad-layout')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertEqual(result.metadata['canvas'], [3072, 1872])
|
||||
|
||||
def test_huge_coordinate_keeps_verified_current(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
metadata = json.loads(self.files['metadata.json'])
|
||||
metadata['positions'][0]['x'] = 10 ** 500
|
||||
self.files['metadata.json'] = json.dumps(metadata).encode()
|
||||
self.manifest = make_manifest(self.files, 'huge-coordinate')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertLess(result.metadata['positions'][0]['x'], 10000000)
|
||||
|
||||
def test_removing_old_crop_image_is_rejected_even_with_valid_hashes(self):
|
||||
with patch('farm_render.catalogue_bundle._fetch',
|
||||
side_effect=self.fetch):
|
||||
self.load()
|
||||
metadata = json.loads(self.files['metadata.json'])
|
||||
name = next(key for key in metadata['images']
|
||||
if key.startswith('plant/'))
|
||||
metadata['images'].pop(name)
|
||||
output = BytesIO()
|
||||
with ZipFile(BytesIO(self.files['assets.zip'])) as old, \
|
||||
ZipFile(output, 'w') as new:
|
||||
for item in old.infolist():
|
||||
if item.filename != name:
|
||||
new.writestr(item, old.read(item))
|
||||
self.files['assets.zip'] = output.getvalue()
|
||||
self.files['metadata.json'] = json.dumps(metadata).encode()
|
||||
self.manifest = make_manifest(self.files, 'missing-image')
|
||||
result = self.load()
|
||||
self.assertEqual(result.origin, 'cache')
|
||||
self.assertIn(name, result.metadata['images'])
|
||||
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
from contextlib import closing
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
from farm_core import SQLiteStore
|
||||
|
||||
|
||||
class CatalogueStoreTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_missing_store_is_not_created_by_catalogue_check(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'not-created.sqlite3'
|
||||
store = SQLiteStore(path)
|
||||
self.assertEqual(await store.catalogue_ids(), set())
|
||||
self.assertFalse(path.exists())
|
||||
|
||||
async def test_existing_ids_read_without_changing_database(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
path = Path(temporary) / 'farm.sqlite3'
|
||||
store = SQLiteStore(path)
|
||||
await store.initialize()
|
||||
await store.close()
|
||||
with closing(sqlite3.connect(path)) as db, db:
|
||||
db.execute('INSERT INTO accounts(identity,name) VALUES (?,?)',
|
||||
('fixture', 'synthetic'))
|
||||
db.execute('INSERT INTO inventory(identity,plant_id,name,seeds)'
|
||||
' VALUES (?,?,?,?)', ('fixture', 9001, 'test', 1))
|
||||
db.execute('INSERT INTO plots(identity,slot,plant) VALUES (?,?,?)',
|
||||
('fixture', 1, json.dumps({'plant_id': 9002})))
|
||||
before = path.read_bytes()
|
||||
self.assertEqual(await store.catalogue_ids(), {9001, 9002})
|
||||
self.assertEqual(path.read_bytes(), before)
|
||||
@@ -11,6 +11,8 @@ from export_render_assets import export
|
||||
class RenderExportTests(unittest.TestCase):
|
||||
def test_export_preserves_published_land_numbering_and_calibration(self):
|
||||
_, metadata = export(ROOT)
|
||||
stored = (ROOT / "src/farm_render/data/metadata.json").read_bytes()
|
||||
self.assertEqual(json.loads(metadata), json.loads(stored))
|
||||
self.assertEqual(metadata, stored)
|
||||
data = json.loads(metadata)
|
||||
self.assertEqual(len(data['plants']), 202)
|
||||
self.assertEqual(len(data['positions']), 30)
|
||||
self.assertEqual(data['placement']['mode'], 'visible_contact_center')
|
||||
self.assertEqual(metadata, export(ROOT)[1])
|
||||
|
||||
@@ -27,6 +27,12 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
module.__path__ = [str(self.root / "built/repository")]
|
||||
sys.modules[self.package] = module
|
||||
self.module = importlib.import_module(self.package + ".runtime")
|
||||
offline = patch(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle._fetch',
|
||||
side_effect=OSError('offline fixture'),
|
||||
)
|
||||
offline.start()
|
||||
self.addCleanup(offline.stop)
|
||||
self.runtime = self.module.Runtime(self.root / "data", choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
await self.runtime.start()
|
||||
@@ -78,6 +84,133 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
for action in ('seeds', 'crops'):
|
||||
self.assertIsInstance(await self.call(action, '1'), bytes)
|
||||
|
||||
async def test_catalogue_is_loaded_once_before_store_initialization(self):
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
await self.call('register')
|
||||
await self.call('buy', '胡萝卜 1', message='seed')
|
||||
await self.call('sow', '胡萝卜 1', message='sow')
|
||||
before = await self.runtime.service.snapshot(self.runtime_identity())
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
original = bundle._bootstrap()
|
||||
document = json.loads((ROOT / 'src/farm_core/data/plants.json')
|
||||
.read_bytes())
|
||||
document['plants'][0]['price'] = 99
|
||||
document['plants'][0]['harvest'] = 999
|
||||
updated = replace(original, catalogue=self.module.Catalogue.loads(
|
||||
json.dumps(document),
|
||||
))
|
||||
self.runtime = self.module.Runtime(self.root / 'data', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
return_value=updated) as load:
|
||||
await asyncio.gather(self.runtime.start(), self.runtime.start())
|
||||
await self.runtime.start()
|
||||
self.assertEqual(load.call_count, 1)
|
||||
self.assertEqual(self.runtime.service.catalogue.get_by_id(1).price, 99)
|
||||
after = await self.runtime.service.snapshot(self.runtime_identity())
|
||||
self.assertEqual(after.plots[0].plant, before.plots[0].plant)
|
||||
self.assertNotEqual(after.plots[0].plant.harvest, 999)
|
||||
|
||||
async def test_close_during_catalogue_load_does_not_start_service(self):
|
||||
import threading
|
||||
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
snapshot = bundle._bootstrap()
|
||||
entered, release = threading.Event(), threading.Event()
|
||||
def slow(*args):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise TimeoutError('fixture did not release')
|
||||
return snapshot
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
side_effect=slow):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await asyncio.wait_for(asyncio.to_thread(entered.wait), 2)
|
||||
closing = asyncio.create_task(self.runtime.close())
|
||||
await asyncio.sleep(0)
|
||||
release.set()
|
||||
await asyncio.gather(starting, closing)
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertFalse((self.root / 'other/farm.sqlite3').exists())
|
||||
|
||||
async def test_close_during_id_read_prevents_later_start(self):
|
||||
await self.runtime.close()
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
async def slow_ids():
|
||||
entered.set()
|
||||
await release.wait()
|
||||
return set()
|
||||
with patch.object(self.runtime.store, 'catalogue_ids',
|
||||
side_effect=slow_ids):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await entered.wait()
|
||||
await self.runtime.close()
|
||||
release.set()
|
||||
await starting
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertFalse((self.root / 'other/farm.sqlite3').exists())
|
||||
|
||||
async def test_cancelled_start_drains_catalogue_download(self):
|
||||
import threading
|
||||
|
||||
await self.runtime.close()
|
||||
bundle = importlib.import_module(
|
||||
self.package + '._vendor.farm_render.catalogue_bundle',
|
||||
)
|
||||
snapshot = bundle._bootstrap()
|
||||
entered, release, finished = (threading.Event() for _ in range(3))
|
||||
def slow(*args):
|
||||
entered.set()
|
||||
if not release.wait(5):
|
||||
raise TimeoutError('fixture did not release')
|
||||
finished.set()
|
||||
return snapshot
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
with patch.object(self.module, 'load_catalogue_release',
|
||||
side_effect=slow):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await asyncio.wait_for(asyncio.to_thread(entered.wait), 2)
|
||||
starting.cancel()
|
||||
await asyncio.sleep(0.01)
|
||||
self.assertFalse(starting.done())
|
||||
release.set()
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
await starting
|
||||
self.assertTrue(finished.is_set())
|
||||
self.assertIsNone(self.runtime.log_handler)
|
||||
self.assertFalse(self.runtime.started)
|
||||
|
||||
async def test_close_during_initialization_prevents_later_start(self):
|
||||
await self.runtime.close()
|
||||
self.runtime = self.module.Runtime(self.root / 'other', choose_font())
|
||||
self.seed_assets(self.runtime)
|
||||
entered, release = asyncio.Event(), asyncio.Event()
|
||||
initialize = self.runtime.store.initialize
|
||||
async def slow_initialize(**kwargs):
|
||||
await initialize(**kwargs)
|
||||
entered.set()
|
||||
await release.wait()
|
||||
with patch.object(self.runtime.store, 'initialize',
|
||||
side_effect=slow_initialize):
|
||||
starting = asyncio.create_task(self.runtime.start())
|
||||
await entered.wait()
|
||||
await self.runtime.close()
|
||||
release.set()
|
||||
await starting
|
||||
self.assertFalse(self.runtime.started)
|
||||
self.assertIsNone(self.runtime.rendering)
|
||||
|
||||
async def test_land_upgrade_command_conditions_charge_and_replay(self):
|
||||
self.assertEqual(self.module.COMMANDS['土地升级'], 'upgrade')
|
||||
await self.call('register')
|
||||
@@ -577,6 +710,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
for resource in ('assets', 'font'):
|
||||
other = self.module.Runtime(self.root / ('stop-' + resource))
|
||||
self.seed_assets(other)
|
||||
if resource == 'assets':
|
||||
next((other.data_dir / 'assets').glob('*/assets.zip')).unlink()
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=TimeoutError()) as fetch:
|
||||
@@ -598,6 +733,7 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
await asyncio.sleep(0)
|
||||
self.assertEqual(fetch.call_count, 1)
|
||||
await other.close()
|
||||
self.seed_assets(other)
|
||||
await other.start()
|
||||
self.assertIsNotNone(other.rendering)
|
||||
finally:
|
||||
@@ -614,6 +750,8 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
):
|
||||
other = self.module.Runtime(self.root / ('failed-' + resource))
|
||||
self.seed_assets(other)
|
||||
if resource == 'assets':
|
||||
next((other.data_dir / 'assets').glob('*/assets.zip')).unlink()
|
||||
try:
|
||||
with patch.object(self.module, 'ensure_' + resource,
|
||||
side_effect=failure), \
|
||||
|
||||
Reference in New Issue
Block a user