Add reversible old-identity migration and freeze corrected release identities

这个提交包含在:
dayu 于 2026-10-22 18:37:21 +08:00 提交
1 父节点 47f6e55ecd
当前提交 767bba8ac1
6 个文件已更改
+283 -26

没有与搜索条件相匹配的文件

+91
查看文件
@@ -0,0 +1,91 @@
import importlib.util, json, pathlib, plistlib, subprocess, tempfile, unittest
from unittest.mock import patch
ROOT = pathlib.Path(__file__).resolve().parent.parent
spec = importlib.util.spec_from_file_location('product_identity', ROOT / 'scripts/product_identity.py')
identity = importlib.util.module_from_spec(spec)
spec.loader.exec_module(identity)
class IdentityMigrationTests(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory(prefix='beautygirl-identity-test.')
self.volume = pathlib.Path(self.tmp.name)
self.legacy = identity.PRODUCT['legacyProducts'][0]
self.old = self.volume / pathlib.Path(self.legacy['installationPath']).relative_to('/')
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents').mkdir(parents=True)
(self.old / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': self.legacy['bundleIdentifier']}))
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents/Info.plist').write_bytes(plistlib.dumps({'ProPlugPlugInList': [{'uuid': identity.PRODUCT['filterUUID']}]}))
self.signature = patch.object(identity.subprocess, 'run', return_value=subprocess.CompletedProcess([], 0, '', 'TeamIdentifier=B3W25FX5H5\n'))
self.signature.start()
self.migration = identity.LegacyMigration(self.volume / 'backups', volume=self.volume)
def tearDown(self):
self.signature.stop()
self.tmp.cleanup()
def test_success_removes_only_validated_old_product(self):
original = (self.old / 'Contents/Info.plist').read_bytes()
unrelated = self.old.parent / 'Unrelated.app'
unrelated.mkdir()
calls = []
self.migration.relocate(calls.append)
self.assertFalse(self.old.exists())
self.assertEqual(len(calls), 1)
self.assertEqual((self.migration.moved[0][2] / 'Contents/Info.plist').read_bytes(), original)
self.assertEqual(len(self.migration.cleanup()), 1)
self.assertTrue(unrelated.exists())
def test_failed_new_install_restores_old_bytes_and_registration(self):
original = (self.old / 'Contents/Info.plist').read_bytes()
self.migration.relocate(lambda p: None)
calls = []
self.migration.rollback(calls.append)
self.assertEqual((self.old / 'Contents/Info.plist').read_bytes(), original)
self.assertEqual(calls, [self.old / 'Contents/PlugIns' / self.legacy['pluginName']])
def test_wrong_bundle_id_is_never_moved(self):
(self.old / 'Contents/Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier': 'com.unrelated.product'}))
with self.assertRaisesRegex(RuntimeError, 'identity mismatch'):
self.migration.relocate(lambda p: self.fail('Must not deregister another product'))
self.assertTrue(self.old.exists())
def test_wrong_filter_uuid_is_never_moved(self):
(self.old / 'Contents/PlugIns' / self.legacy['pluginName'] / 'Contents/Info.plist').write_bytes(plistlib.dumps({'ProPlugPlugInList': []}))
with self.assertRaisesRegex(RuntimeError, 'filter UUID'):
self.migration.relocate(lambda p: self.fail('Must not deregister another filter'))
self.assertTrue(self.old.exists())
def test_invalid_signature_keeps_original(self):
with patch.object(identity.subprocess, 'run', side_effect=subprocess.CalledProcessError(1, 'codesign')):
with self.assertRaises(subprocess.CalledProcessError):
self.migration.relocate(lambda p: self.fail('Unverified APP must remain'))
self.assertTrue(self.old.exists())
def test_existing_backup_cannot_be_overwritten(self):
backup = self.volume / 'backups' / ('legacy-' + self.legacy['name'] + '.app')
backup.mkdir(parents=True)
with self.assertRaisesRegex(RuntimeError, 'already exists'):
self.migration.relocate(lambda p: self.fail('Recovery copy must be preserved'))
self.assertTrue(self.old.exists())
def test_failed_deregistration_keeps_old_installation(self):
def unavailable(path):
raise RuntimeError('registry unavailable')
with self.assertRaisesRegex(RuntimeError, 'registry unavailable'):
self.migration.relocate(unavailable)
self.assertTrue(self.old.exists())
def test_project_and_legacy_template_share_stable_filter_identity(self):
p = identity.PRODUCT
pbx = (ROOT / p['xcodeProject'] / 'project.pbxproj').read_text()
self.assertEqual(pbx.count('PRODUCT_BUNDLE_IDENTIFIER = ' + p['bundleIdentifier'] + ';'), 2)
self.assertEqual(pbx.count('PRODUCT_BUNDLE_IDENTIFIER = ' + p['pluginBundleIdentifier'] + ';'), 2)
self.assertNotIn('beatygril', pbx)
plugin = plistlib.loads((ROOT / p['sourceDirectory'] / 'Plugin/Info.plist').read_bytes())
self.assertEqual(plugin['ProPlugPlugInList'][0]['className'], p['pluginClass'])
self.assertEqual(plugin['ProPlugPlugInList'][0]['uuid'], p['filterUUID'])
self.assertEqual(plugin['CFBundleShortVersionString'], p['version'])
self.assertIn('pluginUUID="' + p['filterUUID'] + '"', (ROOT / 'Templates/大鱼人像美化/大鱼人像美化.moef').read_text())
if __name__ == '__main__':
unittest.main()