Files
nyaterm/scripts/tests/test_package_native.py
T
Kang 105317b106 build(release): package the MCP helper
Ship nyaterm-mcp beside the application in native packages and verify its presence in packaging tests and AUR artifacts.
2026-09-01 10:24:09 +08:00

268 lines
12 KiB
Python

from __future__ import annotations
import struct
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
RELEASE_SCRIPTS = Path(__file__).resolve().parents[1] / "release"
sys.path.insert(0, str(RELEASE_SCRIPTS))
import package_native # noqa: E402
class PackageNativeTests(unittest.TestCase):
def test_release_tag_is_normalized(self) -> None:
self.assertEqual(package_native.validate_version("v2.0.0"), "2.0.0")
self.assertEqual(
package_native.validate_version("2.0.0-preview.1"),
"2.0.0-preview.1",
)
def test_invalid_or_mismatched_version_is_rejected(self) -> None:
with self.assertRaises(ValueError):
package_native.validate_version("release-2")
with self.assertRaisesRegex(ValueError, "does not match"):
package_native.validate_version("v2.0.1", "2.0.0")
def test_snapshot_is_only_allowed_as_an_artifact_label(self) -> None:
self.assertEqual(
package_native.validate_artifact_version("main-snapshot"),
"main-snapshot",
)
with self.assertRaises(ValueError):
package_native.validate_version("main-snapshot")
with self.assertRaises(ValueError):
package_native.validate_artifact_version("nightly")
self.assertEqual(
package_native.artifact_names(
"x86_64-pc-windows-msvc", "main-snapshot"
),
{
"NyaTerm_main-snapshot_windows_x64_portable.zip",
"NyaTerm_main-snapshot_windows_x64-setup.exe",
},
)
def test_all_release_targets_have_expected_artifact_names(self) -> None:
expected = {
"aarch64-apple-darwin": {
"NyaTerm_2.0.0_macos_arm64.dmg",
"NyaTerm_2.0.0_macos_arm64.app.tar.gz",
},
"x86_64-apple-darwin": {
"NyaTerm_2.0.0_macos_x64.dmg",
"NyaTerm_2.0.0_macos_x64.app.tar.gz",
},
"aarch64-unknown-linux-gnu": {
"NyaTerm_2.0.0_linux_arm64.AppImage",
"NyaTerm_2.0.0_linux_arm64.deb",
"NyaTerm_2.0.0_linux_arm64.rpm",
},
"x86_64-unknown-linux-gnu": {
"NyaTerm_2.0.0_linux_x64.AppImage",
"NyaTerm_2.0.0_linux_x64.deb",
"NyaTerm_2.0.0_linux_x64.rpm",
},
"aarch64-pc-windows-msvc": {
"NyaTerm_2.0.0_windows_arm64_portable.zip",
"NyaTerm_2.0.0_windows_arm64-setup.exe",
},
"x86_64-pc-windows-msvc": {
"NyaTerm_2.0.0_windows_x64_portable.zip",
"NyaTerm_2.0.0_windows_x64-setup.exe",
},
}
for target, names in expected.items():
with self.subTest(target=target):
self.assertEqual(package_native.artifact_names(target, "v2.0.0"), names)
def test_release_binary_always_uses_explicit_target_directory(self) -> None:
with mock.patch.dict("os.environ", {}, clear=True):
linux = package_native.release_binary_path("x86_64-unknown-linux-gnu")
windows = package_native.release_binary_path("aarch64-pc-windows-msvc")
self.assertEqual(
linux.relative_to(package_native.ROOT_DIR).as_posix(),
"target/x86_64-unknown-linux-gnu/release/nyaterm",
)
self.assertEqual(
windows.relative_to(package_native.ROOT_DIR).as_posix(),
"target/aarch64-pc-windows-msvc/release/nyaterm.exe",
)
def test_helper_binaries_resolve_beside_the_application(self) -> None:
self.assertIn("nyaterm-rdp-helper", package_native.HELPER_BINS)
self.assertIn("nyaterm-mcp", package_native.HELPER_BINS)
with mock.patch.dict("os.environ", {}, clear=True):
linux = package_native.helper_binary_paths("x86_64-unknown-linux-gnu")
windows = package_native.helper_binary_paths("aarch64-pc-windows-msvc")
self.assertEqual(
[path.name for path in linux], list(package_native.HELPER_BINS)
)
self.assertEqual(
[path.name for path in windows],
[f"{name}.exe" for name in package_native.HELPER_BINS],
)
application = package_native.release_binary_path("x86_64-unknown-linux-gnu")
for path in linux:
self.assertEqual(path.parent, application.parent)
def test_copy_helpers_stages_every_helper_beside_the_application(self) -> None:
target = "x86_64-unknown-linux-gnu"
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
sources = root / "build"
destination = root / "package"
destination.mkdir()
staged = []
for path in package_native.helper_binary_paths(target):
fake = sources / path.name
fake.parent.mkdir(parents=True, exist_ok=True)
fake.write_bytes(b"helper")
staged.append(fake)
with mock.patch.object(
package_native, "helper_binary_paths", return_value=staged
):
copied = package_native.copy_helpers(destination, target)
self.assertEqual(
sorted(path.name for path in copied),
sorted(package_native.HELPER_BINS),
)
for path in copied:
self.assertTrue(path.is_file())
self.assertEqual(path.parent, destination)
def test_windows_installer_script_installs_and_removes_every_helper(self) -> None:
target = "x86_64-pc-windows-msvc"
info = package_native.target_info(target)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
with (
mock.patch.object(package_native, "WORK_DIR", root / "work"),
mock.patch.object(package_native, "DIST_DIR", root / "dist"),
mock.patch.object(package_native, "run"),
mock.patch.object(
package_native, "find_makensis", return_value="makensis"
),
):
package_native.WORK_DIR.mkdir(parents=True)
package_native.DIST_DIR.mkdir(parents=True)
application = root / "nyaterm.exe"
application.write_bytes(b"MZ")
for path in package_native.helper_binary_paths(target):
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(b"MZ")
package_native.create_windows_packages(
application, info, "2.0.0", "2.0.0"
)
script = (
package_native.WORK_DIR / "nyaterm-installer.nsi"
).read_text(encoding="utf-8")
for name in package_native.HELPER_BINS:
filename = f"{name}.exe"
with self.subTest(helper=filename):
self.assertRegex(script, rf'File ".*{filename}"')
self.assertIn(f'Delete "$INSTDIR\\{filename}"', script)
self.assertIn(
r'WriteRegStr HKCU "Software\Classes\nyaterm" "URL Protocol" ""',
script,
)
self.assertIn(
r'WriteRegStr HKCU "Software\Classes\nyaterm\shell\open\command" "" "$\"$INSTDIR\NyaTerm.exe$\" $\"%1$\""',
script,
)
self.assertIn(
r'DeleteRegKey HKCU "Software\Classes\nyaterm"',
script,
)
self.assertNotIn(r"Software\Classes\ssh", script)
self.assertNotIn(r"Software\Classes\telnet", script)
def test_linux_desktop_registers_only_nyaterm_url_scheme(self) -> None:
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "nyaterm.desktop"
package_native.write_desktop_file(path, "/opt/nyaterm/nyaterm")
desktop = path.read_text(encoding="utf-8")
self.assertIn("Exec=/opt/nyaterm/nyaterm %U\n", desktop)
self.assertIn("MimeType=x-scheme-handler/nyaterm;\n", desktop)
self.assertNotIn("x-scheme-handler/ssh", desktop)
self.assertNotIn("x-scheme-handler/telnet", desktop)
def test_deb_dependencies_cover_helper_binaries(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
binaries = [root / "nyaterm", root / "nyaterm-rdp-helper"]
with (
mock.patch.object(package_native, "WORK_DIR", root / "work"),
mock.patch.object(
package_native, "require_tool", return_value="dpkg-shlibdeps"
),
mock.patch.object(
package_native.subprocess,
"check_output",
return_value="shlibs:Depends=libc6, libx11-6\n",
) as check_output,
):
dependencies = package_native.linux_deb_dependencies(binaries)
self.assertEqual(dependencies, "libc6, libx11-6")
command = check_output.call_args.args[0]
for binary in binaries:
self.assertIn(str(binary), command)
self.assertEqual(command.count("-e"), len(binaries))
def test_release_binary_respects_absolute_cargo_target_dir(self) -> None:
# Build the absolute path for the running platform: Path("/cache/cargo") has
# no drive letter, so is_absolute() is False on Windows and the assertion
# would compare against a path joined onto the repository root instead.
target_dir = Path(tempfile.gettempdir(), "nyaterm-cargo-target").resolve()
with mock.patch.dict("os.environ", {"CARGO_TARGET_DIR": str(target_dir)}):
path = package_native.release_binary_path("x86_64-unknown-linux-gnu")
self.assertEqual(
path, target_dir / "x86_64-unknown-linux-gnu" / "release" / "nyaterm"
)
def test_platform_package_versions_are_normalized(self) -> None:
self.assertEqual(package_native.windows_numeric_version("2.4.6-beta.1"), "2.4.6.0")
self.assertEqual(package_native.linux_rpm_version("2.4.6"), ("2.4.6", "1"))
self.assertEqual(
package_native.linux_rpm_version("2.4.6-beta.1"),
("2.4.6", "0.beta.1"),
)
def test_dpkg_dependency_output_is_parsed(self) -> None:
output = "ignored=value\nshlibs:Depends=libc6 (>= 2.34), libx11-6\n"
self.assertEqual(
package_native.parse_dpkg_dependencies(output),
"libc6 (>= 2.34), libx11-6",
)
with self.assertRaises(RuntimeError):
package_native.parse_dpkg_dependencies("shlibs:Depends=\n")
def test_native_icon_resources_have_expected_formats_and_sizes(self) -> None:
expected_png_sizes = {
"32x32.png": (32, 32),
"64x64.png": (64, 64),
"128x128.png": (128, 128),
"256x256.png": (256, 256),
"512x512.png": (512, 512),
}
for name, expected_size in expected_png_sizes.items():
with self.subTest(name=name):
data = (package_native.ICON_DIR / name).read_bytes()
self.assertEqual(data[:8], b"\x89PNG\r\n\x1a\n")
self.assertEqual(struct.unpack(">II", data[16:24]), expected_size)
self.assertEqual(
(package_native.ICON_DIR / "icon.icns").read_bytes()[:4], b"icns"
)
self.assertEqual(
(package_native.ICON_DIR / "icon.ico").read_bytes()[:4], b"\0\0\1\0"
)
if __name__ == "__main__":
unittest.main()