Files
nyaterm/scripts/tests/test_package_native.py
T
KangandClaude Opus 5 (1M context) f07e2745df feat(remote-desktop): introduce nyaterm-vnc-helper and enhance IPC handling
Added the `nyaterm-vnc-helper` crate to manage VNC sessions in a separate process, improving security and stability. Updated the IPC protocol to facilitate communication between the main application and the VNC helper, including new control messages and error handling. Refactored existing code to utilize helper processes for both RDP and VNC, ensuring consistent session management and error reporting.

- Updated Cargo.toml and Cargo.lock to include the new helper.
- Refactored remote desktop session management to support helper processes.
- Enhanced error handling for VNC-specific scenarios.
- Added tests for the new VNC helper functionality.

Co-Authored-By: Claude Opus 5 (1M context)
2026-08-20 17:18:34 +08:00

218 lines
9.2 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_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)
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")
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)
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:
with mock.patch.dict("os.environ", {"CARGO_TARGET_DIR": "/cache/cargo"}):
path = package_native.release_binary_path("x86_64-unknown-linux-gnu")
self.assertEqual(
path.as_posix(), "/cache/cargo/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()