mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-07 15:13:51 +08:00
- 安装前获取维护锁并枚举同目录进程,仅检测到其他实例时触发关闭确认 - 改用 Windows 原生系统模态对话框并先收起下载进度窗口,避免确认提示被遮挡 - 单实例直接进入安装,取消确认或退出检查中止后恢复下载完成状态 - 多语言确认文案展示实际实例数量,补齐前后端与发布工作流回归测试
201 lines
12 KiB
Python
201 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
import unittest
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
WORKFLOWS = (
|
|
ROOT / ".github" / "workflows" / "release.yml",
|
|
ROOT / ".github" / "workflows" / "dev-build.yml",
|
|
)
|
|
PUBLISH_WORKFLOW = ROOT / ".github" / "workflows" / "publish-release.yml"
|
|
INSTALLER = ROOT / "build" / "windows" / "installer.wxs"
|
|
LOCAL_RELEASE_SCRIPT = ROOT / "build-release.sh"
|
|
WIX_NAMESPACE = "http://wixtoolset.org/schemas/v4/wxs"
|
|
UPGRADE_CODE = "CDD6BF2F-ED1E-4345-A0AB-DCDB7E15FB23"
|
|
AMD64_COMPONENT_GUID = "0BCEE70B-9CF2-449C-9ADE-190188493234"
|
|
ARM64_COMPONENT_GUID = "7F4C7757-329F-42B0-B312-D4B8AD50415E"
|
|
AMD64_DESKTOP_SHORTCUT_COMPONENT_GUID = "114030DD-DB5F-460A-9CAC-997680E9F938"
|
|
ARM64_DESKTOP_SHORTCUT_COMPONENT_GUID = "3CF7DCD2-2658-432D-8677-AB6BB34A9659"
|
|
|
|
|
|
class WindowsReleaseArtifactsTest(unittest.TestCase):
|
|
def test_release_workflows_publish_portable_and_msi_assets(self) -> None:
|
|
for workflow in WORKFLOWS:
|
|
with self.subTest(workflow=workflow.name):
|
|
source = workflow.read_text(encoding="utf-8")
|
|
windows_packaging = source.split("# Windows Packaging", 1)[1].split("# Linux Packaging", 1)[0]
|
|
self.assertIn("Package Windows Portable ZIP, bridge EXE, and MSI", source)
|
|
self.assertIn("-Portable.exe", source)
|
|
self.assertIn("-Portable.zip", source)
|
|
self.assertIn("-Installer.msi", source)
|
|
self.assertIn("GoNavi-*.zip", source)
|
|
self.assertIn("GoNavi-*.msi", source)
|
|
self.assertIn('$portableApp = Join-Path $portableDir "GoNavi.exe"', source)
|
|
self.assertIn('$portableLicense = Join-Path $portableDir "LICENSE"', source)
|
|
self.assertIn('$portableNotice = Join-Path $portableDir "NOTICE"', source)
|
|
self.assertIn(
|
|
"Compress-Archive -LiteralPath $portableFiles -DestinationPath $portableZipPath -CompressionLevel Optimal -Force",
|
|
source,
|
|
)
|
|
self.assertIn("dotnet tool install wix --tool-path $wixTools --version $wixVersion", source)
|
|
self.assertIn('WixToolset.UI.wixext/$wixVersion', source)
|
|
self.assertIn('WixToolset.UI.wixext/6.0.2', source)
|
|
self.assertIn("-arch $wixArch", source)
|
|
self.assertIn("TestExpectedAssetNameForWindowsInstallMode", source)
|
|
self.assertIn("TestBuildWindowsLaunchCommandUsesHiddenPowerShellFile", source)
|
|
self.assertIn("TestPortableUpdatePackageAcceptsZipAndLegacyExe", source)
|
|
self.assertIn("TestResolveReusableStagedUpdateForPlatformReusesPortableZipInsideStagedDir", source)
|
|
self.assertIn("TestResolveWindowsUpdateFinalTargetPathMapsDownloadedPortableZipToExe", source)
|
|
self.assertIn("TestInstallUpdateAndRestartMSI", source)
|
|
self.assertIn("TestShouldEnableWindowsMSISingleInstanceOnlyForInstalledMainGUI", source)
|
|
self.assertIn("TestAcquireWindowsMSISingleInstance", source)
|
|
self.assertIn("TestAcquireWindowsUpdateMaintenance", source)
|
|
self.assertIn("TestPrepareWindowsUpdateHandoff", source)
|
|
self.assertIn("TestWindowsUpdateRequiresExplicitCloseConfirmation", source)
|
|
self.assertIn("TestInstallUpdateAndRestartRequiresCloseConfirmationOnWindows", source)
|
|
self.assertIn("TestInstallUpdateAndRestartSkipsCloseConfirmationForSingleWindowsInstance", source)
|
|
self.assertIn("TestFindOtherWindowsUpdateInstances", source)
|
|
self.assertIn("TestCloseWindowsUpdateInstances", source)
|
|
self.assertIn("TestInstallUpdateAndRestartClosesOtherTargetInstances", source)
|
|
self.assertIn("TestBuildWindowsMSIUpdatePowerShellScript", source)
|
|
self.assertIn('-d "ProductName=GoNavi"', source)
|
|
self.assertIn(f'-d "UpgradeCode={UPGRADE_CODE}"', source)
|
|
self.assertIn('-d "InstallFolderName=GoNavi"', source)
|
|
self.assertIn('-d "RegistryKeyName=GoNavi"', source)
|
|
self.assertIn(f'"{AMD64_COMPONENT_GUID}"', source)
|
|
self.assertIn(f'"{ARM64_COMPONENT_GUID}"', source)
|
|
self.assertIn(f'"{AMD64_DESKTOP_SHORTCUT_COMPONENT_GUID}"', source)
|
|
self.assertIn(f'"{ARM64_DESKTOP_SHORTCUT_COMPONENT_GUID}"', source)
|
|
self.assertIn(
|
|
f'$desktopShortcutComponentGuid = if ($archName -eq "Arm64") {{\n'
|
|
f' "{ARM64_DESKTOP_SHORTCUT_COMPONENT_GUID}"\n'
|
|
f' }} else {{\n'
|
|
f' "{AMD64_DESKTOP_SHORTCUT_COMPONENT_GUID}"',
|
|
source,
|
|
)
|
|
self.assertIn('-d "DesktopShortcutComponentGuid=$desktopShortcutComponentGuid"', source)
|
|
self.assertIn('$installMarkerPath = Join-Path $env:RUNNER_TEMP ".gonavi-msi-install"', source)
|
|
self.assertIn('-d "InstallMarker=$installMarkerPath"', source)
|
|
self.assertIn('$licenseFile = (Resolve-Path -LiteralPath "..\\\\..\\\\LICENSE").Path', source)
|
|
self.assertIn('$noticeFile = (Resolve-Path -LiteralPath "..\\\\..\\\\NOTICE").Path', source)
|
|
self.assertIn('-d "LicenseFile=$licenseFile"', source)
|
|
self.assertIn('-d "NoticeFile=$noticeFile"', source)
|
|
self.assertIn("wails build -s -skipbindings -trimpath", source)
|
|
self.assertNotIn("Install UPX (Windows)", source)
|
|
self.assertNotIn("upx", windows_packaging.lower())
|
|
self.assertNotIn('-d "ProductName=GoNavi Dev"', source)
|
|
self.assertNotIn('-d "InstallFolderName=GoNavi Dev"', source)
|
|
self.assertNotIn('-d "RegistryKeyName=GoNavi Dev"', source)
|
|
self.assertNotIn("DA3DACF2-1E4B-428C-8E6F-A37D3A05CAF7", source)
|
|
self.assertNotIn(
|
|
'$finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe"',
|
|
source,
|
|
)
|
|
|
|
dev_source = WORKFLOWS[1].read_text(encoding="utf-8")
|
|
self.assertIn('$productVersion = "255.0.$runNumber"', dev_source)
|
|
self.assertNotIn('$productVersion = "0.0.$runNumber"', dev_source)
|
|
|
|
publish_source = PUBLISH_WORKFLOW.read_text(encoding="utf-8")
|
|
for arch in ("Amd64", "Arm64"):
|
|
self.assertIn(f"GoNavi-${{version}}-Windows-{arch}-Portable.exe", publish_source)
|
|
self.assertIn(f"GoNavi-${{version}}-Windows-{arch}-Portable.zip", publish_source)
|
|
|
|
def test_local_windows_release_keeps_unpacked_trimmed_executables(self) -> None:
|
|
source = LOCAL_RELEASE_SCRIPT.read_text(encoding="utf-8")
|
|
windows_builds = source.split("# --- Windows AMD64", 1)[1].split("# --- Linux AMD64", 1)[0]
|
|
|
|
self.assertIn("wails build -trimpath -platform windows/amd64", windows_builds)
|
|
self.assertIn("wails build -trimpath -platform windows/arm64", windows_builds)
|
|
self.assertNotIn("upx", windows_builds.lower())
|
|
|
|
def test_installer_declares_upgrade_shortcuts_and_uninstall_metadata(self) -> None:
|
|
root = ET.parse(INSTALLER).getroot()
|
|
ns = {"wix": WIX_NAMESPACE}
|
|
package = root.find("wix:Package", ns)
|
|
self.assertIsNotNone(package)
|
|
assert package is not None
|
|
self.assertEqual(package.attrib["Scope"], "perMachine")
|
|
self.assertEqual(package.attrib["InstallerVersion"], "500")
|
|
self.assertEqual(package.attrib["ProductCode"], "*")
|
|
major_upgrade = package.find("wix:MajorUpgrade", ns)
|
|
self.assertIsNotNone(major_upgrade)
|
|
assert major_upgrade is not None
|
|
self.assertEqual(major_upgrade.attrib["AllowDowngrades"], "yes")
|
|
self.assertEqual(major_upgrade.attrib["Schedule"], "afterInstallInitialize")
|
|
self.assertNotIn("AllowSameVersionUpgrades", major_upgrade.attrib)
|
|
self.assertNotIn("DowngradeErrorMessage", major_upgrade.attrib)
|
|
self.assertIsNotNone(package.find("wix:MediaTemplate", ns))
|
|
self.assertIsNotNone(package.find("wix:Property[@Id='ARPPRODUCTICON']", ns))
|
|
desktop_property = package.find("wix:Property[@Id='INSTALLDESKTOPSHORTCUT']", ns)
|
|
self.assertIsNotNone(desktop_property)
|
|
assert desktop_property is not None
|
|
self.assertEqual(desktop_property.attrib["Value"], "1")
|
|
self.assertEqual(desktop_property.attrib["Secure"], "yes")
|
|
self.assertIsNotNone(package.find("wix:SetProperty[@Id='ARPINSTALLLOCATION']", ns))
|
|
|
|
main_feature = package.find("wix:Feature[@Id='MainFeature']", ns)
|
|
self.assertIsNotNone(main_feature)
|
|
assert main_feature is not None
|
|
self.assertIsNotNone(main_feature.find("wix:ComponentRef[@Id='DesktopShortcutComponent']", ns))
|
|
|
|
executable = package.find(".//wix:File[@Id='GoNaviExe']", ns)
|
|
self.assertIsNotNone(executable)
|
|
assert executable is not None
|
|
start_menu_shortcut = executable.find("wix:Shortcut[@Id='StartMenuShortcut']", ns)
|
|
self.assertIsNotNone(start_menu_shortcut)
|
|
assert start_menu_shortcut is not None
|
|
self.assertNotIn("Icon", start_menu_shortcut.attrib)
|
|
self.assertIsNone(executable.find("wix:Shortcut[@Id='DesktopShortcut']", ns))
|
|
|
|
desktop_component = package.find("wix:Component[@Id='DesktopShortcutComponent']", ns)
|
|
self.assertIsNotNone(desktop_component)
|
|
assert desktop_component is not None
|
|
self.assertEqual(desktop_component.attrib["Directory"], "DesktopFolder")
|
|
self.assertEqual(desktop_component.attrib["Guid"], "$(var.DesktopShortcutComponentGuid)")
|
|
self.assertEqual(desktop_component.attrib["Bitness"], "always64")
|
|
self.assertEqual(desktop_component.attrib["Condition"], "INSTALLDESKTOPSHORTCUT = 1")
|
|
desktop_shortcut = desktop_component.find("wix:Shortcut[@Id='DesktopShortcut']", ns)
|
|
self.assertIsNotNone(desktop_shortcut)
|
|
assert desktop_shortcut is not None
|
|
self.assertNotIn("Icon", desktop_shortcut.attrib)
|
|
self.assertEqual(desktop_shortcut.attrib["Target"], "[#GoNaviExe]")
|
|
self.assertEqual(desktop_shortcut.attrib["WorkingDirectory"], "INSTALLFOLDER")
|
|
desktop_key_path = desktop_component.find("wix:RegistryValue[@KeyPath='yes']", ns)
|
|
self.assertIsNotNone(desktop_key_path)
|
|
assert desktop_key_path is not None
|
|
self.assertEqual(desktop_key_path.attrib["Root"], "HKLM")
|
|
self.assertEqual(desktop_key_path.attrib["Key"], r"Software\Syngnat\$(var.RegistryKeyName)")
|
|
self.assertEqual(desktop_key_path.attrib["Name"], "DesktopShortcutInstalled")
|
|
self.assertEqual(desktop_key_path.attrib["Value"], "1")
|
|
self.assertEqual(desktop_key_path.attrib["Type"], "integer")
|
|
marker = package.find(".//wix:RegistryValue[@Name='InstallType']", ns)
|
|
self.assertIsNotNone(marker)
|
|
assert marker is not None
|
|
self.assertEqual(marker.attrib["Value"], "MSI")
|
|
install_path = package.find(".//wix:RegistryValue[@Name='InstallPath']", ns)
|
|
self.assertIsNotNone(install_path)
|
|
assert install_path is not None
|
|
self.assertEqual(install_path.attrib["Value"], "[INSTALLFOLDER]GoNavi.exe")
|
|
file_marker = package.find(".//wix:File[@Id='GoNaviMsiInstallMarker']", ns)
|
|
self.assertIsNotNone(file_marker)
|
|
assert file_marker is not None
|
|
self.assertEqual(file_marker.attrib["Source"], "$(var.InstallMarker)")
|
|
self.assertEqual(file_marker.attrib["Name"], ".gonavi-msi-install")
|
|
license_file = package.find(".//wix:File[@Id='GoNaviLicense']", ns)
|
|
self.assertIsNotNone(license_file)
|
|
assert license_file is not None
|
|
self.assertEqual(license_file.attrib["Source"], "$(var.LicenseFile)")
|
|
self.assertEqual(license_file.attrib["Name"], "LICENSE.txt")
|
|
notice_file = package.find(".//wix:File[@Id='GoNaviNotice']", ns)
|
|
self.assertIsNotNone(notice_file)
|
|
assert notice_file is not None
|
|
self.assertEqual(notice_file.attrib["Source"], "$(var.NoticeFile)")
|
|
self.assertEqual(notice_file.attrib["Name"], "NOTICE.txt")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|