"""Client 打包目录和发布清单测试。""" import hashlib import json import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from build_tools.launcher import ( UpdateApplyError, app_executable, apply_pending_update, finalize_applied_update, install_root, is_main_program_running, ) from build_tools.release_manifest import ( MANIFEST_FILE_NAME, versioned_manifest_file_name, write_manifest, ) from src.db import app_dir, data_dir class LauncherPathTest(unittest.TestCase): def test_main_program_is_inside_app_directory(self): root = Path("D:/CMAutoBuy") self.assertEqual( app_executable(root), root / "app" / "CMAutoBuy.exe", ) def test_install_root_is_launcher_parent(self): self.assertEqual( install_root("D:/CMAutoBuy/Launcher.exe"), Path("D:/CMAutoBuy"), ) class LauncherUpdateTest(unittest.TestCase): def setUp(self): self.temporary_directory = tempfile.TemporaryDirectory() self.root = Path(self.temporary_directory.name) self.current_app = self.root / "app" self.staged_app = self.root / "data" / "update" / "app.new" self.current_app.mkdir(parents=True) self.staged_app.mkdir(parents=True) (self.current_app / "CMAutoBuy.exe").write_bytes(b"old") (self.staged_app / "CMAutoBuy.exe").write_bytes(b"new") self.pending_path = self.root / "data" / "update" / "pending.json" self.pending_path.write_text( json.dumps( { "schema_version": 1, "state": "ready", "version": "0.2.0", } ), encoding="utf-8", ) def tearDown(self): self.temporary_directory.cleanup() def test_apply_keeps_old_app_until_health_is_confirmed(self): version = apply_pending_update(self.root) self.assertEqual(version, "0.2.0") self.assertEqual((self.root / "app" / "CMAutoBuy.exe").read_bytes(), b"new") self.assertEqual((self.root / "app.old" / "CMAutoBuy.exe").read_bytes(), b"old") pending = json.loads(self.pending_path.read_text(encoding="utf-8")) self.assertEqual(pending["state"], "applied") healthy_path = self.root / "data" / "update" / "healthy.json" healthy_path.write_text('{"version":"0.2.0"}', encoding="utf-8") finalize_applied_update(self.root) self.assertFalse(self.pending_path.exists()) self.assertTrue((self.root / "app.old").exists()) def test_applied_without_health_rolls_back_on_next_launch(self): apply_pending_update(self.root) result = apply_pending_update(self.root) self.assertIsNone(result) self.assertEqual((self.root / "app" / "CMAutoBuy.exe").read_bytes(), b"old") self.assertFalse(self.pending_path.exists()) self.assertTrue((self.root / "data" / "update" / "app.failed").exists()) def test_move_failure_restores_current_app(self): real_replace = os.replace def fail_for_staged_app(source, destination): if Path(source).name == "app.new": raise OSError("simulated lock") return real_replace(source, destination) with patch("build_tools.launcher.os.replace", side_effect=fail_for_staged_app): with self.assertRaises(UpdateApplyError): apply_pending_update(self.root) self.assertEqual((self.root / "app" / "CMAutoBuy.exe").read_bytes(), b"old") self.assertEqual((self.staged_app / "CMAutoBuy.exe").read_bytes(), b"new") def test_running_pid_must_belong_to_expected_executable(self): running_path = self.root / "data" / "update" / "running.json" running_path.write_text('{"pid":123}', encoding="utf-8") self.assertTrue( is_main_program_running( self.root, checker=lambda pid, executable: pid == 123 and executable == self.root / "app" / "CMAutoBuy.exe", ) ) self.assertFalse( is_main_program_running(self.root, checker=lambda _pid, _path: False) ) self.assertFalse(running_path.exists()) def test_corrupt_pending_state_stops_update_instead_of_being_ignored(self): self.pending_path.write_text("not-json", encoding="utf-8") with self.assertRaises(UpdateApplyError): apply_pending_update(self.root) self.assertEqual((self.current_app / "CMAutoBuy.exe").read_bytes(), b"old") class PackagedDataPathTest(unittest.TestCase): def test_packaged_main_program_uses_root_data_directory(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) executable = root / "app" / "CMAutoBuy.exe" with patch("src.db.sys.frozen", True, create=True), patch( "src.db.sys.executable", str(executable) ): self.assertEqual(data_dir(), root / "data") def test_packaged_resource_path_uses_pyinstaller_directory(self): with tempfile.TemporaryDirectory() as directory, patch( "src.db.sys.frozen", True, create=True ), patch("src.db.sys._MEIPASS", directory, create=True): self.assertEqual(app_dir(), Path(directory).resolve()) class BundledAdbPackagingTest(unittest.TestCase): def test_build_script_packages_and_checks_all_adb_files(self): client_root = Path(__file__).resolve().parents[1] script = (client_root / "packaging" / "build.ps1").read_text( encoding="utf-8-sig" ) self.assertIn("--add-binary", script) self.assertIn("vendor/android-platform-tools/windows", script) self.assertIn("adb.exe", script) self.assertIn("AdbWinApi.dll", script) self.assertIn("AdbWinUsbApi.dll", script) class ReleaseManifestTest(unittest.TestCase): def test_manifest_name_and_hash_match_release_files(self): with tempfile.TemporaryDirectory() as directory: root = Path(directory) update_zip = root / "CMAutoBuy_0.1.0.zip" portable_zip = root / "自动采集采购工具.zip" update_zip.write_bytes(b"update") portable_zip.write_bytes(b"portable") output = root / MANIFEST_FILE_NAME write_manifest("0.1.0", update_zip, portable_zip, output) self.assertEqual(output.name, "autobuy_manifest.json") manifest = json.loads(output.read_text(encoding="utf-8")) self.assertEqual(manifest["version"], "0.1.0") self.assertEqual(manifest["update"]["size"], len(b"update")) self.assertEqual( manifest["update"]["sha256"], hashlib.sha256(b"update").hexdigest(), ) self.assertEqual(manifest["update"]["file"], "CMAutoBuy_0.1.0.zip") self.assertEqual(manifest["portable"]["file"], portable_zip.name) def test_versioned_manifest_name_is_stable_and_validated(self): self.assertEqual( versioned_manifest_file_name("0.1.0"), "autobuy_manifest_0.1.0.json", ) with self.assertRaises(ValueError): versioned_manifest_file_name("../latest") if __name__ == "__main__": unittest.main()