mirror of
https://github.com/Shu-Ying/main_plugin_farm.git
synced 2026-10-28 21:19:58 +08:00
fixed # 修复迁移数据库失败的BUG
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -20,66 +19,8 @@ class DataDirectoryTests(unittest.TestCase):
|
||||
self.base = Path(self.temp.name)
|
||||
|
||||
def test_default_and_explicit_directory(self):
|
||||
self.assertEqual(self.module.data_directories(self.base), (
|
||||
self.base / 'farm_db', self.base / 'farm_preview',
|
||||
))
|
||||
custom = self.base / 'custom'
|
||||
self.assertEqual(self.module.data_directories(self.base, custom), (
|
||||
custom, None,
|
||||
))
|
||||
|
||||
def test_migrate_preserves_database_log_and_sidecars(self):
|
||||
old = self.base / 'farm_preview'
|
||||
old.mkdir()
|
||||
for name in ('farm-preview.sqlite3', 'farm-preview.sqlite3-wal',
|
||||
'farm-preview.log'):
|
||||
(old / name).write_bytes(name.encode())
|
||||
target = self.base / 'farm_db'
|
||||
self.assertTrue(self.module.prepare_directory(target, old))
|
||||
self.assertFalse(old.exists())
|
||||
for path in target.iterdir():
|
||||
self.assertEqual(path.read_bytes(), path.name.encode())
|
||||
self.assertFalse(self.module.prepare_directory(target, old))
|
||||
|
||||
def test_conflicting_directory_stops_without_overwrite(self):
|
||||
old, target = self.base / 'farm_preview', self.base / 'farm_db'
|
||||
old.mkdir()
|
||||
target.mkdir()
|
||||
(old / 'farm-preview.sqlite3').write_bytes(b'old')
|
||||
(target / 'farm-preview.sqlite3').write_bytes(b'new')
|
||||
with self.assertRaises(FileExistsError):
|
||||
self.module.prepare_directory(target, old)
|
||||
self.assertEqual((old / 'farm-preview.sqlite3').read_bytes(), b'old')
|
||||
self.assertEqual((target / 'farm-preview.sqlite3').read_bytes(), b'new')
|
||||
|
||||
def test_migration_rejects_paths_outside_default_siblings(self):
|
||||
with self.assertRaises(ValueError):
|
||||
self.module.prepare_directory(
|
||||
self.base / 'farm_db', self.base / 'other/farm_preview',
|
||||
)
|
||||
|
||||
@unittest.skipUnless(os.name == 'nt', 'Windows short paths only')
|
||||
def test_migration_accepts_short_and_long_parent_paths(self):
|
||||
import ctypes
|
||||
|
||||
parent = self.base / 'Farm migration long directory'
|
||||
parent.mkdir()
|
||||
buffer = ctypes.create_unicode_buffer(32768)
|
||||
get_short_path = ctypes.windll.kernel32.GetShortPathNameW
|
||||
get_short_path.argtypes = (
|
||||
ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint,
|
||||
self.assertEqual(
|
||||
self.module.data_directory(self.base), self.base / 'farm_db',
|
||||
)
|
||||
get_short_path.restype = ctypes.c_uint
|
||||
length = get_short_path(str(parent), buffer, len(buffer))
|
||||
self.assertTrue(0 < length < len(buffer))
|
||||
short_parent = Path(buffer.value)
|
||||
if short_parent == parent.resolve():
|
||||
self.skipTest('Short path aliases are disabled on this volume')
|
||||
old = short_parent / 'farm_preview'
|
||||
old.mkdir()
|
||||
(old / 'farm-preview.sqlite3').write_bytes(b'preserved')
|
||||
target = parent.resolve() / 'farm_db'
|
||||
self.assertTrue(self.module.prepare_directory(target, old))
|
||||
self.assertEqual((target / 'farm-preview.sqlite3').read_bytes(),
|
||||
b'preserved')
|
||||
self.assertFalse(old.exists())
|
||||
custom = self.base / 'custom'
|
||||
self.assertEqual(self.module.data_directory(self.base, custom), custom)
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import asyncio
|
||||
from contextlib import closing
|
||||
from dataclasses import replace
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'src'))
|
||||
|
||||
from farm_core import Catalogue, FarmService, Identity, SQLiteStore
|
||||
from farm_core.domain import FarmError
|
||||
|
||||
|
||||
def create_legacy(path):
|
||||
with closing(sqlite3.connect(path)) as db, db:
|
||||
db.executescript('''
|
||||
CREATE TABLE user(uid TEXT PRIMARY KEY, name TEXT, exp INTEGER,
|
||||
point INTEGER, vipPoint INTEGER, soil INTEGER);
|
||||
CREATE TABLE userSeed(uid TEXT, seed TEXT, count INTEGER);
|
||||
CREATE TABLE userPlant(uid TEXT, plant TEXT, count INTEGER,
|
||||
isLock INTEGER);
|
||||
CREATE TABLE userSoil(uid TEXT, soilIndex INTEGER, plantName TEXT,
|
||||
matureTime INTEGER, soilLevel INTEGER, wiltStatus INTEGER,
|
||||
bugStatus INTEGER, harvestCount INTEGER, isSoilPlanted INTEGER);
|
||||
CREATE TABLE userItem(uid TEXT, item TEXT, count INTEGER);
|
||||
INSERT INTO user VALUES ('1', '旧农场', 900, 1234, 56, 4);
|
||||
INSERT INTO userSeed VALUES ('1', '白萝卜', 12);
|
||||
INSERT INTO userPlant VALUES ('1', '白萝卜', 23, 1);
|
||||
INSERT INTO userSoil VALUES
|
||||
('1', 1, '白萝卜', 2000, 2, 0, 1, 0, 1),
|
||||
('1', 2, '白萝卜', 1000, 3, 1, 0, 0, 1),
|
||||
('1', 3, '', 0, 0, 0, 0, 0, 0);
|
||||
INSERT INTO userItem VALUES ('1', '化肥', 7);
|
||||
''')
|
||||
|
||||
|
||||
class LegacyMigrationTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name)
|
||||
self.source = self.root / 'farm.db'
|
||||
create_legacy(self.source)
|
||||
self.catalogue = Catalogue.bundled()
|
||||
self.store = SQLiteStore(self.root / 'farm.sqlite3')
|
||||
await self.store.initialize(self.catalogue)
|
||||
self.who = Identity('zhenxun-preview', 'bot', 'OneBot V11', '1')
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.store.close()
|
||||
self.temp.cleanup()
|
||||
|
||||
async def migrate(self, who=None):
|
||||
return await self.store.migrate_legacy(
|
||||
self.source, who or self.who, self.catalogue, 1500,
|
||||
)
|
||||
|
||||
async def assert_empty(self):
|
||||
with closing(sqlite3.connect(self.store.path)) as db, db:
|
||||
for table in ('accounts', 'inventory', 'plots', 'operations',
|
||||
'ledger'):
|
||||
self.assertEqual(db.execute(
|
||||
f'SELECT COUNT(*) FROM {table}'
|
||||
).fetchone()[0], 0)
|
||||
|
||||
async def test_assets_plots_archive_and_source_preserved(self):
|
||||
before = self.source.read_bytes()
|
||||
self.assertTrue(await self.migrate())
|
||||
snapshot = await self.store.snapshot(self.who)
|
||||
self.assertEqual((snapshot.account.name, snapshot.account.point,
|
||||
snapshot.account.vip_point,
|
||||
snapshot.account.experience),
|
||||
('旧农场', 1234, 56, 900))
|
||||
item = snapshot.inventory[0]
|
||||
self.assertEqual((item.seeds, item.crops, item.locked), (12, 23, True))
|
||||
self.assertEqual(len(snapshot.plots), 4)
|
||||
self.assertEqual(snapshot.plots[0].mature_at, 2000)
|
||||
self.assertEqual(snapshot.plots[0].pest_count, 1)
|
||||
self.assertEqual(snapshot.plots[0].yield_bonus, 20)
|
||||
self.assertTrue(snapshot.plots[1].withered)
|
||||
self.assertEqual(snapshot.plots[1].experience_bonus, 28)
|
||||
self.assertIsNone(snapshot.plots[3].plant)
|
||||
with closing(sqlite3.connect(self.store.path)) as db, db:
|
||||
result = json.loads(db.execute(
|
||||
"SELECT result FROM operations WHERE action='legacy_import'"
|
||||
).fetchone()[0])
|
||||
self.assertEqual(result['legacy_rows']['userItem'][0]['count'], 7)
|
||||
self.assertEqual(result['unsupported_tables'], ['userItem'])
|
||||
self.assertEqual(db.execute(
|
||||
'SELECT SUM(delta) FROM ledger WHERE asset=?', ('point',),
|
||||
).fetchone()[0], 1234)
|
||||
self.assertEqual(db.execute('PRAGMA foreign_key_check').fetchall(),
|
||||
[])
|
||||
self.assertEqual(self.source.read_bytes(), before)
|
||||
|
||||
async def test_repeated_import_does_not_restore_spent_assets(self):
|
||||
await self.migrate()
|
||||
service = FarmService(self.store, self.catalogue, clock=lambda: 1500)
|
||||
await service.buy(self.who, '白萝卜', 1, 'buy')
|
||||
before = await self.store.snapshot(self.who)
|
||||
self.assertFalse(await self.migrate())
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
await self.store.close()
|
||||
self.store = SQLiteStore(self.root / 'farm.sqlite3')
|
||||
await self.store.initialize(self.catalogue)
|
||||
self.source.rename(self.root / 'farm.db.saved')
|
||||
self.assertFalse(await self.migrate())
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
|
||||
async def test_concurrent_identities_bind_only_once(self):
|
||||
other = Identity('zhenxun-preview', 'other', 'QQ', '1')
|
||||
results = await asyncio.gather(
|
||||
self.migrate(), self.migrate(other), return_exceptions=True,
|
||||
)
|
||||
self.assertEqual(sum(result is True for result in results), 1)
|
||||
failures = [r for r in results if isinstance(r, FarmError)]
|
||||
self.assertEqual([r.code for r in failures], ['legacy_identity_bound'])
|
||||
with closing(sqlite3.connect(self.store.path)) as db, db:
|
||||
self.assertEqual(db.execute(
|
||||
'SELECT COUNT(*) FROM accounts'
|
||||
).fetchone()[0], 1)
|
||||
|
||||
async def test_existing_account_is_not_overwritten(self):
|
||||
service = FarmService(self.store, self.catalogue, clock=lambda: 1500)
|
||||
await service.register(self.who, '新农场', 'new')
|
||||
before = await self.store.snapshot(self.who)
|
||||
with self.assertRaisesRegex(FarmError, 'legacy_account_conflict'):
|
||||
await self.migrate()
|
||||
self.assertEqual(await self.store.snapshot(self.who), before)
|
||||
|
||||
async def test_unknown_crop_rolls_back_all_tables(self):
|
||||
with closing(sqlite3.connect(self.source)) as db, db:
|
||||
db.execute("UPDATE userSeed SET seed='不存在的作物'")
|
||||
with self.assertRaisesRegex(FarmError, 'unknown_plant'):
|
||||
await self.migrate()
|
||||
await self.assert_empty()
|
||||
|
||||
async def test_negative_balance_is_rejected(self):
|
||||
with closing(sqlite3.connect(self.source)) as db, db:
|
||||
db.execute('UPDATE user SET point=-1')
|
||||
with self.assertRaisesRegex(FarmError, 'invalid_integer'):
|
||||
await self.migrate()
|
||||
await self.assert_empty()
|
||||
|
||||
async def test_ambiguous_crop_is_not_guessed(self):
|
||||
plant = self.catalogue.get('白萝卜')
|
||||
self.catalogue = Catalogue((plant, replace(plant, plant_id=999999)))
|
||||
with self.assertRaisesRegex(FarmError, 'ambiguous_plant'):
|
||||
await self.migrate()
|
||||
await self.assert_empty()
|
||||
|
||||
async def test_duplicate_plot_rolls_back_and_can_retry_after_repair(self):
|
||||
with closing(sqlite3.connect(self.source)) as db, db:
|
||||
db.execute('INSERT INTO userSoil SELECT * FROM userSoil '
|
||||
'WHERE soilIndex=1')
|
||||
with self.assertRaisesRegex(FarmError, 'legacy_data_invalid'):
|
||||
await self.migrate()
|
||||
await self.assert_empty()
|
||||
with closing(sqlite3.connect(self.source)) as db, db:
|
||||
db.execute('DELETE FROM userSoil WHERE rowid='
|
||||
'(SELECT MAX(rowid) FROM userSoil)')
|
||||
self.assertTrue(await self.migrate())
|
||||
|
||||
async def test_missing_source_or_user_does_not_create_account(self):
|
||||
self.assertFalse(await self.migrate(
|
||||
Identity('zhenxun-preview', 'bot', 'OneBot V11', 'missing'),
|
||||
))
|
||||
self.source.unlink()
|
||||
self.assertFalse(await self.migrate())
|
||||
await self.assert_empty()
|
||||
|
||||
async def test_missing_schema_is_rejected(self):
|
||||
with closing(sqlite3.connect(self.source)) as db, db:
|
||||
db.execute('DROP TABLE userSeed')
|
||||
with self.assertRaisesRegex(FarmError, 'legacy_schema_unsupported'):
|
||||
await self.migrate()
|
||||
await self.assert_empty()
|
||||
@@ -1,4 +1,5 @@
|
||||
import asyncio
|
||||
from contextlib import closing
|
||||
from dataclasses import replace
|
||||
import importlib
|
||||
import importlib.util
|
||||
@@ -216,29 +217,61 @@ class ZhenxunRuntimeTests(unittest.IsolatedAsyncioTestCase):
|
||||
)
|
||||
self.assertEqual(before, await self.call("inventory"))
|
||||
|
||||
async def test_default_directory_migration_keeps_account_and_inventory(self):
|
||||
old = self.root / 'migration/farm_preview'
|
||||
async def test_existing_preview_directory_is_ignored(self):
|
||||
old = self.root / 'directories/farm_preview'
|
||||
target = old.with_name('farm_db')
|
||||
previous = self.module.Runtime(old, choose_font())
|
||||
await previous.start()
|
||||
try:
|
||||
await previous.execute('register', '迁移农场', **self.context)
|
||||
before = await previous.execute('inventory', '', **self.context)
|
||||
await previous.execute('register', '旧农场', **self.context)
|
||||
finally:
|
||||
await previous.close()
|
||||
current = self.module.Runtime(
|
||||
target, choose_font(), legacy_data_dir=old,
|
||||
)
|
||||
old_database = old / 'farm.sqlite3'
|
||||
before = old_database.read_bytes()
|
||||
target.mkdir()
|
||||
current = self.module.Runtime(target, choose_font())
|
||||
await current.start()
|
||||
try:
|
||||
self.assertFalse(old.exists())
|
||||
self.assertTrue((target / 'farm-preview.sqlite3').is_file())
|
||||
self.assertEqual(old_database.read_bytes(), before)
|
||||
self.assertTrue((target / 'farm.sqlite3').is_file())
|
||||
self.assertEqual(await current.execute(
|
||||
'inventory', '', **self.context,
|
||||
), before)
|
||||
), '请先发送:开通农场')
|
||||
finally:
|
||||
await current.close()
|
||||
|
||||
async def test_first_command_imports_legacy_and_binds_identity(self):
|
||||
from test_legacy_migration import create_legacy
|
||||
|
||||
source = self.root / 'data/farm.db'
|
||||
create_legacy(source)
|
||||
before = source.read_bytes()
|
||||
result = await self.call('inventory')
|
||||
self.assertIn('旧农场', result)
|
||||
self.assertIn('1234', result)
|
||||
self.assertTrue((self.root / 'data/farm.sqlite3').is_file())
|
||||
self.assertFalse((self.root / 'data/farm-preview.sqlite3').exists())
|
||||
self.assertEqual(await self.call('inventory'), result)
|
||||
self.assertIn('已绑定其他机器人或平台', await self.call(
|
||||
'register', bot_id='other',
|
||||
))
|
||||
self.assertEqual(source.read_bytes(), before)
|
||||
|
||||
async def test_failed_migration_does_not_register_empty_account(self):
|
||||
from test_legacy_migration import create_legacy
|
||||
import sqlite3
|
||||
|
||||
source = self.root / 'data/farm.db'
|
||||
create_legacy(source)
|
||||
with closing(sqlite3.connect(source)) as db, db:
|
||||
db.execute("UPDATE userSeed SET seed='不存在的作物'")
|
||||
self.assertIn('无法迁移', await self.call('register'))
|
||||
identity = self.module.Identity(
|
||||
'zhenxun-preview', 'bot', 'OneBot V11', '1',
|
||||
)
|
||||
with self.assertRaisesRegex(self.module.FarmError, 'not_registered'):
|
||||
await self.runtime.store.snapshot(identity)
|
||||
|
||||
async def test_mock_host_load_registers_hooks_and_original_matchers(self):
|
||||
hooks = {"start":[], "close":[], "commands":{}}
|
||||
driver = SimpleNamespace(
|
||||
|
||||
Reference in New Issue
Block a user