neser 1.2.0

NESER - Nintendo Emulation Systems Engine (Rust). Desktop and WebAssembly frontends.
Documentation
"""Unit tests for release package verification."""

import tarfile
import tempfile
import unittest
import zipfile
from pathlib import Path
from unittest.mock import patch

from scripts.verify_release_package import (
    ReleasePackageVerificationError,
    VerificationConfig,
    _run_smoke_command,
    main,
    verify_release_package,
)


def write_text(path: Path, text: str) -> None:
    """Write UTF-8 text, creating parent directories first."""

    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")


def create_package_tree(root: Path, *, binary_name: str = "neser") -> Path:
    """Create a minimal extracted release package tree."""

    package_root = root / "neser"
    write_text(package_root / "assets/fonts/NunitoSans-Regular.ttf", "font\n")
    write_text(package_root / "gamecontrollerdb.txt", "controller mappings\n")
    write_text(package_root / "neser.conf.example", "# config\n")
    write_text(package_root / "README.md", "# neser\n")
    write_text(package_root / "README-NES.md", "# nes\n")
    write_text(package_root / "README-GB.md", "# gb\n")
    write_text(package_root / "README-GBA.md", "# gba\n")
    write_text(package_root / "LICENSE", "license\n")
    write_text(package_root / "shaders/stock.slangp", "shader0 = stock.slang\n")
    write_text(package_root / "shaders/stock.slang", "void main() {}\n")
    binary_path = package_root / binary_name
    write_text(binary_path, "#!/bin/sh\nprintf 'neser 1.0.0\\n'\n")
    binary_path.chmod(0o755)
    return package_root


def create_tar_gz(package_root: Path, archive_path: Path) -> None:
    """Create a tar.gz archive from an extracted package root."""

    with tarfile.open(archive_path, "w:gz") as archive:
        archive.add(package_root, arcname="neser")


def create_zip(package_root: Path, archive_path: Path) -> None:
    """Create a zip archive from an extracted package root."""

    with zipfile.ZipFile(archive_path, "w") as archive:
        for path in sorted(package_root.rglob("*")):
            if path.is_file():
                archive.write(path, Path("neser") / path.relative_to(package_root))


class VerifyReleasePackageTests(unittest.TestCase):
    """Behavior tests for release archive verification."""

    def test_verify_tar_gz_package_accepts_required_layout_and_smoke_command(
        self,
    ) -> None:
        """A complete Unix package passes manifest, permission, and smoke checks."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            create_tar_gz(package_root, archive_path)

            verify_release_package(
                VerificationConfig(
                    archive_path=archive_path,
                    binary_name="neser",
                    smoke_command=["./neser", "--version"],
                    require_unix_executable=True,
                )
            )

    def test_verify_zip_package_accepts_required_windows_layout(self) -> None:
        """A complete Windows zip package passes manifest checks."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(
                temp_dir / "src", binary_name="neser.exe"
            )
            archive_path = temp_dir / "neser-windows-x86_64.zip"
            create_zip(package_root, archive_path)

            verify_release_package(
                VerificationConfig(
                    archive_path=archive_path,
                    binary_name="neser.exe",
                )
            )

    def test_verify_package_reports_missing_required_resource(self) -> None:
        """Missing runtime resources fail with the missing archive path."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            (package_root / "gamecontrollerdb.txt").unlink()
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            create_tar_gz(package_root, archive_path)

            with self.assertRaisesRegex(
                ReleasePackageVerificationError, "neser/gamecontrollerdb.txt"
            ):
                verify_release_package(
                    VerificationConfig(
                        archive_path=archive_path,
                        binary_name="neser",
                        require_unix_executable=True,
                    )
                )

    def test_verify_package_reports_missing_system_readme(self) -> None:
        """Release archives must keep system-specific README links valid."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            (package_root / "README-NES.md").unlink()
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            create_tar_gz(package_root, archive_path)

            with self.assertRaisesRegex(
                ReleasePackageVerificationError, "neser/README-NES.md"
            ):
                verify_release_package(
                    VerificationConfig(
                        archive_path=archive_path,
                        binary_name="neser",
                    )
                )

    def test_verify_package_requires_default_shader_source(self) -> None:
        """The verifier requires the shader source referenced by stock.slangp."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            (package_root / "shaders/stock.slang").unlink()
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            create_tar_gz(package_root, archive_path)

            with self.assertRaisesRegex(
                ReleasePackageVerificationError, "neser/shaders/stock.slang"
            ):
                verify_release_package(
                    VerificationConfig(
                        archive_path=archive_path,
                        binary_name="neser",
                    )
                )

    def test_verify_tar_gz_package_rejects_link_members(self) -> None:
        """Tar packages with symlink members are rejected before extraction."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            with tarfile.open(archive_path, "w:gz") as archive:
                archive.add(package_root, arcname="neser")
                link_info = tarfile.TarInfo("neser/shaders/escape")
                link_info.type = tarfile.SYMTYPE
                link_info.linkname = "../../outside"
                archive.addfile(link_info)

            with self.assertRaisesRegex(
                ReleasePackageVerificationError, "unsupported tar link"
            ):
                verify_release_package(
                    VerificationConfig(
                        archive_path=archive_path,
                        binary_name="neser",
                    )
                )

    def test_verify_package_reports_unsupported_single_suffix_archive(self) -> None:
        """Unsupported archive suffixes fail with a clean verifier error."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            archive_path = Path(temp_dir_str) / "neser-linux-x86_64.tgz"
            archive_path.write_bytes(b"not an archive")

            with self.assertRaisesRegex(
                ReleasePackageVerificationError, "unsupported archive format"
            ):
                verify_release_package(
                    VerificationConfig(
                        archive_path=archive_path,
                        binary_name="neser",
                    )
                )

    def test_main_verifies_archive_from_cli_arguments(self) -> None:
        """CLI arguments verify an archive and run the smoke command."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            temp_dir = Path(temp_dir_str)
            package_root = create_package_tree(temp_dir / "src")
            archive_path = temp_dir / "neser-linux-x86_64.tar.gz"
            create_tar_gz(package_root, archive_path)

            exit_code = main(
                [
                    str(archive_path),
                    "--binary-name",
                    "neser",
                    "--require-unix-executable",
                    "--smoke-command",
                    "./neser",
                    "--version",
                ]
            )

            self.assertEqual(exit_code, 0)

    def test_smoke_command_resolves_cwd_relative_executable_before_running(
        self,
    ) -> None:
        """Given a cwd-relative executable, the verifier runs its extracted path."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            package_root = Path(temp_dir_str) / "neser"
            package_root.mkdir()
            executable_path = package_root / "neser.exe"

            with patch("scripts.verify_release_package.subprocess.run") as run:
                run.return_value.returncode = 0

                _run_smoke_command(["./neser.exe", "--version"], package_root)

            run.assert_called_once()
            command = run.call_args.args[0]
            self.assertEqual(command[0], str(executable_path))
            self.assertEqual(command[1:], ["--version"])

    def test_smoke_command_resolves_windows_style_subdirectory_executable(
        self,
    ) -> None:
        """Given a Windows-style cwd-relative executable, separators are normalized."""

        with tempfile.TemporaryDirectory() as temp_dir_str:
            package_root = Path(temp_dir_str) / "neser"
            executable_path = package_root / "bin" / "neser.exe"

            with patch("scripts.verify_release_package.subprocess.run") as run:
                run.return_value.returncode = 0

                _run_smoke_command([".\\bin\\neser.exe", "--version"], package_root)

            run.assert_called_once()
            command = run.call_args.args[0]
            self.assertEqual(command[0], str(executable_path))
            self.assertEqual(command[1:], ["--version"])


if __name__ == "__main__":
    unittest.main()